aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml15
-rw-r--r--autoload/beancount.vim302
-rw-r--r--compiler/beancount.vim12
-rw-r--r--doc/beancount.txt71
-rw-r--r--ftplugin/beancount.vim23
-rw-r--r--indent/beancount.vim32
-rw-r--r--rplugin/python3/deoplete/sources/beancount.py114
-rw-r--r--syntax/beancount.vim32
-rw-r--r--syntax_checkers/beancount/bean_check.vim13
-rw-r--r--test/align.vader39
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