diff options
-rw-r--r-- | contrib/vim/README | 61 | ||||
-rw-r--r-- | contrib/vim/ftplugin/ledger.vim | 196 |
2 files changed, 242 insertions, 15 deletions
diff --git a/contrib/vim/README b/contrib/vim/README index 7c56f6da..4da73ea6 100644 --- a/contrib/vim/README +++ b/contrib/vim/README @@ -11,17 +11,60 @@ Configuration Include the following let-statements somewhere in your .vimrc to modify the behaviour of the ledger filetype. -Number of colums that will be used to display the foldtext. -Set this when you think that the amount is too far off to the right. -let g:ledger_maxwidth = 80 +* Number of colums that will be used to display the foldtext. + Set this when you think that the amount is too far off to the right. + let g:ledger_maxwidth = 80 -String that will be used to fill the space between account name -and amount in the foldtext. Set this to get some kind of lines -or visual aid. -let g:ledger_fillstring = ' -' +* String that will be used to fill the space between account name + and amount in the foldtext. Set this to get some kind of lines + or visual aid. + let g:ledger_fillstring = ' -' + My special tip is to use so-called digraphs: + Press <C-K> followed by the two-characters key sequence below. + (in insert-mode) + '. = ˙ or ': = ¨ --> ˙˙˙˙˙˙ or ¨¨¨¨¨¨ + ', = ¸ --> ¸¸¸¸¸¸ + .M = · --> ······ + >> = » --> »»»»»» + All those look rather unobstrusive + and provide a good visual aid to find the correct amount. -Revision history +* If you want the account completion to be sorted by level of detail/depth + instead of alphabetical, include the following line: + let g:ledger_detailed_first = 1 + +Completion +====================================================================== +Omni completion is implemented for account names and tags. + +Accounts +---------------------------------------------------------------------- +Account names are matched by the start of every sub-level. +When you insert an account name like this: + Asse<C-X><C-O> +You will get a list of top-level accounts that start like this. + +Go ahead and try something like: + As:Ban:Che<C-X><C-O> +When you have an account like this, 'Assets:Bank:Checking' should show up. + +When you want to complete on a virtual transaction, +it's currently best to keep the cursor in front of the closing bracket. +Of course you can insert the closing bracket after calling the completion, too. + +Tags +---------------------------------------------------------------------- +The support for completing tags is pretty basic right now +but it's useful to keep the spelling of your tags consistent. +You can call the completion after the ';' to get a list of all tags. +When you have a list of tags (:like: :this:) you can call +the completion too and everything up to the last ':' (excluding whitespace) +will be considered the beginning of the tag to search for. + +Revision history (major changes) ====================================================================== + 2009-06-23 & 2009-06-25 + J. Klähn: Omni-Completion for account names and tags 2009-06-17 J. Klähn: Highlight account text Updated documentation and added fillstring option. 2009-06-15 J. Klähn: Split into multiple files @@ -37,7 +80,7 @@ Revision history 2005-02-05 first version (partly copied from ledger.vim 0.0.1) License -======= +====================================================================== Copyright 2009 by Johann Klähn Copyright 2009 by Stefan Karrmann Copyright 2005 by Wolfgang Oertl diff --git a/contrib/vim/ftplugin/ledger.vim b/contrib/vim/ftplugin/ledger.vim index a2a4c468..d75a6869 100644 --- a/contrib/vim/ftplugin/ledger.vim +++ b/contrib/vim/ftplugin/ledger.vim @@ -12,7 +12,7 @@ let b:did_ftplugin = 1 let b:undo_ftplugin = "setlocal ". \ "foldmethod< foldtext< ". - \ "include< comments< iskeyword< " + \ "include< comments< omnifunc< " " don't fill fold lines --> cleaner look setl fillchars="fold: " @@ -20,10 +20,7 @@ setl foldtext=LedgerFoldText() setl foldmethod=syntax setl include=^!include setl comments=b:; -" so you can use C-X C-N completion on accounts -" FIXME: Does not work with something like: -" Assets:Accountname with Spaces -setl iskeyword+=: +setl omnifunc=LedgerComplete " You can set a maximal number of columns the fold text (excluding amount) " will use by overriding g:ledger_maxwidth in your .vimrc. @@ -37,6 +34,30 @@ if !exists('g:ledger_fillstring') let g:ledger_fillstring = ' ' endif +" If enabled this will list the most detailed matches at the top {{{ +" of the completion list. +" For example when you have some accounts like this: +" A:Ba:Bu +" A:Bu:Bu +" and you complete on A:B:B normal behaviour may be the following +" A:B:B +" A:Bu:Bu +" A:Bu +" A:Ba:Bu +" A:Ba +" A +" with this option turned on it will be +" A:B:B +" A:Bu:Bu +" A:Ba:Bu +" A:Bu +" A:Ba +" A +" }}} +if !exists('g:ledger_detailed_first') + let g:ledger_detailed_first = 0 +endif + let s:rx_amount = '\('. \ '\%([0-9]\+\)'. \ '\%([,.][0-9]\+\)*'. @@ -54,7 +75,7 @@ function! LedgerFoldText() "{{{1 let line = getline(lnum) " Skip metadata/leading comment - if line !~ '^\s\+;' + if line !~ '^\%(\s\+;\|\d\)' " No comment, look for amount... let groups = matchlist(line, s:rx_amount) if ! empty(groups) @@ -97,11 +118,149 @@ function! LedgerFoldText() "{{{1 return printf(fmt, foldtext, amount) endfunction "}}} +function! LedgerComplete(findstart, base) "{{{1 + if a:findstart + let lnum = line('.') + let line = getline('.') + let lastcol = col('.') - 2 + if line =~ '^\d' "{{{2 (date / payee / description) + let b:compl_context = 'payee' + return -1 + elseif line =~ '^\s\+;' "{{{2 (metadata / tags) + let b:compl_context = 'meta-tag' + let first_possible = matchend(line, '^\s\+;') + + " find first column of text to be replaced + let firstcol = lastcol + while firstcol >= 0 + if firstcol <= first_possible + " Stop before the ';' don't ever include it + let firstcol = first_possible + break + elseif line[firstcol] =~ ':' + " Stop before first ':' + let firstcol += 1 + break + endif + + let firstcol -= 1 + endwhile + + " strip whitespace starting from firstcol + let end_of_whitespace = matchend(line, '^\s\+', firstcol) + if end_of_whitespace != -1 + let firstcol = end_of_whitespace + endif + + return firstcol + elseif line =~ '^\s\+' "{{{2 (account) + let b:compl_context = 'account' + if matchend(line, '^\s\+\%(\S \S\|\S\)\+') <= lastcol + " only allow completion when in or at end of account name + return -1 + endif + " the start of the first non-blank character + " (excluding virtual-transaction-marks) + " is the beginning of the account name + return matchend(line, '^\s\+[\[(]\?') + else "}}} + return -1 + endif + else + if b:compl_context == 'account' "{{{2 (account) + unlet! b:compl_context + let hierarchy = split(a:base, ':') + if a:base =~ ':$' + call add(hierarchy, '') + endif + + let results = LedgerFindInTree(LedgerGetAccountHierarchy(), hierarchy) + " sort by alphabet and reverse because it will get reversed one more time + let results = reverse(sort(results)) + if g:ledger_detailed_first + let results = sort(results, 's:sort_accounts_by_depth') + endif + call add(results, a:base) + return reverse(results) + elseif b:compl_context == 'meta-tag' "{{{2 + unlet! b:compl_context + let results = [a:base] + call extend(results, sort(s:filter_items(keys(LedgerGetTags()), a:base))) + return results + else "}}} + unlet! b:compl_context + return [] + endif + endif +endf "}}} + +function! LedgerFindInTree(tree, levels) "{{{1 + if empty(a:levels) + return [] + endif + let results = [] + let currentlvl = a:levels[0] + let nextlvls = a:levels[1:] + let branches = s:filter_items(keys(a:tree), currentlvl) + for branch in branches + call add(results, branch) + if !empty(nextlvls) + for result in LedgerFindInTree(a:tree[branch], nextlvls) + call add(results, branch.':'.result) + endfor + endif + endfor + return results +endf "}}} + +function! LedgerGetAccountHierarchy() "{{{1 + let hierarchy = {} + let accounts = s:grep_buffer('^\s\+\zs[^[:blank:];]\%(\S \S\|\S\)\+\ze') + for name in accounts + " remove virtual-transaction-marks + let name = substitute(name, '\%(^\s*[\[(]\?\|[\])]\?\s*$\)', '', 'g') + let last = hierarchy + for part in split(name, ':') + let last[part] = get(last, part, {}) + let last = last[part] + endfor + endfor + return hierarchy +endf "}}} + +function! LedgerGetTags() "{{{1 + let alltags = {} + let metalines = s:grep_buffer('^\s\+;\s*\zs.*$') + for line in metalines + " (spaces at beginning are stripped by matchstr!) + if line[0] == ':' + " multiple tags + for val in split(line, ':') + if val !~ '^\s*$' + let name = s:strip_spaces(val) + let alltags[name] = get(alltags, name, []) + endif + endfor + elseif line =~ '^.*:.*$' + " line with tag=value + let name = s:strip_spaces(split(line, ':')[0]) + let val = s:strip_spaces(join(split(line, ':')[1:], ':')) + let values = get(alltags, name, []) + call add(values, val) + let alltags[name] = values + endif + endfor + return alltags +endf "}}} + " Helper functions {{{1 + +" return length of string with fix for multibyte characters function! s:multibyte_strlen(text) "{{{2 return strlen(substitute(a:text, ".", "x", "g")) endfunction "}}} +" get # of visible/usable columns in current window function! s:get_columns(win) "{{{2 " As long as vim doesn't provide a command natively, " we have to compute the available columns. @@ -116,3 +275,28 @@ function! s:get_columns(win) "{{{2 return columns endfunction "}}} +" remove spaces at start and end of string +function! s:strip_spaces(text) "{{{2 + return matchstr(a:text, '^\s*\zs\S\%(.*\S\)\?\ze\s*$') +endf "}}} + +" return only those items that start with a specified keyword +function! s:filter_items(list, keyword) "{{{2 + return filter(a:list, 'v:val =~ ''^\V'.substitute(a:keyword, '\\', '\\\\', 'g').'''') +endf "}}} + +" return all lines matching an expression, returning only the matched part +function! s:grep_buffer(expression) "{{{2 + let lines = map(getline(1, '$'), 'matchstr(v:val, '''.a:expression.''')') + return filter(lines, 'v:val != ""') +endf "}}} + +function! s:sort_accounts_by_depth(name1, name2) "{{{2 + let depth1 = s:count_expression(a:name1, ':') + let depth2 = s:count_expression(a:name2, ':') + return depth1 == depth2 ? 0 : depth1 > depth2 ? 1 : -1 +endf "}}} + +function! s:count_expression(text, expression) "{{{2 + return len(split(a:text, a:expression, 1))-1 +endf "}}} |