450 lines
13 KiB
VimL
450 lines
13 KiB
VimL
" Vim filetype plugin file
|
|
" filetype: ledger
|
|
" by Johann Klähn; Use according to the terms of the GPL>=2.
|
|
" vim:ts=2:sw=2:sts=2:foldmethod=marker
|
|
|
|
if exists("b:did_ftplugin")
|
|
finish
|
|
endif
|
|
|
|
let b:did_ftplugin = 1
|
|
|
|
let b:undo_ftplugin = "setlocal ".
|
|
\ "foldtext< ".
|
|
\ "include< comments< commentstring< omnifunc< formatprg<"
|
|
|
|
if !exists('current_compiler')
|
|
compiler ledger
|
|
endif
|
|
|
|
setl foldtext=LedgerFoldText()
|
|
setl include=^!\\?include
|
|
setl comments=b:;
|
|
setl commentstring=;%s
|
|
setl omnifunc=LedgerComplete
|
|
|
|
" set location of ledger binary for checking and auto-formatting
|
|
if ! exists("g:ledger_bin") || empty(g:ledger_bin) || ! executable(g:ledger_bin)
|
|
if executable('ledger')
|
|
let g:ledger_bin = 'ledger'
|
|
else
|
|
unlet! g:ledger_bin
|
|
echohl WarningMsg
|
|
echomsg "ledger command not found. Set g:ledger_bin or extend $PATH ".
|
|
\ "to enable error checking and auto-formatting."
|
|
echohl None
|
|
endif
|
|
endif
|
|
|
|
if exists("g:ledger_bin")
|
|
exe 'setl formatprg='.substitute(g:ledger_bin, ' ', '\\ ', 'g').'\ -f\ -\ print'
|
|
endif
|
|
|
|
if !exists('g:ledger_extra_options')
|
|
let g:ledger_extra_options = ''
|
|
endif
|
|
|
|
if !exists('g:ledger_date_format')
|
|
let g:ledger_date_format = '%Y/%m/%d'
|
|
endif
|
|
|
|
" You can set a maximal number of columns the fold text (excluding amount)
|
|
" will use by overriding g:ledger_maxwidth in your .vimrc.
|
|
" When maxwidth is zero, the amount will be displayed at the far right side
|
|
" of the screen.
|
|
if !exists('g:ledger_maxwidth')
|
|
let g:ledger_maxwidth = 0
|
|
endif
|
|
|
|
if !exists('g:ledger_fillstring')
|
|
let g:ledger_fillstring = ' '
|
|
endif
|
|
|
|
if !exists('g:ledger_decimal_sep')
|
|
let g:ledger_decimal_sep = '.'
|
|
endif
|
|
|
|
if !exists('g:ledger_align_at')
|
|
let g:ledger_align_at = 60
|
|
endif
|
|
|
|
if !exists('g:ledger_default_commodity')
|
|
let g:ledger_default_commodity = ''
|
|
endif
|
|
|
|
if !exists('g:ledger_commodity_before')
|
|
let g:ledger_commodity_before = 1
|
|
endif
|
|
|
|
if !exists('g:ledger_commodity_sep')
|
|
let g:ledger_commodity_sep = ''
|
|
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 = 1
|
|
endif
|
|
|
|
" only display exact matches (no parent accounts etc.)
|
|
if !exists('g:ledger_exact_only')
|
|
let g:ledger_exact_only = 0
|
|
endif
|
|
|
|
" display original text / account name as completion
|
|
if !exists('g:ledger_include_original')
|
|
let g:ledger_include_original = 0
|
|
endif
|
|
|
|
" Settings for Ledger reports {{{
|
|
if !exists('g:ledger_main')
|
|
let g:ledger_main = '%'
|
|
endif
|
|
|
|
if !exists('g:ledger_winpos')
|
|
let g:ledger_winpos = 'B' " Window position (see s:winpos_map)
|
|
endif
|
|
|
|
if !exists('g:ledger_use_location_list')
|
|
let g:ledger_use_location_list = 0 " Use quickfix list by default
|
|
endif
|
|
|
|
if !exists('g:ledger_cleared_string')
|
|
let g:ledger_cleared_string = 'Cleared: '
|
|
endif
|
|
|
|
if !exists('g:ledger_pending_string')
|
|
let g:ledger_pending_string = 'Cleared or pending: '
|
|
endif
|
|
|
|
if !exists('g:ledger_target_string')
|
|
let g:ledger_target_string = 'Difference from target: '
|
|
endif
|
|
" }}}
|
|
|
|
" Settings for the quickfix window {{{
|
|
if !exists('g:ledger_qf_register_format')
|
|
let g:ledger_qf_register_format = '%(date) %-50(payee) %-30(account) %15(amount) %15(total)\n'
|
|
endif
|
|
|
|
if !exists('g:ledger_qf_reconcile_format')
|
|
let g:ledger_qf_reconcile_format = '%(date) %-4(code) %-50(payee) %-30(account) %15(amount)\n'
|
|
endif
|
|
|
|
if !exists('g:ledger_qf_size')
|
|
let g:ledger_qf_size = 10 " Size of the quickfix window
|
|
endif
|
|
|
|
if !exists('g:ledger_qf_vertical')
|
|
let g:ledger_qf_vertical = 0
|
|
endif
|
|
|
|
if !exists('g:ledger_qf_hide_file')
|
|
let g:ledger_qf_hide_file = 1
|
|
endif
|
|
" }}}
|
|
|
|
" Highlight groups for Ledger reports {{{
|
|
hi link LedgerNumber Number
|
|
hi link LedgerNegativeNumber Special
|
|
hi link LedgerCleared Constant
|
|
hi link LedgerPending Todo
|
|
hi link LedgerTarget Statement
|
|
hi link LedgerImproperPerc Special
|
|
" }}}
|
|
|
|
let s:rx_amount = '\('.
|
|
\ '\%([0-9]\+\)'.
|
|
\ '\%([,.][0-9]\+\)*'.
|
|
\ '\|'.
|
|
\ '[,.][0-9]\+'.
|
|
\ '\)'.
|
|
\ '\s*\%([[:alpha:]¢$€£]\+\s*\)\?'.
|
|
\ '\%(\s*;.*\)\?$'
|
|
|
|
function! LedgerFoldText() "{{{1
|
|
" find amount
|
|
let amount = ""
|
|
let lnum = v:foldstart + 1
|
|
while lnum <= v:foldend
|
|
let line = getline(lnum)
|
|
|
|
" Skip metadata/leading comment
|
|
if line !~ '^\%(\s\+;\|\d\)'
|
|
" No comment, look for amount...
|
|
let groups = matchlist(line, s:rx_amount)
|
|
if ! empty(groups)
|
|
let amount = groups[1]
|
|
break
|
|
endif
|
|
endif
|
|
let lnum += 1
|
|
endwhile
|
|
|
|
let fmt = '%s %s '
|
|
" strip whitespace at beginning and end of line
|
|
let foldtext = substitute(getline(v:foldstart),
|
|
\ '\(^\s\+\|\s\+$\)', '', 'g')
|
|
|
|
" number of columns foldtext can use
|
|
let columns = s:get_columns()
|
|
if g:ledger_maxwidth
|
|
let columns = min([columns, g:ledger_maxwidth])
|
|
endif
|
|
let columns -= s:multibyte_strlen(printf(fmt, '', amount))
|
|
|
|
" add spaces so the text is always long enough when we strip it
|
|
" to a certain width (fake table)
|
|
if strlen(g:ledger_fillstring)
|
|
" add extra spaces so fillstring aligns
|
|
let filen = s:multibyte_strlen(g:ledger_fillstring)
|
|
let folen = s:multibyte_strlen(foldtext)
|
|
let foldtext .= repeat(' ', filen - (folen%filen))
|
|
|
|
let foldtext .= repeat(g:ledger_fillstring,
|
|
\ s:get_columns()/filen)
|
|
else
|
|
let foldtext .= repeat(' ', s:get_columns())
|
|
endif
|
|
|
|
" we don't use slices[:5], because that messes up multibyte characters
|
|
let foldtext = substitute(foldtext, '.\{'.columns.'}\zs.*$', '', '')
|
|
|
|
return printf(fmt, foldtext, amount)
|
|
endfunction "}}}
|
|
|
|
function! LedgerComplete(findstart, base) "{{{1
|
|
if a:findstart
|
|
let lnum = line('.')
|
|
let line = getline('.')
|
|
let b:compl_context = ''
|
|
if line =~ '^\s\+[^[:blank:];]' "{{{2 (account)
|
|
" only allow completion when in or at end of account name
|
|
if matchend(line, '^\s\+\%(\S \S\|\S\)\+') >= col('.') - 1
|
|
" the start of the first non-blank character
|
|
" (excluding virtual-transaction and 'cleared' marks)
|
|
" is the beginning of the account name
|
|
let b:compl_context = 'account'
|
|
return matchend(line, '^\s\+[*!]\?\s*[\[(]\?')
|
|
endif
|
|
elseif line =~ '^\d' "{{{2 (description)
|
|
let pre = matchend(line, '^\d\S\+\%(([^)]*)\|[*?!]\|\s\)\+')
|
|
if pre < col('.') - 1
|
|
let b:compl_context = 'description'
|
|
return pre
|
|
endif
|
|
elseif line =~ '^$' "{{{2 (new line)
|
|
let b:compl_context = 'new'
|
|
endif "}}}
|
|
return -1
|
|
else
|
|
if ! exists('b:compl_cache')
|
|
let b:compl_cache = s:collect_completion_data()
|
|
let b:compl_cache['#'] = changenr()
|
|
endif
|
|
let update_cache = 0
|
|
|
|
let results = []
|
|
if b:compl_context == 'account' "{{{2 (account)
|
|
let hierarchy = split(a:base, ':')
|
|
if a:base =~ ':$'
|
|
call add(hierarchy, '')
|
|
endif
|
|
|
|
let results = ledger#find_in_tree(b:compl_cache.accounts, hierarchy)
|
|
let exacts = filter(copy(results), 'v:val[1]')
|
|
|
|
if len(exacts) < 1
|
|
" update cache if we have no exact matches
|
|
let update_cache = 1
|
|
endif
|
|
|
|
if g:ledger_exact_only
|
|
let results = exacts
|
|
endif
|
|
|
|
call map(results, 'v:val[0]')
|
|
|
|
if g:ledger_detailed_first
|
|
let results = reverse(sort(results, 's:sort_accounts_by_depth'))
|
|
else
|
|
let results = sort(results)
|
|
endif
|
|
elseif b:compl_context == 'description' "{{{2 (description)
|
|
let results = ledger#filter_items(b:compl_cache.descriptions, a:base)
|
|
|
|
if len(results) < 1
|
|
let update_cache = 1
|
|
endif
|
|
elseif b:compl_context == 'new' "{{{2 (new line)
|
|
return [strftime(g:ledger_date_format)]
|
|
endif "}}}
|
|
|
|
|
|
if g:ledger_include_original
|
|
call insert(results, a:base)
|
|
endif
|
|
|
|
" no completion (apart from a:base) found. update cache if file has changed
|
|
if update_cache && b:compl_cache['#'] != changenr()
|
|
unlet b:compl_cache
|
|
return LedgerComplete(a:findstart, a:base)
|
|
else
|
|
unlet! b:compl_context
|
|
return results
|
|
endif
|
|
endif
|
|
endf "}}}
|
|
|
|
" Deprecated functions {{{1
|
|
let s:deprecated = {
|
|
\ 'LedgerToggleTransactionState': 'ledger#transaction_state_toggle',
|
|
\ 'LedgerSetTransactionState': 'ledger#transaction_state_set',
|
|
\ 'LedgerSetDate': 'ledger#transaction_date_set'
|
|
\ }
|
|
|
|
for [s:old, s:new] in items(s:deprecated)
|
|
let s:fun = "function! {s:old}(...)\nechohl WarningMsg\necho '" . s:old .
|
|
\ " is deprecated. Use ".s:new." instead!'\nechohl None\n" .
|
|
\ "call call('" . s:new . "', a:000)\nendf"
|
|
exe s:fun
|
|
endfor
|
|
unlet s:old s:new s:fun
|
|
" }}}1
|
|
|
|
function! s:collect_completion_data() "{{{1
|
|
let transactions = ledger#transactions()
|
|
let cache = {'descriptions': [], 'tags': {}, 'accounts': {}}
|
|
let accounts = ledger#declared_accounts()
|
|
for xact in transactions
|
|
" collect descriptions
|
|
if has_key(xact, 'description') && index(cache.descriptions, xact['description']) < 0
|
|
call add(cache.descriptions, xact['description'])
|
|
endif
|
|
let [t, postings] = xact.parse_body()
|
|
let tagdicts = [t]
|
|
|
|
" collect account names
|
|
for posting in postings
|
|
if has_key(posting, 'tags')
|
|
call add(tagdicts, posting.tags)
|
|
endif
|
|
" remove virtual-transaction-marks
|
|
let name = substitute(posting.account, '\%(^\s*[\[(]\?\|[\])]\?\s*$\)', '', 'g')
|
|
if index(accounts, name) < 0
|
|
call add(accounts, name)
|
|
endif
|
|
endfor
|
|
|
|
" collect tags
|
|
for tags in tagdicts | for [tag, val] in items(tags)
|
|
let values = get(cache.tags, tag, [])
|
|
if index(values, val) < 0
|
|
call add(values, val)
|
|
endif
|
|
let cache.tags[tag] = values
|
|
endfor | endfor
|
|
endfor
|
|
|
|
for account in accounts
|
|
let last = cache.accounts
|
|
for part in split(account, ':')
|
|
let last[part] = get(last, part, {})
|
|
let last = last[part]
|
|
endfor
|
|
endfor
|
|
|
|
return cache
|
|
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() " {{{2
|
|
" As long as vim doesn't provide a command natively,
|
|
" we have to compute the available columns.
|
|
" see :help todo.txt -> /Add argument to winwidth()/
|
|
|
|
let columns = (winwidth(0) == 0 ? 80 : winwidth(0)) - &foldcolumn
|
|
if &number
|
|
" line('w$') is the line number of the last line
|
|
let columns -= max([len(line('w$'))+1, &numberwidth])
|
|
endif
|
|
|
|
" are there any signs/is the sign column displayed?
|
|
redir => signs
|
|
silent execute 'sign place buffer='.string(bufnr("%"))
|
|
redir END
|
|
if signs =~# 'id='
|
|
let columns -= 2
|
|
endif
|
|
|
|
return columns
|
|
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 "}}}
|
|
|
|
function! s:autocomplete_account_or_payee(argLead, cmdLine, cursorPos) "{{{2
|
|
return (a:argLead =~ '^@') ?
|
|
\ map(filter(systemlist(g:ledger_bin . ' -f ' . shellescape(expand(g:ledger_main)) . ' payees'),
|
|
\ "v:val =~? '" . strpart(a:argLead, 1) . "' && v:val !~? '^Warning: '"), '"@" . escape(v:val, " ")')
|
|
\ :
|
|
\ map(filter(systemlist(g:ledger_bin . ' -f ' . shellescape(expand(g:ledger_main)) . ' accounts'),
|
|
\ "v:val =~? '" . a:argLead . "' && v:val !~? '^Warning: '"), 'escape(v:val, " ")')
|
|
endf "}}}
|
|
|
|
function! s:reconcile(file, account) "{{{2
|
|
" call inputsave()
|
|
let l:amount = input('Target amount' . (empty(g:ledger_default_commodity) ? ': ' : ' (' . g:ledger_default_commodity . '): '))
|
|
" call inputrestore()
|
|
call ledger#reconcile(a:file, a:account, str2float(l:amount))
|
|
endf "}}}
|
|
|
|
" Commands {{{1
|
|
command! -buffer -nargs=? -complete=customlist,s:autocomplete_account_or_payee
|
|
\ Balance call ledger#show_balance(g:ledger_main, <q-args>)
|
|
|
|
command! -buffer -nargs=+ -complete=customlist,s:autocomplete_account_or_payee
|
|
\ Ledger call ledger#output(ledger#report(g:ledger_main, <q-args>))
|
|
|
|
command! -buffer -range LedgerAlign <line1>,<line2>call ledger#align_commodity()
|
|
|
|
command! -buffer -nargs=1 -complete=customlist,s:autocomplete_account_or_payee
|
|
\ Reconcile call <sid>reconcile(g:ledger_main, <q-args>)
|
|
|
|
command! -buffer -complete=customlist,s:autocomplete_account_or_payee -nargs=*
|
|
\ Register call ledger#register(g:ledger_main, <q-args>)
|
|
" }}}
|
|
|