aboutsummaryrefslogtreecommitdiffstats
path: root/autoload/beancount.vim
blob: 93373d1d81ef537ccb0ed245f403b085d8e83be4 (plain) (blame)
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
" Align currency on decimal point.
function! beancount#align_commodity(line1, line2)
    " Saving cursor position to adjust it if necessary.
    let cursor_col = col('.')
    let cursor_line = line('.')
    " This matches the line up to the first dot (or other separator),
    " excluding comments.
    " Note very nomagic so that the separator is not interpreted as regex.
    let separator_regex = '^\V\[^;]\{-}' . g:beancount_decimal_separator
    " This lets me increment at start of loop, because of continue statements.
    let i = a:line1 - 1
    while i < a:line2
        let i += 1
        let s = getline(i)
        " This matches an account name followed by a space. There may be
        " some conflicts with non-transaction syntax that I don't know about.
        " It won't match a comment or any non-indented line.
        let end_acc = matchend(s, '^\v([-\d]+\s+(balance|price))? +\S+ ')
        if end_acc < 0 | continue | endif
        " Where does commodity amount begin?
        let end_space = matchend(s, '^ *', end_acc)
        " Find the first decimal point, not counting comments.
        let separator = matchend(s, separator_regex, end_space)
        if separator < 0
            " If there is no separator, pretend there's one after the last digit.
            let separator = matchend(s, '^\v[^;]*\d+') + 1
        endif
        if separator < 0 | continue | endif
        let has_spaces = end_space - end_acc
        let need_spaces = g:beancount_separator_col - separator + has_spaces
        if need_spaces < 0 | continue | endif
        call setline(i, s[0 : end_acc - 1] . repeat(" ", need_spaces) . s[ end_space : -1])
        if i == cursor_line && cursor_col >= end_acc
            " Adjust cursor position for continuity.
            call cursor(0, cursor_col + need_spaces - has_spaces)
        endif
    endwhile
endfunction

function! s:count_expression(text, expression)
  return len(split(a:text, a:expression, 1)) - 1
endfunction

function! s:sort_accounts_by_depth(name1, name2)
  let l:depth1 = s:count_expression(a:name1, ':')
  let l:depth2 = s:count_expression(a:name2, ':')
  return depth1 == depth2 ? 0 : depth1 > depth2 ? 1 : -1
endfunction

" Complete account name.
function! beancount#complete_account(findstart, base)
    if a:findstart
        let l:col = searchpos('\s', "bn", line("."))[1]
        if col == 0
            return -1
        else
            return col
        endif
    endif

    if !exists('b:beancount_accounts')
        if exists('b:beancount_root')
            let l:root = b:beancount_root
        else
            let l:root = expand('%')
        endif
        let b:beancount_accounts = beancount#find_accounts(l:root)
    endif

    if g:beancount_account_completion ==? 'chunks'
        let l:pattern = '^\V' . substitute(a:base, ":", '\\[^:]\\*:', "g") . '\[^:]\*'
    else
        let l:pattern = '^\V\.\*' . substitute(a:base, ":", '\\.\\*:\\.\\*', "g") . '\.\*'
    endif

    let l:matches = []
    let l:index = -1
    while 1
        let l:index = match(b:beancount_accounts, l:pattern, l:index + 1)
        if l:index == -1 | break | endif
        call add(l:matches, matchstr(b:beancount_accounts[l:index], l:pattern))
    endwhile

    if g:beancount_detailed_first
        let l:matches = reverse(sort(l:matches, 's:sort_accounts_by_depth'))
    endif

    return l:matches
endfunction

" Get list of acounts.
function! beancount#find_accounts(root_file)
    python << EOM
import collections
import os
import re
import sys
import vim

RE_INCLUDE = re.compile(r'^include\s+"([^\n"]+)"')
RE_ACCOUNT = re.compile(r'^\d{4,}-\d{2}-\d{2}\s+open\s+(\S+)')

def combine_paths(old, new):
    return os.path.normpath(
        new if os.path.isabs(new) else os.path.join(old, new))

def parse_file(fh, files, accounts):
    for line in fh:
        m = RE_INCLUDE.match(line)
        if m: files.append(combine_paths(os.path.dirname(fh.name), m.group(1)))
        m = RE_ACCOUNT.match(line)
        if m: accounts.add(m.group(1))

files = collections.deque([vim.eval("a:root_file")])
accounts = set()
seen = set()
while files:
    current = files.popleft()
    if current in seen:
        continue
    seen.add(current)
    try:
        with open(current, 'r') as fh:
            parse_file(fh, files, accounts)
    except IOError as err:
        pass

vim.command('return [{}]'.format(','.join(repr(x) for x in sorted(accounts))))
EOM
endfunction