From 67aa83349f8dd791d932172ddbcc7ff1d9ebfae5 Mon Sep 17 00:00:00 2001 From: rbong Date: Mon, 26 Aug 2024 22:52:54 -0400 Subject: [PATCH] Generalize format specifier parsing --- autoload/flog.vim | 2 +- autoload/flog/floggraph/format.vim | 171 +++++++++++++++++++++ autoload/flog/format.vim | 234 ++++------------------------- 3 files changed, 204 insertions(+), 203 deletions(-) create mode 100644 autoload/flog/floggraph/format.vim diff --git a/autoload/flog.vim b/autoload/flog.vim index 88cae70..da04996 100644 --- a/autoload/flog.vim +++ b/autoload/flog.vim @@ -37,7 +37,7 @@ function! flog#ExecTmp(cmd, ...) abort endfunction function! flog#Format(cmd) abort - return flog#format#FormatCommand(a:cmd) + return flog#floggraph#format#FormatCommand(a:cmd) endfunction " Deprecations diff --git a/autoload/flog/floggraph/format.vim b/autoload/flog/floggraph/format.vim new file mode 100644 index 0000000..1d9cabb --- /dev/null +++ b/autoload/flog/floggraph/format.vim @@ -0,0 +1,171 @@ +" +" This file contains functions for formatting contextual Floggraph command specifiers. +" + +function! flog#floggraph#format#GetCacheCmdRefs(dict, commit) abort + let l:refs = flog#state#GetCommitRefs(a:commit) + let a:dict.refs[a:commit.hash] = l:refs + return l:refs +endfunction + +function! flog#floggraph#format#FormatHash(save) abort + let l:commit = flog#floggraph#commit#GetAtLine('.') + + if !empty(l:commit) + if a:save + call flog#floggraph#mark#SetInternal('!', '.') + endif + return l:commit.hash + endif + + return '' +endfunction + +function! flog#floggraph#format#FormatMarkHash(key) abort + let l:commit = flog#floggraph#mark#Get(a:key) + return empty(l:commit) ? '' : l:commit.hash +endfunction + +function! flog#floggraph#format#FormatCommitBranch(dict, commit) abort + let l:local_branch = '' + let l:remote_branch = '' + + for l:ref in flog#floggraph#format#GetCacheCmdRefs(a:dict, a:commit) + " Skip non-branches + if l:ref.tag || l:ref.tail =~# 'HEAD$' + continue + endif + + " Get local branch + if empty(l:ref.remote) && empty(l:ref.prefix) + let l:local_branch = l:ref.tail + break + endif + + " Get remote branch + if empty(l:remote_branch) && !empty(l:ref.remote) + let l:remote_branch = l:ref.path + endif + endfor + + let l:branch = empty(l:local_branch) ? l:remote_branch : l:local_branch + + return flog#shell#Escape(l:branch) +endfunction + +function! flog#floggraph#format#FormatBranch(dict) abort + let l:commit = flog#floggraph#commit#GetAtLine('.') + return flog#floggraph#format#FormatCommitBranch(a:dict, l:commit) +endfunction + +function! flog#floggraph#format#FormatMarkBranch(dict, key) abort + let l:commit = flog#floggraph#mark#Get(a:key) + return flog#floggraph#format#FormatCommitBranch(a:dict, l:commit) +endfunction + +function! flog#floggraph#format#FormatCommitLocalBranch(dict, commit) abort + let l:branch = flog#floggraph#format#FormatCommitBranch(a:dict, a:commit) + return substitute(l:branch, '.\{-}/', '', '') +endfunction + +function! flog#floggraph#format#FormatLocalBranch(dict) abort + let l:commit = flog#floggraph#commit#GetAtLine('.') + return flog#floggraph#format#FormatCommitLocalBranch(a:dict, l:commit) +endfunction + +function! flog#floggraph#format#FormatMarkLocalBranch(dict, key) abort + let l:commit = flog#floggraph#mark#Get(a:key) + return flog#floggraph#format#FormatCommitLocalBranch(a:dict, l:commit) +endfunction + +function! flog#floggraph#format#FormatPath() abort + let l:state = flog#state#GetBufState() + let l:path = l:state.opts.path + + if !empty(l:state.opts.limit) + let [l:range, l:limit_path] = flog#args#SplitGitLimitArg(l:state.opts.limit) + + if empty(l:limit_path) + return '' + endif + + let l:path = [l:limit_path] + elseif empty(l:path) + return '' + endif + + return join(flog#shell#EscapeList(l:path), ' ') +endfunction + +function! flog#floggraph#format#FormatIndexTree(dict) abort + if empty(a:dict.index_tree) + let l:cmd = flog#fugitive#GetGitCommand() + let l:cmd .= ' write-tree' + let a:dict.index_tree = flog#shell#Run(l:cmd)[0] + endif + return a:dict.index_tree +endfunction + +function! flog#floggraph#format#HandleCommandItem(dict, item, end) abort + let l:items = a:dict.items + + let l:formatted_item = '' + let l:save = v:true + + if a:item !~# '^%' + let l:formatted_item = a:item + let l:save = v:false + elseif has_key(l:items, a:item) + let l:formatted_item = l:items[a:item] + let l:save = v:false + elseif a:item ==# '%%' + let l:formatted_item = '%' + elseif a:item ==# '%h' + let l:formatted_item = flog#floggraph#format#FormatHash(v:true) + elseif a:item ==# '%H' + let l:formatted_item = flog#floggraph#format#FormatHash(v:false) + elseif a:item =~# "^%(h'." + let l:formatted_item = flog#floggraph#format#FormatMarkHash(a:item[4 : -2]) + elseif a:item =~# '%b' + let l:formatted_item = flog#floggraph#format#FormatBranch(a:dict) + elseif a:item =~# "^%(b'." + let l:formatted_item = flog#floggraph#format#FormatMarkBranch(a:dict, a:item[4 : -2]) + elseif a:item =~# '%l' + let l:formatted_item = flog#floggraph#format#FormatLocalBranch(a:dict) + elseif a:item =~# "^%(l'." + let l:formatted_item = flog#floggraph#format#FormatMarkLocalBranch(a:dict, a:item[4 : -2]) + elseif a:item ==# '%p' + let l:formatted_item = flog#floggraph#format#FormatPath() + elseif a:item ==# '%t' + let l:formatted_item = flog#floggraph#format#FormatIndexTree(a:dict) + else + call flog#print#err('error converting "%s"', a:item) + throw g:flog_unsupported_exec_format_item + endif + + if empty(l:formatted_item) + return -1 + endif + + if l:save + let l:items[a:item] = l:formatted_item + endif + + let a:dict.result .= l:formatted_item + return 1 +endfunction + +function! flog#floggraph#format#FormatCommand(str) abort + call flog#floggraph#buf#AssertFlogBuf() + + let l:dict = { + \ 'items': {}, + \ 'refs': {}, + \ 'index_tree': '', + \ 'result': '', + \ } + + call flog#format#ParseFormat(a:str, l:dict, function("flog#floggraph#format#HandleCommandItem")) + + return l:dict.result +endfunction diff --git a/autoload/flog/format.vim b/autoload/flog/format.vim index f7edc01..eca4f93 100644 --- a/autoload/flog/format.vim +++ b/autoload/flog/format.vim @@ -1,217 +1,47 @@ " -" This file contains utility functions for "flog#Format()". +" This file contains utility functions for working with format specifiers. " -function! flog#format#GetCacheRefs(cache, commit) abort - let l:ref_cache = a:cache.refs +function! flog#format#ParseFormat(str, dict, cb) abort + " Parse state + let l:parens = v:false + let l:item = '' - let l:refs = flog#state#GetCommitRefs(a:commit) - - let l:ref_cache[a:commit.hash] = l:refs - return l:refs -endfunction - -function! flog#format#FormatHash(save) abort - let l:commit = flog#floggraph#commit#GetAtLine('.') - - if !empty(l:commit) - if a:save - call flog#floggraph#mark#SetInternal('!', '.') - endif - return l:commit.hash - endif - - return '' -endfunction - -function! flog#format#FormatMarkHash(key) abort - let l:commit = flog#floggraph#mark#Get(a:key) - return empty(l:commit) ? '' : l:commit.hash -endfunction - -function! flog#format#FormatCommitBranch(cache, commit) abort - let l:local_branch = '' - let l:remote_branch = '' + " Parse character-by-character + for l:char in split(a:str, '\zs') + let l:item .= l:char - for l:ref in flog#format#GetCacheRefs(a:cache, a:commit) - " Skip non-branches - if l:ref.tag || l:ref.tail =~# 'HEAD$' + if l:item ==# '%' + " Item start + continue + elseif l:item ==# '%(' + " Paren start + let l:parens = v:true + continue + elseif l:parens && l:char !=# ')' + " Paren continue continue endif - " Get local branch - if empty(l:ref.remote) && empty(l:ref.prefix) - let l:local_branch = l:ref.tail - break - endif + " Handle item + let res = a:cb(a:dict, l:item, l:parens) - " Get remote branch - if empty(l:remote_branch) && !empty(l:ref.remote) - let l:remote_branch = l:ref.path + if res > 0 + " Item found + let l:item = '' + elseif res < 0 + " Abort + return endif - endfor - - let l:branch = empty(l:local_branch) ? l:remote_branch : l:local_branch - return flog#shell#Escape(l:branch) -endfunction - -function! flog#format#FormatBranch(cache) abort - let l:commit = flog#floggraph#commit#GetAtLine('.') - return flog#format#FormatCommitBranch(a:cache, l:commit) -endfunction - -function! flog#format#FormatMarkBranch(cache, key) abort - let l:commit = flog#floggraph#mark#Get(a:key) - return flog#format#FormatCommitBranch(a:cache, l:commit) -endfunction - -function! flog#format#FormatCommitLocalBranch(cache, commit) abort - let l:branch = flog#format#FormatCommitBranch(a:cache, a:commit) - return substitute(l:branch, '.\{-}/', '', '') -endfunction - -function! flog#format#FormatLocalBranch(cache) abort - let l:commit = flog#floggraph#commit#GetAtLine('.') - return flog#format#FormatCommitLocalBranch(a:cache, l:commit) -endfunction - -function! flog#format#FormatMarkLocalBranch(cache, key) abort - let l:commit = flog#floggraph#mark#Get(a:key) - return flog#format#FormatCommitLocalBranch(a:cache, l:commit) -endfunction - -function! flog#format#FormatPath() abort - let l:state = flog#state#GetBufState() - let l:path = l:state.opts.path - - if !empty(l:state.opts.limit) - let [l:range, l:limit_path] = flog#args#SplitGitLimitArg(l:state.opts.limit) - - if empty(l:limit_path) - return '' - endif - - let l:path = [l:limit_path] - elseif empty(l:path) - return '' - endif - - return join(flog#shell#EscapeList(l:path), ' ') -endfunction - -function! flog#format#FormatIndexTree(cache) abort - if empty(a:cache.index_tree) - let l:cmd = flog#fugitive#GetGitCommand() - let l:cmd .= ' write-tree' - let a:cache.index_tree = flog#shell#Run(l:cmd)[0] - endif - return a:cache.index_tree -endfunction - -function! flog#format#FormatItem(cache, item) abort - let l:item_cache = a:cache.items - - " Return cached items - - if has_key(l:item_cache, a:item) - return l:item_cache[a:item] - endif - - " Format the item - - let l:formatted_item = '' - - if a:item ==# 'h' - let l:formatted_item = flog#format#FormatHash(v:true) - elseif a:item ==# 'H' - let l:formatted_item = flog#format#FormatHash(v:false) - elseif a:item =~# "^h'." - let l:formatted_item = flog#format#FormatMarkHash(a:item[2 : ]) - elseif a:item =~# 'b' - let l:formatted_item = flog#format#FormatBranch(a:cache) - elseif a:item =~# "^b'." - let l:formatted_item = flog#format#FormatMarkBranch(a:cache, a:item[2 : ]) - elseif a:item =~# 'l' - let l:formatted_item = flog#format#FormatLocalBranch(a:cache) - elseif a:item =~# "^l'." - let l:formatted_item = flog#format#FormatMarkLocalBranch(a:cache, a:item[2 : ]) - elseif a:item ==# 'p' - let l:formatted_item = flog#format#FormatPath() - elseif a:item ==# 't' - let l:formatted_item = flog#format#FormatIndexTree(a:cache) - else - call flog#print#err('error converting "%s"', a:item) - throw g:flog_unsupported_exec_format_item - endif - - " Handle result - let l:item_cache[a:item] = l:formatted_item - return l:formatted_item -endfunction - -function! flog#format#FormatCommand(str) abort - call flog#floggraph#buf#AssertFlogBuf() - - " Special token flags - let l:is_in_item = v:false - let l:is_in_long_item = v:false - - " Special token data - let l:long_item = '' - - " Memoized data - let l:cache = { - \ 'items': {}, - \ 'refs': {}, - \ 'index_tree': '', - \ } - - " Return data - let l:result = '' - - for l:char in split(a:str, '\zs') - if l:is_in_long_item - " Parse characters in %() - - if l:char ==# ')' - " End long specifier - let l:formatted_item = flog#format#FormatItem(l:cache, l:long_item) - if empty(l:formatted_item) - return '' - endif - - let l:result .= l:formatted_item - let l:is_in_long_item = v:false - let l:long_item = '' - else - let l:long_item .= l:char - endif - elseif l:is_in_item - " Parse character after % - - if l:char ==# '(' - " Start long specifier - let l:is_in_long_item = v:true - else - " Parse specifier character - let l:formatted_item = flog#format#FormatItem(l:cache, l:char) - if empty(l:formatted_item) - return '' - endif - - let l:result .= l:formatted_item - endif - - let l:is_in_item = v:false - elseif l:char ==# '%' - " Start specifier - let l:is_in_item = v:true - else - " Append normal character - let l:result .= l:char + " End parens + if l:parens + let l:parens = v:false endif endfor - return l:result + " Handle final item + if l:item !=# '' + call a:cb(a:dict, l:item, v:true) + endif endfunction