diff options
-rw-r--r-- | .travis.yml | 15 | ||||
-rw-r--r-- | autoload/beancount.vim | 302 | ||||
-rw-r--r-- | compiler/beancount.vim | 12 | ||||
-rw-r--r-- | doc/beancount.txt | 71 | ||||
-rw-r--r-- | ftplugin/beancount.vim | 23 | ||||
-rw-r--r-- | indent/beancount.vim | 32 | ||||
-rw-r--r-- | rplugin/python3/deoplete/sources/beancount.py | 114 | ||||
-rw-r--r-- | syntax/beancount.vim | 32 | ||||
-rw-r--r-- | syntax_checkers/beancount/bean_check.vim | 13 | ||||
-rw-r--r-- | test/align.vader | 39 |
10 files changed, 507 insertions, 146 deletions
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..099d4d5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +matrix: + include: + - env: TEST_ENV=lint + language: python + before_script: pip install vim-vint + script: vint -s . + - env: TEST_ENV=test + language: viml + before_script: git clone https://github.com/junegunn/vader.vim.git + script: | + vim -Nu <(cat << VIMRC + set rtp+=vader.vim + set rtp+=. + filetype plugin indent on + VIMRC) -c 'Vader! test/*' > /dev/null diff --git a/autoload/beancount.vim b/autoload/beancount.vim index ba2a5cc..81067a3 100644 --- a/autoload/beancount.vim +++ b/autoload/beancount.vim @@ -1,76 +1,222 @@ +let s:using_python3 = has('python3') + +" Equivalent to python's startswith +" Matches based on user's ignorecase preference +function! s:startswith(string, prefix) abort + return strpart(a:string, 0, strlen(a:prefix)) == a:prefix +endfunction + " 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 +function! beancount#align_commodity(line1, line2) abort + " Save cursor position to adjust it if necessary. + let l:cursor_col = col('.') + let l:cursor_line = line('.') + + " Increment at start of loop, because of continue statements. + let l:current_line = a:line1 - 1 + while l:current_line < a:line2 + let l:current_line += 1 + let l:line = getline(l:current_line) + " This matches an account name followed by a space in one of the two + " following cases: + " - A posting line, i.e., the line starts with indentation followed + " by an optional flag and the account. + " - A balance directive, i.e., the line starts with a date followed + " by the 'balance' keyword and the account. + " - A price directive, i.e., the line starts with a date followed by + " the 'price' keyword and a currency. + let l:end_account = matchend(l:line, '^\v' . + \ '[\-/[:digit:]]+\s+balance\s+([A-Z][A-Za-z0-9\-]+)(:[A-Z][A-Za-z0-9\-]*)+ ' . + \ '|[\-/[:digit:]]+\s+price\s+\S+ ' . + \ '|\s+([!&#?%PSTCURM]\s+)?([A-Z][A-Za-z0-9\-]+)(:[A-Z][A-Za-z0-9\-]*)+ ' + \ ) + if l:end_account < 0 + continue 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 + + " Where does the number begin? + let l:begin_number = matchend(l:line, '^ *', l:end_account) + + " Look for a minus sign and a number (possibly containing commas) and + " align on the next column. + let l:separator = matchend(l:line, '^\v(-)?[,[:digit:]]+', l:begin_number) + 1 + if l:separator < 0 | continue | endif + let l:has_spaces = l:begin_number - l:end_account + let l:need_spaces = g:beancount_separator_col - l:separator + l:has_spaces + if l:need_spaces < 0 | continue | endif + call setline(l:current_line, l:line[0 : l:end_account - 1] . repeat(' ', l:need_spaces) . l:line[ l:begin_number : -1]) + if l:current_line == l:cursor_line && l:cursor_col >= l:end_account " Adjust cursor position for continuity. - call cursor(0, cursor_col + need_spaces - has_spaces) + call cursor(0, l:cursor_col + l:need_spaces - l:has_spaces) endif endwhile endfunction -function! s:count_expression(text, expression) - return len(split(a:text, a:expression, 1)) - 1 +function! s:count_expression(text, expression) abort + 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 +function! s:sort_accounts_by_depth(name1, name2) abort + let l:depth1 = s:count_expression(a:name1, ':') + let l:depth2 = s:count_expression(a:name2, ':') + return l:depth1 == l:depth2 ? 0 : l:depth1 > l:depth2 ? 1 : -1 endfunction -" Complete account name. -function! beancount#complete_account(findstart, base) +let s:directives = ['open', 'close', 'commodity', 'txn', 'balance', 'pad', 'note', 'document', 'price', 'event', 'query', 'custom'] + +" ------------------------------ +" Completion functions +" ------------------------------ +function! beancount#complete(findstart, base) abort if a:findstart - let l:col = searchpos('\s', "bn", line("."))[1] - if col == 0 + let l:col = searchpos('\s', 'bn', line('.'))[1] + if l:col == 0 return -1 else - return col + return l: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) + let l:partial_line = strpart(getline('.'), 0, getpos('.')[2]-1) + " Match directive types + if l:partial_line =~# '^\d\d\d\d\(-\|/\)\d\d\1\d\d $' + return beancount#complete_basic(s:directives, a:base, '') + endif + + " If we are using python3, now is a good time to load everything + call beancount#load_everything() + + " Split out the first character (for cases where we don't want to match the + " leading character: ", #, etc) + let l:first = strpart(a:base, 0, 1) + let l:rest = strpart(a:base, 1) + + if l:partial_line =~# '^\d\d\d\d\(-\|/\)\d\d\1\d\d event $' && l:first ==# '"' + return beancount#complete_basic(b:beancount_events, l:rest, '"') + endif + + let l:two_tokens = searchpos('\S\+\s', 'bn', line('.'))[1] + let l:prev_token = strpart(getline('.'), l:two_tokens, getpos('.')[2] - l:two_tokens) + " Match curriences if previous token is number + if l:prev_token =~# '^\d\+\([\.,]\d\+\)*' + call beancount#load_currencies() + return beancount#complete_basic(b:beancount_currencies, a:base, '') + endif + + if l:first ==# '#' + call beancount#load_tags() + return beancount#complete_basic(b:beancount_tags, l:rest, '#') + elseif l:first ==# '^' + call beancount#load_links() + return beancount#complete_basic(b:beancount_links, l:rest, '^') + elseif l:first ==# '"' + call beancount#load_payees() + return beancount#complete_basic(b:beancount_payees, l:rest, '"') + else + call beancount#load_accounts() + return beancount#complete_account(a:base) + endif +endfunction + +function! beancount#get_root() abort + if exists('b:beancount_root') + return b:beancount_root + endif + return expand('%') +endfunction + +function! beancount#load_everything() abort + if s:using_python3 && !exists('b:beancount_loaded') + let l:root = beancount#get_root() +python3 << EOF +import vim +from beancount import loader +from beancount.core import data + +accounts = set() +currencies = set() +events = set() +links = set() +payees = set() +tags = set() + +entries, errors, options_map = loader.load_file(vim.eval('l:root')) +for index, entry in enumerate(entries): + if isinstance(entry, data.Open): + accounts.add(entry.account) + if entry.currencies: + currencies.update(entry.currencies) + elif isinstance(entry, data.Commodity): + currencies.add(entry.currency) + elif isinstance(entry, data.Event): + events.add(entry.type) + elif isinstance(entry, data.Transaction): + if entry.tags: + tags.update(entry.tags) + if entry.links: + links.update(entry.links) + if entry.payee: + payees.add(entry.payee) + +vim.command('let b:beancount_accounts = [{}]'.format(','.join(repr(x) for x in sorted(accounts)))) +vim.command('let b:beancount_currencies = [{}]'.format(','.join(repr(x) for x in sorted(currencies)))) +vim.command('let b:beancount_events = [{}]'.format(','.join(repr(x) for x in sorted(events)))) +vim.command('let b:beancount_links = [{}]'.format(','.join(repr(x) for x in sorted(links)))) +vim.command('let b:beancount_payees = [{}]'.format(','.join(repr(x) for x in sorted(payees)))) +vim.command('let b:beancount_tags = [{}]'.format(','.join(repr(x) for x in sorted(tags)))) +vim.command('let b:beancount_loaded = v:true'.format(','.join(repr(x) for x in sorted(tags)))) +EOF + endif +endfunction + +function! beancount#load_accounts() abort + if !s:using_python3 && !exists('b:beancount_accounts') + let l:root = beancount#get_root() + let b:beancount_accounts = beancount#query_single(l:root, 'select distinct account;') + endif +endfunction + +function! beancount#load_tags() abort + if !s:using_python3 && !exists('b:beancount_tags') + let l:root = beancount#get_root() + let b:beancount_tags = beancount#query_single(l:root, 'select distinct tags;') endif +endfunction +function! beancount#load_links() abort + if !s:using_python3 && !exists('b:beancount_links') + let l:root = beancount#get_root() + let b:beancount_links = beancount#query_single(l:root, 'select distinct links;') + endif +endfunction + +function! beancount#load_currencies() abort + if !s:using_python3 && !exists('b:beancount_currencies') + let l:root = beancount#get_root() + let b:beancount_currencies = beancount#query_single(l:root, 'select distinct currency;') + endif +endfunction + +function! beancount#load_payees() abort + if !s:using_python3 && !exists('b:beancount_payees') + let l:root = beancount#get_root() + let b:beancount_payees = beancount#query_single(l:root, 'select distinct payee;') + endif +endfunction + +" General completion function +function! beancount#complete_basic(input, base, prefix) abort + let l:matches = filter(copy(a:input), 's:startswith(v:val, a:base)') + + return map(l:matches, 'a:prefix . v:val') +endfunction + +" Complete account name. +function! beancount#complete_account(base) abort if g:beancount_account_completion ==? 'chunks' - let l:pattern = '^\V' . substitute(a:base, ":", '\\[^:]\\*:', "g") . '\[^:]\*' + let l:pattern = '^\V' . substitute(a:base, ':', '\\[^:]\\*:', 'g') . '\[^:]\*' else - let l:pattern = '^\V\.\*' . substitute(a:base, ":", '\\.\\*:\\.\\*', "g") . '\.\*' + let l:pattern = '^\V\.\*' . substitute(a:base, ':', '\\.\\*:\\.\\*', 'g') . '\.\*' endif let l:matches = [] @@ -88,43 +234,27 @@ function! beancount#complete_account(findstart, base) return l:matches endfunction -" Get list of acounts. -function! beancount#find_accounts(root_file) - python << EOM -import collections -import os -import re -import sys +function! beancount#query_single(root_file, query) abort +python << EOF import vim +import subprocess +import os -RE_INCLUDE = re.compile(r'^include\s+"([^\n"]+)"') -RE_ACCOUNT = re.compile(r'^\d{4,}-\d{2}-\d{2}\s+open\s+(\S+)') +# We intentionally want to ignore stderr so it doesn't mess up our query processing +output = subprocess.check_output(['bean-query', vim.eval('a:root_file'), vim.eval('a:query')], stderr=open(os.devnull, 'w')).split('\n') +output = output[2:] -def combine_paths(old, new): - return os.path.normpath( - new if os.path.isabs(new) else os.path.join(old, new)) +result_list = [y for y in (x.strip() for x in output) if y] -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)) +vim.command('return [{}]'.format(','.join(repr(x) for x in sorted(result_list)))) +EOF +endfunction -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 +" Call bean-doctor on the current line and dump output into a scratch buffer +function! beancount#get_context() abort + let l:context = system('bean-doctor context ' . expand('%') . ' ' . line('.')) + botright new + setlocal buftype=nofile bufhidden=hide noswapfile + call append(0, split(l:context, '\v\n')) + normal! gg endfunction diff --git a/compiler/beancount.vim b/compiler/beancount.vim index 5bcaa15..b6db2d3 100644 --- a/compiler/beancount.vim +++ b/compiler/beancount.vim @@ -1,19 +1,19 @@ -if exists("current_compiler") +if exists('g:current_compiler') finish endif -let current_compiler = "beancount" +let g:current_compiler = 'beancount' -if exists(":CompilerSet") != 2 " older Vim always used :setlocal +if exists(':CompilerSet') != 2 " older Vim always used :setlocal command -nargs=* CompilerSet setlocal <args> endif -let s:cpo_save = &cpo -set cpo-=C +let s:cpo_save = &cpoptions +set cpoptions-=C CompilerSet makeprg=bean-check\ % CompilerSet errorformat=%-G " Skip blank lines CompilerSet errorformat+=%f:%l:\ %m " File:line: message CompilerSet errorformat+=%-G\ %.%# " Skip indented lines. -let &cpo = s:cpo_save +let &cpoptions = s:cpo_save unlet s:cpo_save diff --git a/doc/beancount.txt b/doc/beancount.txt index 03f4c17..2c7a20e 100644 --- a/doc/beancount.txt +++ b/doc/beancount.txt @@ -11,6 +11,7 @@ Filetype plugin and functions for working with beancount files. Contents: Commands.............|beancount-commands| + Options...............|beancount-options| Completion.........|beancount-completion| Syntax.................|beancount-syntax| Compiler.............|beancount-compiler| @@ -23,7 +24,7 @@ COMMANDS *beancount-commands* *beancount-:AlignCommodity* :AlignCommodity Adds spaces between an account and commodity so that the decimal points of the commodities all occur in the column - given by `g:decimal_separator_col`. If an amount has no + given by |g:beancount_separator_col|. If an amount has no decimal point, the imaginary decimal point to the right of the least significant digit will align. @@ -33,15 +34,14 @@ COMMANDS *beancount-commands* will be pushed to the right the appropriate amount, so that it remains on the same character. - The alignment character can be set using - `g:beancount_decimal_separator`. The script assumes the - use of spaces for alignment. It does not understand tabs. + The script assumes the use of spaces for alignment. It + does not understand tabs. You can use the following insert-mode remap to automatically align commodities every time you type a decimal point: > - inoremap . .<C-O>:AlignCommodity<CR> + inoremap . .<C-\><C-O>:AlignCommodity<CR> < You may also want to set other mappings for this. For example, I use > @@ -50,14 +50,54 @@ COMMANDS *beancount-commands* vnoremap <buffer> <leader>= :AlignCommodity<CR> < + *beancount-:GetContext* +:GetContext Uses bean-doctor context to display the context of the + current line. + + You can use the following normal mode remap to open the + context of the line you're currently on: > + + nnoremap <buffer> <LocalLeader>c :GetContext<CR> +< + +OPTIONS *beancount-options* + +*g:beancount_account_completion* + See |beancount-completion|. Can be either 'default' or 'chunks'. + + Default value: 'default' + +*g:beancount_detailed_first* + If non-zero, accounts higher down the hierarchy will be listed first as + completions. + + Default value: 0 + +*g:beancount_separator_col* + The column that the decimal separator is aligned to. + + Default value: 50 + +*b:beancount_root* + Set the root Beancount file. This is used to gather values for the + completion. If not set, the current file will be used. + + Default value: not set COMPLETION *beancount-completion* -You can complete account names using CTRL-X CTRL-O. |i_CTRL-X_CTRL-O| -Accounts must have their `open` directive in the current file. Completion is -always case sensitive and exact. If the base string includes colons, each -colon-separated piece can separately match a piece of the account. +The plugin sets 'omnifunc' to provide omni completion for account names, tags, +links, and payees. Omni completion is accessible with |i_CTRL-X_CTRL-O|. + +Completion works for values defined |b:beancount_root| and included files. + +Account Completion~ + *beancount-complete-accounts* + +Completion is always case sensitive and exact. If the base string includes +colons, each colon-separated piece can separately match a piece of the +account. For example, `Ex:Other` would complete to `Expenses:Donations:Other` or `Liabilities:AmericanExpress:InterestOther`. @@ -67,15 +107,22 @@ has to match at the beginning of that level of the account hierarchy, e.g. `Ex:Oth` would match `Expenses:Other` but not `Expenses:Other:Something` nor one of the two examples given above. `Ex:Oth:` would, however, list all direct sub-accounts of `Expenses:Other`. -To enable this mode use +To enable this mode use > let g:beancount_account_completion = 'chunks' - +< Optionally, the list of candidates can be sorted by the number of levels in the account hierarchy (e.g. return 'Expenses:Other' before 'Expenses'). -This behavior can be enabled using +This behavior can be enabled using > let g:beancount_detailed_first = 1 +< +Deoplete~ + *beancount-complete-deoplete* + +The plugin ships with a source for the deoplete.nvim, which provides +asynchronous autocompletion. This should just work if deoplete is enabled. The +options for the account completion do not work for the deoplete source. SYNTAX *beancount-syntax* diff --git a/ftplugin/beancount.vim b/ftplugin/beancount.vim index bcdc8c4..b6e3540 100644 --- a/ftplugin/beancount.vim +++ b/ftplugin/beancount.vim @@ -1,32 +1,31 @@ -" These two variables customize the behavior of the AlignCommodity command. - -if exists("b:did_ftplugin") - finish +if exists('b:did_ftplugin') + finish endif let b:did_ftplugin = 1 -let b:undo_ftplugin = "setlocal foldmethod< comments< commentstring<" +let b:undo_ftplugin = 'setlocal foldmethod< comments< commentstring<' setl foldmethod=syntax setl comments=b:; setl commentstring=;%s compiler beancount -if !exists("g:beancount_separator_col") +" This variable customizes the behavior of the AlignCommodity command. +if !exists('g:beancount_separator_col') let g:beancount_separator_col = 50 endif -if !exists("g:beancount_decimal_separator") - let g:beancount_decimal_separator = "." -endif if !exists('g:beancount_account_completion') - let g:beancount_account_completion = 'default' + let g:beancount_account_completion = 'default' endif if !exists('g:beancount_detailed_first') - let g:beancount_detailed_first = 0 + let g:beancount_detailed_first = 0 endif command! -buffer -range AlignCommodity \ :call beancount#align_commodity(<line1>, <line2>) +command! -buffer -range GetContext + \ :call beancount#get_context() + " Omnifunc for account completion. -setl omnifunc=beancount#complete_account +setl omnifunc=beancount#complete diff --git a/indent/beancount.vim b/indent/beancount.vim index cfc8aa6..4303986 100644 --- a/indent/beancount.vim +++ b/indent/beancount.vim @@ -1,44 +1,44 @@ -if exists("b:did_indent") +if exists('b:did_indent') finish endif let b:did_indent = 1 setlocal indentexpr=GetBeancountIndent(v:lnum) -if exists("*GetBeancountIndent") +if exists('*GetBeancountIndent') finish endif function! s:IsDirective(str) - return a:str =~ '\v^\s*(\d{4}-\d{2}-\d{2}|pushtag|poptag|option|plugin|include)' + return a:str =~# '\v^\s*(\d{4}-\d{2}-\d{2}|pushtag|poptag|option|plugin|include)' endfunction -function! s:IsPost(str) - return a:str =~ '\v^\s*(Assets|Liabilities|Expenses|Equity|Income):' +function! s:IsPosting(str) + return a:str =~# '\v^\s*[A-Z]\w+:' endfunction function! s:IsMetadata(str) - return a:str =~ '\v^\s*\w+:\s' + return a:str =~# '\v^\s*[a-z][a-zA-Z0-9\-_]+:' endfunction function! s:IsTransaction(str) " The final \S represents the flag (e.g. * or !). - return a:str =~ '\v^\s*\d{4}-\d{2}-\d{2}\s+(txn\s+)?\S(\s|$)' + return a:str =~# '\v^\s*\d{4}-\d{2}-\d{2}\s+(txn\s+)?\S(\s|$)' endfunction function GetBeancountIndent(line_num) - let this_line = getline(a:line_num) - let prev_line = getline(a:line_num - 1) + let l:this_line = getline(a:line_num) + let l:prev_line = getline(a:line_num - 1) " Don't touch comments - if this_line =~ '\v^\s*;' | return -1 | endif + if l:this_line =~# '\v^\s*;' | return -1 | endif " This is a new directive or previous line is blank. - if prev_line =~ '^\s*$' || s:IsDirective(this_line) | return 0 | endif + if l:prev_line =~# '^\s*$' || s:IsDirective(l:this_line) | return 0 | endif " Previous line is transaction or this is a posting. - if s:IsTransaction(prev_line) || s:IsPost(this_line) | return &sw | endif - if s:IsMetadata(this_line) - let this_indent = indent(a:line_num - 1) - if ! s:IsMetadata(prev_line) | let this_indent += &sw | endif - return this_indent + if s:IsTransaction(l:prev_line) || s:IsPosting(l:this_line) | return &shiftwidth | endif + if s:IsMetadata(l:this_line) + let l:this_indent = indent(a:line_num - 1) + if ! s:IsMetadata(l:prev_line) | let l:this_indent += &shiftwidth | endif + return l:this_indent endif return -1 endfunction diff --git a/rplugin/python3/deoplete/sources/beancount.py b/rplugin/python3/deoplete/sources/beancount.py new file mode 100644 index 0000000..0194da3 --- /dev/null +++ b/rplugin/python3/deoplete/sources/beancount.py @@ -0,0 +1,114 @@ +import collections +import re + +from deoplete.source.base import Base + +try: + from beancount.loader import load_file + from beancount.core import data + HAS_BEANCOUNT = True +except ImportError: + HAS_BEANCOUNT = False + +DIRECTIVES = [ + 'open', 'close', 'commodity', 'txn', 'balance', 'pad', 'note', 'document', + 'price', 'event', 'query', 'custom' +] + + +class Source(Base): + def __init__(self, vim): + super().__init__(vim) + self.vim = vim + + self.name = 'beancount' + self.mark = '[bc]' + self.filetypes = ['beancount'] + self.rank = 500 + self.min_pattern_length = 0 + self.attributes = collections.defaultdict(list) + + def on_init(self, context): + if not HAS_BEANCOUNT: + self.error('Importing beancount failed.') + + def on_event(self, context): + self.__make_cache(context) + + def get_complete_position(self, context): + m = re.search(r'\S*$', context['input']) + return m.start() if m else -1 + + def gather_candidates(self, context): + attrs = self.attributes + if re.match(r'^\d{4}[/-]\d\d[/-]\d\d \w*$', context['input']): + return [{'word': x, 'kind': 'directive'} for x in DIRECTIVES] + # line that starts with whitespace (-> accounts) + if re.match(r'^(\s)+[\w:]+$', context['input']): + return [{'word': x, 'kind': 'account'} for x in attrs['accounts']] + # directive followed by account + if re.search( + r'(balance|document|note|open|close|pad(\s[\w:]+)?)' + r'\s[\w:]+$', + context['input']): + return [{'word': x, 'kind': 'account'} for x in attrs['accounts']] + # events + if re.search(r'event "[^"]*$', context['input']): + return [{ + 'word': '"{}"'.format(x), + 'kind': 'event' + } for x in attrs['events']] + # commodity after number + if re.search(r'\s([0-9]+|[0-9][0-9,]+[0-9])(\.[0-9]*)?\s\w+$', + context['input']): + return [{ + 'word': x, + 'kind': 'commodity' + } for x in attrs['commodities']] + if not context['complete_str']: + return [] + first = context['complete_str'][0] + if first == '#': + return [{'word': '#' + w, 'kind': 'tag'} for w in attrs['tags']] + elif first == '^': + return [{'word': '^' + w, 'kind': 'link'} for w in attrs['links']] + elif first == '"': + return [{ + 'word': '"{}"'.format(w), + 'kind': 'payee' + } for w in attrs['payees']] + return [] + + def __make_cache(self, context): + if not HAS_BEANCOUNT: + return + + entries, _, options = load_file(self.vim.eval("beancount#get_root()")) + + accounts = set() + events = set() + links = set() + payees = set() + tags = set() + + for entry in entries: + if isinstance(entry, data.Open): + accounts.add(entry.account) + elif isinstance(entry, data.Transaction): + if entry.payee: + payees.add(entry.payee) + if hasattr(entry, 'links') and entry.links: + links.update(entry.links) + if hasattr(entry, 'tags') and entry.tags: + tags.update(entry.tags) + if isinstance(entry, data.Event): + events.add(entry.type) + + self.attributes = { + 'accounts': sorted(accounts), + 'events': sorted(events), + 'commodities': options['commodities'], + 'links': sorted(links), + 'payees': sorted(payees), + 'tags': sorted(tags), + } diff --git a/syntax/beancount.vim b/syntax/beancount.vim index 04e72ea..5961c17 100644 --- a/syntax/beancount.vim +++ b/syntax/beancount.vim @@ -1,7 +1,7 @@ " Vim syntax file " Language: beancount " Maintainer: Nathan Grigg -" Latest Revision: 2014-10-07 +" Latest Revision: 2016-04-25 " if exists("b:current_syntax") " finish @@ -19,10 +19,13 @@ syn match beanCurrency "\v\w+" contained syn match beanAccount "\v[[:alnum:]]+:[-[:alnum:]:]+" contained syn match beanTag "\v#[-[:alnum:]]+" contained syn match beanLink "\v\^\S+" contained - +" We must require a space after the flag because you can have flags per +" transaction leg, and the letter-based flags might get confused with the +" start of an account name. +syn match beanFlag "\v[*!&#?%PSTCURM]\s\@=" contained " Most directives start with a date. -syn match beanDate "^\v\d{4}-\d{2}-\d{2}" skipwhite +syn match beanDate "^\v\d{4}[-/]\d{2}[-/]\d{2}" skipwhite \ nextgroup=beanOpen,beanTxn,beanClose,beanNote,beanBalance,beanEvent,beanPad,beanPrice " Options and events have two string arguments. The first, we are matching as " beanOptionTitle and the second as a regular string. @@ -30,6 +33,8 @@ syn region beanOption matchgroup=beanKeyword start="^option" end="$" \ keepend contains=beanOptionTitle,beanComment syn region beanOption matchgroup=beanKeyword start="^plugin" end="$" \ keepend contains=beanString,beanComment +syn region beanInclude matchgroup=beanKeyword start="^include" end="$" + \ keepend contains=beanString,beanComment syn region beanEvent matchgroup=beanKeyword start="event" end="$" contained \ keepend contains=beanOptionTitle,beanComment syn region beanOptionTitle start='"' skip='\\"' end='"' contained @@ -49,16 +54,28 @@ syn region beanPushTag matchgroup=beanKeyword start="\v^(push|pop)tag" end="$" syn region beanPad matchgroup=beanKeyword start="pad" end="$" contained \ keepend contains=beanAccount,beanComment -syn region beanTxn matchgroup=beanKeyword start="\v(txn)?\s+[*!]" skip="^\s" +syn region beanTxn matchgroup=beanKeyword start="\v\s+(txn|[*!&#?%PSTCURM])" skip="^\s" \ end="^" keepend contained fold \ contains=beanString,beanPost,beanComment,beanTag,beanLink,beanMeta -syn region beanPost start="^\v\C\s+[A-Z]@=" end="$" - \ contains=beanAccount,beanAmount,beanComment,beanCost,beanPrice -syn region beanMeta matchgroup=beanTag start="^\v\C\s+[-a-z]+:(\s|$)@=" end="$" +syn region beanPost start="^\v\C\s+(([*!&#?%PSTCURM]\s+)?[A-Z])@=" end="$" + \ contains=beanFlag,beanAccount,beanAmount,beanComment,beanCost,beanPrice +syn region beanMeta matchgroup=beanTag start="^\v\C\s+[a-z][-_a-zA-Z0-9]*:(\s|$)@=" end="$" syn region beanCost start="{" end="}" contains=beanAmount contained syn match beanPrice "\V@@\?" nextgroup=beanAmount contained +syn region beanHashHeaderFold + \ start="^\z(#\+\)" + \ skip="^\s*\z1#\+" + \ end="^\(#\)\@=" + \ fold contains=TOP + +syn region beanStarHeaderFold + \ start="^\z(\*\+\)" + \ skip="^\s*\z1\*\+" + \ end="^\(\*\)\@=" + \ fold contains=TOP + highlight default link beanKeyword Keyword highlight default link beanOptionTitle Keyword highlight default link beanDate Keyword @@ -72,3 +89,4 @@ highlight default link beanPrice Number highlight default link beanTag Comment highlight default link beanLink Comment highlight default link beanMeta Comment +highlight default link beanFlag Keyword diff --git a/syntax_checkers/beancount/bean_check.vim b/syntax_checkers/beancount/bean_check.vim index c3e5fce..b477104 100644 --- a/syntax_checkers/beancount/bean_check.vim +++ b/syntax_checkers/beancount/bean_check.vim @@ -4,17 +4,16 @@ endif let g:loaded_syntastic_beancount_bean_check=1 -let s:save_cpo = &cpo -set cpo&vim +let s:save_cpo = &cpoptions +set cpoptions&vim function! SyntaxCheckers_beancount_bean_check_IsAvailable() dict - return executable(self.getExec()) + return executable(l:self.getExec()) endfunction function! SyntaxCheckers_beancount_bean_check_GetLocList() dict - let makeprg = self.makeprgBuild({}) - - return SyntasticMake({ 'makeprg': makeprg }) + let l:makeprg = l:self.makeprgBuild({}) + return SyntasticMake({ 'makeprg': l:makeprg }) endfunction call g:SyntasticRegistry.CreateAndRegisterChecker({ @@ -22,5 +21,5 @@ call g:SyntasticRegistry.CreateAndRegisterChecker({ \ 'name': 'bean_check', \ 'exec': 'bean-check'}) -let &cpo = s:save_cpo +let &cpoptions = s:save_cpo unlet s:save_cpo diff --git a/test/align.vader b/test/align.vader new file mode 100644 index 0000000..76496fc --- /dev/null +++ b/test/align.vader @@ -0,0 +1,39 @@ +Given beancount: + 2012-12-12 balance Assets:LongLongLongAccount 50.00 + 2012-12-12 balance Assets:Cash 50.00 + 2012-12-12 price EUR 50.00 USD + metadata: 50 + Assets:Cash 50 + Assets:Cash 50.00 + ! Assets:Cash 50.00 + Assets:Cash 50.00 + Assets:Cash 50.00 USD + +Execute (align): + %AlignCommodity + +Expect beancount: + 2012-12-12 balance Assets:LongLongLongAccount 50.00 + 2012-12-12 balance Assets:Cash 50.00 + 2012-12-12 price EUR 50.00 USD + metadata: 50 + Assets:Cash 50 + Assets:Cash 50.00 + ! Assets:Cash 50.00 + Assets:Cash 50.00 + Assets:Cash 50.00 USD + +Execute (change alignment column and align again): + let g:beancount_separator_col=40 + %AlignCommodity + +Expect beancount: + 2012-12-12 balance Assets:LongLongLongAccount 50.00 + 2012-12-12 balance Assets:Cash 50.00 + 2012-12-12 price EUR 50.00 USD + metadata: 50 + Assets:Cash 50 + Assets:Cash 50.00 + ! Assets:Cash 50.00 + Assets:Cash 50.00 + Assets:Cash 50.00 USD |