2026-01-25 09:38:17 +01:00

382 lines
11 KiB
VimL

" SPDX-FileCopyrightText: © 2019 Caleb Maclennan <caleb@alerque.com>
" SPDX-FileCopyrightText: © 2009 Johann Klähn <kljohann@gmail.com>
" SPDX-FileCopyrightText: © 2009 Stefan Karrmann
" SPDX-FileCopyrightText: © 2005 Wolfgang Oertl
" SPDX-License-Identifier: GPL-2.0-or-later
scriptencoding utf-8
call ledger#init()
if exists('b:did_ftplugin')
finish
endif
let b:did_ftplugin = 1
let b:undo_ftplugin = 'setlocal '.
\ 'foldtext< '.
\ 'include< comments< commentstring< omnifunc< formatexpr< formatprg<'
setlocal foldtext=LedgerFoldText()
setlocal include=^!\\?include
setlocal comments=b:;
setlocal commentstring=;%s
setlocal omnifunc=LedgerComplete
setlocal formatexpr=ledger#align_formatexpr(v:lnum,v:count)
" Automatic formatting is disabled by default because it can cause data loss when run
" on non-transaction blocks, see https://github.com/ledger/vim-ledger/issues/168.
if b:ledger_dangerous_formatprg
execute 'setlocal formatprg='.substitute(b:ledger_bin, ' ', '\\ ', 'g').'\ -f\ -\ print'
endif
if !exists('current_compiler')
compiler ledger
endif
" Highlight groups for Ledger reports
highlight default link LedgerNumber Number
highlight default link LedgerNegativeNumber Special
highlight default link LedgerCleared Constant
highlight default link LedgerPending Todo
highlight default link LedgerTarget Statement
highlight default link LedgerImproperPerc Special
let s:currency_symbol = '[[:alpha:]¢$€£]\+'
let s:value_regex = '\('.
\ '\%([0-9]\+\)'.
\ '\%([,.][0-9]\+\)*'.
\ '\|'.
\ '[,.][0-9]\+'.
\ '\)'
let s:optional_sign = '[+-]\?'
let s:currency_sign = '\('.
\ s:optional_sign.
\ '\s*'.
\ s:currency_symbol.
\ '\|'.
\ s:currency_symbol.
\ '\s*'.
\ s:optional_sign.
\ '\)'
let s:optional_balance_assertion = '\(\s*=\s*'.s:currency_sign.'\s*'.s:value_regex.'\)\?'
let s:rx_amount = s:value_regex.
\ s:optional_balance_assertion.
\ '\s*\%('.s:currency_symbol.'\s*\)\?'.
\ '\%(\s*;.*\)\?$'
function! LedgerFoldText()
" find amount
let amount = ''
let line_number = v:foldstart + 1
while line_number <= v:foldend
let line = getline(line_number)
" 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 line_number += 1
endwhile
" 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 b:ledger_maxwidth
let columns = min([columns, b:ledger_maxwidth])
endif
let amount = printf(' %s ', amount)
" left cut-off if window is too narrow to display the amount
while columns < strdisplaywidth(amount)
let amount = substitute(amount, '^.', '', '')
endwhile
let columns -= strdisplaywidth(amount)
if columns <= 0
return amount
endif
" right cut-off if there is not sufficient space to display the description
while columns < strdisplaywidth(foldtext)
let foldtext = substitute(foldtext, '.$', '', '')
endwhile
let columns -= strdisplaywidth(foldtext)
if columns <= 0
return foldtext . amount
endif
" fill in the fillstring
if strlen(b:ledger_fillstring)
let fillstring = b:ledger_fillstring
else
let fillstring = ' '
endif
let fillstrlen = strdisplaywidth(fillstring)
let foldtext .= ' '
let columns -= 1
while columns >= fillstrlen
let foldtext .= fillstring
let columns -= fillstrlen
endwhile
while columns < strdisplaywidth(fillstring)
let fillstring = substitute(fillstring, '.$', '', '')
endwhile
let foldtext .= fillstring
return foldtext . amount
endfunction
function! LedgerComplete(findstart, base)
if a:findstart
let line_number = line('.')
let line = getline('.')
let b:completion_context = ''
if line =~# '^\s\+[^[:blank:];]'
" 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:completion_context = 'account'
return matchend(line, '^\s\+[*!]\?\s*[\[(]\?')
endif
elseif line =~# '^account '
let prefix = matchend(line, '^account ')
let b:completion_context = 'account'
return prefix
elseif line =~# '^\d'
let prefix = matchend(line, '^\d\S\+\%\(\s\(([^\)]*)\|[*?!]\)\)\?\s\+')
if prefix <= col('.') - 1
let b:completion_context = 'description'
if prefix == -1
return -3
endif
return prefix
endif
elseif b:ledger_is_hledger && line =~# '^payee '
let prefix = matchend(line, '^payee ')
let b:completion_context = 'description'
return prefix
elseif line =~# '^$'
let b:completion_context = 'new'
return 0
endif
return -3
else
if ! exists('b:completion_cache')
let b:completion_cache = s:collect_completion_data()
let b:completion_cache['#'] = changenr()
endif
let update_cache = 0
let results = []
if b:completion_context ==# 'account'
let hierarchy = split(a:base, ':')
if a:base =~# ':$'
call add(hierarchy, '')
endif
let results = ledger#find_in_tree(b:completion_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 b:ledger_exact_only
let results = exacts
endif
call map(results, 'v:val[0]')
if b:ledger_fuzzy_account_completion
let results = matchfuzzy(b:completion_cache.flat_accounts, a:base, {'matchseq':1})
elseif b:ledger_detailed_first
let results = reverse(sort(results, 's:sort_accounts_by_depth'))
else
let results = sort(results)
endif
elseif b:completion_context ==# 'description'
let results = ledger#filter_items(b:completion_cache.descriptions, a:base)
if len(results) < 1
let update_cache = 1
endif
elseif b:completion_context ==# 'new'
return [strftime(b:ledger_date_format)]
endif
if b: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:completion_cache['#'] != changenr()
unlet b:completion_cache
return LedgerComplete(a:findstart, a:base)
else
unlet! b:completion_context
return results
endif
endif
endfunction
function! s:collect_completion_data()
let transactions = ledger#transactions()
let cache = {'descriptions': [], 'tags': {}, 'accounts': {}, 'flat_accounts': []}
let accounts = s:get_accounts_list()
let cache.flat_accounts = accounts
let cache.descriptions = s:get_descriptions_list()
for transaction in transactions
let [tags, postings] = transaction.parse_body()
let tagdicts = [tags]
" collect account names (only when not using ledger binary)
if b:ledger_bin ==# v:false
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')
let name = substitute(name, '^(.*)$', '\1', '')
let name = substitute(name, '^\[.*\]$', '\1', '')
if index(accounts, name) < 0
call add(accounts, name)
endif
endfor
endif
" 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
endfunction
function! s:get_accounts_list()
if b:ledger_bin !=# v:false
return split(system(b:ledger_accounts_cmd), '\n')
else
return ledger#declared_accounts()
endif
endfunction
function! s:get_descriptions_list()
if b:ledger_bin !=# v:false
return split(system(b:ledger_descriptions_cmd), '\n')
else
let transactions = ledger#transactions()
let descriptions = []
for xact in transactions
if has_key(xact, 'description') && index(descriptions, xact['description']) < 0
call add(descriptions, xact['description'])
endif
endfor
return descriptions
endif
endfunction
" Helper functions
" get # of visible/usable columns in current window
function! s:get_columns()
" 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
endfunction
function! s:sort_accounts_by_depth(name1, name2)
let depth1 = s:count_expression(a:name1, ':')
let depth2 = s:count_expression(a:name2, ':')
return depth1 == depth2 ? 0 : depth1 > depth2 ? 1 : -1
endfunction
function! s:count_expression(text, expression)
return len(split(a:text, a:expression, 1))-1
endfunction
function! s:autocomplete_account_or_payee(argument_lead, command_line, cursor_position)
if a:argument_lead =~# '^@'
let payees = s:get_descriptions_list()
let pattern = strpart(a:argument_lead, 1)
return map(filter(payees, "v:val =~? '" . pattern . "' && v:val !~? '^Warning: '"),
\ '"@" . escape(v:val, " ")')
else
let accounts = s:get_accounts_list()
return map(filter(accounts, "v:val =~? '" . a:argument_lead . "' && v:val !~? '^Warning: '"),
\ 'escape(v:val, " ")')
endif
endfunction
function! s:reconcile(file, account)
let l:amount = input('Target amount' . (empty(b:ledger_default_commodity) ? ': ' : ' (' . b:ledger_default_commodity . '): '))
call ledger#reconcile(a:file, a:account, str2float(l:amount))
endfunction
" Commands
command! -buffer -nargs=? -complete=customlist,<SID>autocomplete_account_or_payee
\ Balance call ledger#show_balance(b:ledger_main, <q-args>)
command! -buffer -nargs=+ -complete=customlist,<SID>autocomplete_account_or_payee
\ Ledger call ledger#output(ledger#report(b:ledger_main, <q-args>))
command! -buffer -range LedgerAlign <line1>,<line2>call ledger#align_commodity()
command! -buffer LedgerAlignBuffer call ledger#align_commodity_buffer()
command! -buffer -nargs=1 -complete=customlist,<SID>autocomplete_account_or_payee
\ Reconcile call <SID>reconcile(b:ledger_main, <q-args>)
command! -buffer -complete=customlist,<SID>autocomplete_account_or_payee -nargs=*
\ Register call ledger#register(b:ledger_main, <q-args>)