File: ledger.vim

package info (click to toggle)
vim-ledger 1.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 272 kB
  • sloc: makefile: 2
file content (381 lines) | stat: -rw-r--r-- 11,372 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
" 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:cursym = '[[:alpha:]¢$€£]\+'
let s:valreg = '\('.
             \   '\%([0-9]\+\)'.
             \   '\%([,.][0-9]\+\)*'.
             \ '\|'.
             \   '[,.][0-9]\+'.
             \ '\)'
let s:optsgn = '[+-]\?'
let s:cursgn = '\('.
             \   s:optsgn.
             \   '\s*'.
             \   s:cursym.
             \ '\|'.
             \   s:cursym.
             \   '\s*'.
             \   s:optsgn.
             \ '\)'

let s:optional_balance_assertion = '\(\s*=\s*'.s:cursgn.'\s*'.s:valreg.'\)\?'

let s:rx_amount = s:valreg.
                \ s:optional_balance_assertion.
                \ '\s*\%('.s:cursym.'\s*\)\?'.
                \ '\%(\s*;.*\)\?$'

function! LedgerFoldText()
  " 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

  " 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 lnum = line('.')
    let line = getline('.')
    let b:compl_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:compl_context = 'account'
        return matchend(line, '^\s\+[*!]\?\s*[\[(]\?')
      endif
    elseif line =~# '^account '
        let pre = matchend(line, '^account ')
        let b:compl_context = 'account'
        return pre
    elseif line =~# '^\d'
      let pre = matchend(line, '^\d\S\+\%\(\s\(([^\)]*)\|[*?!]\)\)\?\s\+')
      if pre <= col('.') - 1
        let b:compl_context = 'description'
        if pre == -1
          return -3
        endif
        return pre
      endif
    elseif b:ledger_is_hledger && line =~# '^payee '
      let pre = matchend(line, '^payee ')
      let b:compl_context = 'description'
      return pre
    elseif line =~# '^$'
      let b:compl_context = 'new'
      return 0
    endif
    return -3
  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'
      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 b:ledger_exact_only
        let results = exacts
      endif

      call map(results, 'v:val[0]')

      if b:ledger_fuzzy_account_completion
        let results = matchfuzzy(b:compl_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:compl_context ==# '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'
      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:compl_cache['#'] != changenr()
      unlet b:compl_cache
      return LedgerComplete(a:findstart, a:base)
    else
      unlet! b:compl_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 xact in transactions
    let [t, postings] = xact.parse_body()
    let tagdicts = [t]

    " 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(argLead, cmdLine, cursorPos)
  if a:argLead =~# '^@'
    let payees = s:get_descriptions_list()
    let pattern = strpart(a:argLead, 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:argLead . "' && 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,s:autocomplete_account_or_payee
      \ Balance call ledger#show_balance(b:ledger_main, <q-args>)

command! -buffer -nargs=+ -complete=customlist,s: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,s:autocomplete_account_or_payee
      \ Reconcile call <sid>reconcile(b:ledger_main, <q-args>)

command! -buffer -complete=customlist,s:autocomplete_account_or_payee -nargs=*
      \ Register call ledger#register(b:ledger_main, <q-args>)