summaryrefslogtreecommitdiff
path: root/contrib/vim/ftplugin/ledger.vim
diff options
context:
space:
mode:
authorJohn Wiegley <johnw@newartisans.com>2009-06-29 16:17:22 +0100
committerJohn Wiegley <johnw@newartisans.com>2009-06-29 16:17:22 +0100
commit752677edf05f3bac8b950ac449723a44740cbfa0 (patch)
tree84d2884302beaaf833461e6c3c0fade1f123aa9e /contrib/vim/ftplugin/ledger.vim
parent5ac73e1a1f97d92f5ab873f40255ba126a2361e8 (diff)
parent2aa9f5115cde5db8b98af45eb0fedfa4955ce6a0 (diff)
downloadfork-ledger-752677edf05f3bac8b950ac449723a44740cbfa0.tar.gz
fork-ledger-752677edf05f3bac8b950ac449723a44740cbfa0.tar.bz2
fork-ledger-752677edf05f3bac8b950ac449723a44740cbfa0.zip
Merge commit 'kljohann/master' into next
Diffstat (limited to 'contrib/vim/ftplugin/ledger.vim')
-rw-r--r--contrib/vim/ftplugin/ledger.vim196
1 files changed, 190 insertions, 6 deletions
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 "}}}