runtime(beancount): Include Beancount runtime files
Commit:
https://github.com/vim/vim/commit/1fee3cd4b9a38b18656933d38a4977548d27808b
Author: Bruno BELANYI <
br...@belanyi.fr>
Date: Sat Jun 13 18:32:59 2026 +0000
runtime(beancount): Include Beancount runtime files
Include with adjustments from the upstream repo:
https://github.com/nathangrigg/vim-beancount
closes: #20373
Signed-off-by: Bruno BELANYI <
br...@belanyi.fr>
Signed-off-by: Nathan Grigg <
nat...@nathangrigg.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/.github/MAINTAINERS b/.github/MAINTAINERS
index fb7ebddf0..6a786bfb1 100644
--- a/.github/MAINTAINERS
+++ b/.github/MAINTAINERS
@@ -10,6 +10,8 @@
# will be requested to review.
nsis/lang/russian.nsi @RestorerZ
+runtime/autoload/beancount.vim @nathangrigg
+runtime/autoload/beancountcomplete.vim @nathangrigg
runtime/autoload/context.vim @lifepillar
runtime/autoload/freebasic.vim @dkearns
runtime/autoload/hare.vim @selenebun
@@ -50,6 +52,7 @@ runtime/colors/wildcharm.vim @habamax @romainl @neutaaaaan
runtime/colors/zaibatsu.vim @habamax @romainl @neutaaaaan
runtime/colors/zellner.vim @habamax @romainl @neutaaaaan
runtime/compiler/bash.vim @Konfekt
+runtime/compiler/bean_check.vim @nathangrigg
runtime/compiler/biome.vim @Konfekt
runtime/compiler/cabal.vim @mateoxh
runtime/compiler/checkstyle.vim @dkearns
@@ -143,6 +146,7 @@ runtime/ftplugin/asy.vim @avidseeker
runtime/ftplugin/autohotkey.vim @telemachus
runtime/ftplugin/awk.vim @dkearns
runtime/ftplugin/basic.vim @dkearns
+runtime/ftplugin/beancount.vim @nathangrigg
runtime/ftplugin/bicep.vim @scottmckendry
runtime/ftplugin/bicep-params.vim @scottmckendry
runtime/ftplugin/brighterscript.vim @ribru17
@@ -363,6 +367,7 @@ runtime/import/dist/vimhighlight.vim @lacygoill
runtime/indent/arduino.vim @k-takata
runtime/indent/astro.vim @wuelnerdotexe
runtime/indent/basic.vim @dkearns
+runtime/indent/beancount.vim @nathangrigg
runtime/indent/bpftrace.vim @sgruszka
runtime/indent/bst.vim @tpope
runtime/indent/cdl.vim @dkearns
@@ -491,6 +496,7 @@ runtime/syntax/asy.vim @avidseeker
runtime/syntax/autohotkey.vim @mmikeww
runtime/syntax/awk.vim @dkearns
runtime/syntax/basic.vim @dkearns
+runtime/syntax/beancount.vim @nathangrigg
runtime/syntax/bpftrace.vim @sgruszka
runtime/syntax/bst.vim @tpope
runtime/syntax/bzl.vim @dbarnett
diff --git a/runtime/autoload/README.txt b/runtime/autoload/README.txt
index 2180a744b..acc1bbf37 100644
--- a/runtime/autoload/README.txt
+++ b/runtime/autoload/README.txt
@@ -11,6 +11,8 @@ paste.vim common code for mswin.vim, menu.vim and macmap.vim
spellfile.vim downloading of a missing spell file
Omni completion files:
+adacomplete.vim Ada
+beancount.vim Beancount
ccomplete.vim C
csscomplete.vim HTML / CSS
htmlcomplete.vim HTML
diff --git a/runtime/autoload/beancount.vim b/runtime/autoload/beancount.vim
new file mode 100644
index 000000000..ca1aaf09d
--- /dev/null
+++ b/runtime/autoload/beancount.vim
@@ -0,0 +1,59 @@
+" Beancount specific formatting
+" Language: beancount
+" Maintainer: Nathan Grigg
+" Latest Revision: 2021-03-06
+
+" Align currency on decimal point.
+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, ' ' .
+ \ '^[\-/[:digit:]]+\s+balance\s+([A-Z][A-Za-z0-9\-]+)(:[A-Z0-9][A-Za-z0-9\-]*)+ ' .
+ \ '|^[\-/[:digit:]]+\s+price\s+\S+ ' .
+ \ '|^\s+([!&#?%PSTCURM]\s+)?([A-Z][A-Za-z0-9\-]+)(:[A-Z0-9][A-Za-z0-9\-]*)+ '
+ \ )
+ if l:end_account < 0
+ continue
+ endif
+
+ " 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, '^ ([-+])?[,[: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, l:cursor_col + l:need_spaces - l:has_spaces)
+ endif
+ endwhile
+endfunction
+
+" 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 ' . shellescape(expand('%')) . ' ' . line('.'))
+ botright new
+ setlocal buftype=nofile bufhidden=hide noswapfile
+ call append(0, split(l:context, '
'))
+ normal! gg
+endfunction
diff --git a/runtime/autoload/beancountcomplete.vim b/runtime/autoload/beancountcomplete.vim
new file mode 100644
index 000000000..4b778d30e
--- /dev/null
+++ b/runtime/autoload/beancountcomplete.vim
@@ -0,0 +1,215 @@
+" Vim completion script
+" Language: beancount
+" Maintainer: Nathan Grigg
+" Latest Revision: 2021-03-06
+
+let s:using_python3 = has('python3') || has('python3/dyn')
+
+" 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
+
+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) 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
+
+let s:directives = ['open', 'close', 'commodity', 'txn', 'balance', 'pad', 'note', 'document', 'price', 'event', 'query', 'custom']
+
+" ------------------------------
+" Completion functions
+" ------------------------------
+function! beancountcomplete#complete(findstart, base) abort
+ if a:findstart
+ let l:col = searchpos('\s', 'bn', line('.'))[1]
+ if l:col == 0
+ return -1
+ else
+ return l:col
+ endif
+ endif
+
+ let l:partial_line = strpart(getline('.'), 0, getpos('.')[2]-1)
+ " Match directive types
+ if l:partial_line =~# '^\d\d\d\d\(-\|/\)\d\d \d\d $'
+ return beancountcomplete#complete_basic(s:directives, a:base, '')
+ endif
+
+ " If we are using python3, now is a good time to load everything
+ call beancountcomplete#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 \d\d event $' && l:first ==# '"'
+ return beancountcomplete#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 beancountcomplete#load_currencies()
+ return beancountcomplete#complete_basic(b:beancount_currencies, a:base, '')
+ endif
+
+ if l:first ==# '#'
+ call beancountcomplete#load_tags()
+ return beancountcomplete#complete_basic(b:beancount_tags, l:rest, '#')
+ elseif l:first ==# '^'
+ call beancountcomplete#load_links()
+ return beancountcomplete#complete_basic(b:beancount_links, l:rest, '^')
+ elseif l:first ==# '"'
+ call beancountcomplete#load_payees()
+ return beancountcomplete#complete_basic(b:beancount_payees, l:rest, '"')
+ else
+ call beancountcomplete#load_accounts()
+ return beancountcomplete#complete_account(a:base)
+ endif
+endfunction
+
+function! beancountcomplete#get_root() abort
+ if exists('b:beancount_root')
+ return b:beancount_root
+ endif
+ return expand('%')
+endfunction
+
+function! beancountcomplete#load_everything() abort
+ if s:using_python3 && !exists('b:beancount_loaded')
+ let l:root = beancountcomplete#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.bindeval('b:')['beancount_accounts'] = sorted(accounts)
+vim.bindeval('b:')['beancount_currencies'] = sorted(currencies)
+vim.bindeval('b:')['beancount_events'] = sorted(events)
+vim.bindeval('b:')['beancount_links'] = sorted(links)
+vim.bindeval('b:')['beancount_payees'] = sorted(payees)
+vim.bindeval('b:')['beancount_tags'] = sorted(tags)
+vim.bindeval('b:')['beancount_loaded'] = 1
+EOF
+ endif
+endfunction
+
+function! beancountcomplete#load_accounts() abort
+ if !s:using_python3 && !exists('b:beancount_accounts')
+ let l:root = beancountcomplete#get_root()
+ let b:beancount_accounts = beancountcomplete#query_single(l:root, 'select distinct account;')
+ endif
+endfunction
+
+function! beancountcomplete#load_tags() abort
+ if !s:using_python3 && !exists('b:beancount_tags')
+ let l:root = beancountcomplete#get_root()
+ let b:beancount_tags = beancountcomplete#query_single(l:root, 'select distinct tags;')
+ endif
+endfunction
+
+function! beancountcomplete#load_links() abort
+ if !s:using_python3 && !exists('b:beancount_links')
+ let l:root = beancountcomplete#get_root()
+ let b:beancount_links = beancountcomplete#query_single(l:root, 'select distinct links;')
+ endif
+endfunction
+
+function! beancountcomplete#load_currencies() abort
+ if !s:using_python3 && !exists('b:beancount_currencies')
+ let l:root = beancountcomplete#get_root()
+ let b:beancount_currencies = beancountcomplete#query_single(l:root, 'select distinct currency;')
+ endif
+endfunction
+
+function! beancountcomplete#load_payees() abort
+ if !s:using_python3 && !exists('b:beancount_payees')
+ let l:root = beancountcomplete#get_root()
+ let b:beancount_payees = beancountcomplete#query_single(l:root, 'select distinct payee;')
+ endif
+endfunction
+
+" General completion function
+function! beancountcomplete#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! beancountcomplete#complete_account(base) abort
+ 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
+
+function! beancountcomplete#query_single(root_file, query) abort
+ if s:using_python3
+python3 << EOF
+import vim
+import subprocess
+
+# 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=subprocess.DEVNULL,
+ text=True,
+).splitlines()
+output = output[2:]
+result_list = sorted(y for y in (x.strip() for x in output) if y)
+EOF
+ return py3eval('result_list')
+ else
+ return []
+ endif
+endfunction
diff --git a/runtime/compiler/bean_check.vim b/runtime/compiler/bean_check.vim
new file mode 100644
index 000000000..45f6b0b4d
--- /dev/null
+++ b/runtime/compiler/bean_check.vim
@@ -0,0 +1,22 @@
+" Vim compiler file
+" Compiler: bean-check
+" Maintainer: Nathan Grigg
+" Latest Revision: 2017-03-20
+
+if exists('g:current_compiler')
+ finish
+endif
+let g:current_compiler = 'bean_check'
+
+let s:cpo_save = &cpoptions
+set cpoptions-=C
+
+CompilerSet makeprg=bean-check\ %
+" File:line: message
+" Skip blank lines and indented lines.
+CompilerSet errorformat=%-G
+CompilerSet errorformat+=%f:%l:\ %m
+CompilerSet errorformat+=%-G\ %.%#
+
+let &cpoptions = s:cpo_save
+unlet s:cpo_save
diff --git a/runtime/doc/filetype.txt b/runtime/doc/filetype.txt
index a9a0e9220..36edcaab2 100644
--- a/runtime/doc/filetype.txt
+++ b/runtime/doc/filetype.txt
@@ -1,4 +1,4 @@
-*filetype.txt* For Vim version 9.2. Last change: 2026 May 29
+*filetype.txt* For Vim version 9.2. Last change: 2026 Jun 13
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -482,6 +482,65 @@ Support for features specific to GNU Awk, like @include, can be enabled by
setting: >
:let g:awk_is_gawk = 1
+BEANCOUNT *ft-beancount-plugin*
+
+Beancount omni-completion |compl-omni| is provided by beancountcomplete.vim,
+if enabled with |g:beancount_completion_enable|.
+
+To enable completion of account names, set: >
+ let g:beancount_completion_enable = 1
+
+< Note enabling this may cause beancount to load additional plugin files.
+Only enable for code you trust.
+
+Variables:
+*g:beancount_account_completion*
+ Can be either 'default' or 'chunks'.
+
+ Default value: 'default'
+
+*g:beancount_completion_enable*
+ Enable omni-completion |compl-omni| for accounts.
+
+ Default value: 0
+
+*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
+
+
+Commands:
+:AlignCommodity Adds spaces between an account and commodity so that the
+ decimal points of the commodities all occur in the column
+ 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.
+
+ The command acts on a range, with the default being the
+ current line. If the cursor happens to be inside that
+ range and to the right of the account name, the cursor
+ will be pushed to the right the appropriate amount, so
+ that it remains on the same character.
+
+ The script assumes the use of spaces for alignment. It
+ does not understand tabs.
+
+:GetContext Uses bean-doctor context to display the context of the
+ current line.
CHANGELOG *ft-changelog-plugin*
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 88fad5440..d056a7eed 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -6343,6 +6343,7 @@ a{ motion.txt /*a{*
a} motion.txt /*a}*
b motion.txt /*b*
b: eval.txt /*b:*
+b:beancount_root filetype.txt /*b:beancount_root*
b:changedtick eval.txt /*b:changedtick*
b:changelog_name filetype.txt /*b:changelog_name*
b:clojure_syntax_keywords syntax.txt /*b:clojure_syntax_keywords*
@@ -7544,6 +7545,7 @@ ft-asy-syntax syntax.txt /*ft-asy-syntax*
ft-awk-plugin filetype.txt /*ft-awk-plugin*
ft-bash-syntax syntax.txt /*ft-bash-syntax*
ft-basic-syntax syntax.txt /*ft-basic-syntax*
+ft-beancount-plugin filetype.txt /*ft-beancount-plugin*
ft-c-omni insert.txt /*ft-c-omni*
ft-c-syntax syntax.txt /*ft-c-syntax*
ft-cangjie-syntax syntax.txt /*ft-cangjie-syntax*
@@ -7820,6 +7822,10 @@ g:ada_space_errors ft_ada.txt /*g:ada_space_errors*
g:ada_standard_types ft_ada.txt /*g:ada_standard_types*
g:ada_with_gnat_project_files ft_ada.txt /*g:ada_with_gnat_project_files*
g:ada_withuse_ordinary ft_ada.txt /*g:ada_withuse_ordinary*
+g:beancount_account_completion filetype.txt /*g:beancount_account_completion*
+g:beancount_completion_enable filetype.txt /*g:beancount_completion_enable*
+g:beancount_detailed_first filetype.txt /*g:beancount_detailed_first*
+g:beancount_separator_col filetype.txt /*g:beancount_separator_col*
g:cargo_makeprg_params ft_rust.txt /*g:cargo_makeprg_params*
g:cargo_shell_command_runner ft_rust.txt /*g:cargo_shell_command_runner*
g:clojure_align_multiline_strings indent.txt /*g:clojure_align_multiline_strings*
diff --git a/runtime/ftplugin/beancount.vim b/runtime/ftplugin/beancount.vim
new file mode 100644
index 000000000..d8bcecde9
--- /dev/null
+++ b/runtime/ftplugin/beancount.vim
@@ -0,0 +1,35 @@
+if exists('b:did_ftplugin')
+ finish
+endif
+
+let b:did_ftplugin = 1
+let b:undo_ftplugin = 'setlocal foldmethod< comments< commentstring< omnifunc<'
+let b:undo_ftplugin .= '| delc -buffer AlignCommodity'
+let b:undo_ftplugin .= '| delc -buffer GetContext'
+
+setl foldmethod=syntax
+setl comments=b:;
+setl commentstring=;\ %s
+compiler bean_check
+
+" 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_account_completion')
+ let g:beancount_account_completion = 'default'
+endif
+if !exists('g:beancount_detailed_first')
+ 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.
+if get(g:, 'beancount_completion_enable', 0)
+ setl omnifunc=beancountcomplete#complete
+endif
diff --git a/runtime/indent/beancount.vim b/runtime/indent/beancount.vim
new file mode 100644
index 000000000..594ae7108
--- /dev/null
+++ b/runtime/indent/beancount.vim
@@ -0,0 +1,51 @@
+" Vim indent file
+" Language: beancount
+" Maintainer: Nathan Grigg
+" Latest Revision: 2017-03-20
+
+if exists('b:did_indent')
+ finish
+endif
+let b:did_indent = 1
+
+setlocal indentexpr=GetBeancountIndent(v:lnum)
+
+let b:undo_indent = "setl inde<"
+
+if exists('*GetBeancountIndent')
+ finish
+endif
+
+function! s:IsDirective(str)
+ return a:str =~# ' ^\s*(\d{4}-\d{2}-\d{2}|pushtag|poptag|option|plugin|include)'
+endfunction
+
+function! s:IsPosting(str)
+ return a:str =~# ' ^\s*[A-Z]\w+:'
+endfunction
+
+function! s:IsMetadata(str)
+ return a:str =~# ' ^\s*[a-z][a-zA-Z0-9\-_]+:'
+endfunction
+
+function! s:IsTransaction(str)
+ " The final \S represents the flag (e.g. * or !).
+ return a:str =~# ' ^\s*\d{4}-\d{2}-\d{2}\s+(txn\s+)?\S(\s|$)'
+endfunction
+
+function GetBeancountIndent(line_num)
+ let l:this_line = getline(a:line_num)
+ let l:prev_line = getline(a:line_num - 1)
+ " Don't touch comments
+ if l:this_line =~# ' ^\s*;' | return -1 | endif
+ " This is a new directive or previous line is blank.
+ 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(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/runtime/syntax/beancount.vim b/runtime/syntax/beancount.vim
new file mode 100644
index 000000000..4909c4bc0
--- /dev/null
+++ b/runtime/syntax/beancount.vim
@@ -0,0 +1,94 @@
+" Vim syntax file
+" Language: beancount
+" Maintainer: Nathan Grigg
+" Latest Revision: 2024-11-25
+
+if exists("b:current_syntax")
+ finish
+endif
+
+syntax clear
+" Basics.
+syn region beanComment start="\s*;" end="$" keepend contains=beanMarker
+syn match beanMarker " (\{\{\{|\}\}\})\d?" contained
+syn region beanString start='"' skip='\"' end='"' contained
+syn match beanAmount " [-+]?[[:digit:].,]+" nextgroup=beanCurrency contained
+ \ skipwhite
+syn match beanCurrency " \w+" contained
+" Account name: alphanumeric with at least one colon.
+syn match beanAccount " [[:alnum:]]+:[-[:alnum:]:]+" contained
+syn match beanTag " #[-[:alnum:]]+" contained
+syn match beanLink " \^\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 " [*!&#?%PSTCURM]\s\@=" contained
+
+" Most directives start with a date.
+syn match beanDate "^ \d{4}[-/]\d{2}[-/]\d{2}" skipwhite
+ \ nextgroup=beanOpen,beanTxn,beanClose,beanCommodity,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.
+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
+ \ nextgroup=beanString skipwhite
+syn region beanOpen matchgroup=beanKeyword start="open" end="$" keepend
+ \ contained contains=beanAccount,beanCurrency,beanComment
+syn region beanClose matchgroup=beanKeyword start="close" end="$" keepend
+ \ contained contains=beanAccount,beanComment
+syn region beanCommodity matchgroup=beanKeyword start="commodity" end="$" keepend
+ \ contained contains=beanCurrency,beanComment
+syn region beanNote matchgroup=beanKeyword start=" note|document" end="$"
+ \ keepend contains=beanAccount,beanString,beanComment contained
+syn region beanBalance matchgroup=beanKeyword start="balance" end="$" contained
+ \ keepend contains=beanAccount,beanAmount,beanComment
+syn region beanPrice matchgroup=beanKeyword start="price" end="$" contained
+ \ keepend contains=beanCurrency,beanAmount
+syn region beanPushTag matchgroup=beanKeyword start=" ^(push|pop)tag" end="$"
+ \ keepend contains=beanTag
+syn region beanPad matchgroup=beanKeyword start="pad" end="$" contained
+ \ keepend contains=beanAccount,beanComment
+
+syn region beanTxn matchgroup=beanKeyword start=" \s+(txn|[*!&#?%PSTCURM])" skip="^\s"
+ \ end="^" keepend contained fold
+ \ contains=beanString,beanPost,beanComment,beanTag,beanLink,beanMeta
+syn region beanPost start="^ \C\s+(([*!&#?%PSTCURM]\s+)?[A-Z])@=" end="$"
+ \ contains=beanFlag,beanAccount,beanAmount,beanComment,beanCost,beanPrice
+syn region beanMeta matchgroup=beanTag start="^ \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
+highlight default link beanString String
+highlight default link beanComment Comment
+highlight default link beanAccount Identifier
+highlight default link beanAmount Number
+highlight default link beanCurrency Number
+highlight default link beanCost Number
+highlight default link beanPrice Number
+highlight default link beanTag Tag
+highlight default link beanLink Comment
+highlight default link beanMeta Special
+highlight default link beanFlag Keyword