diff --git a/runtime/autoload/README.txt b/runtime/autoload/README.txt index 3a1f1e3e2a..5f531f8ea1 100644 --- a/runtime/autoload/README.txt +++ b/runtime/autoload/README.txt @@ -11,6 +11,8 @@ paste.vim common code for mswin.vim and menu.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 0000000000..ca1aaf09d1 --- /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, '\v' . + \ '^[\-/[: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, '^\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, 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, '\v\n')) + normal! gg +endfunction diff --git a/runtime/autoload/beancountcomplete.vim b/runtime/autoload/beancountcomplete.vim new file mode 100644 index 0000000000..4b778d30e0 --- /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\1\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\1\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 0000000000..45f6b0b4d2 --- /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 2954de8e2b..f01073215c 100644 --- a/runtime/doc/filetype.txt +++ b/runtime/doc/filetype.txt @@ -446,6 +446,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/ftplugin/beancount.vim b/runtime/ftplugin/beancount.vim new file mode 100644 index 0000000000..d8bcecde94 --- /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(, ) + +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 0000000000..594ae71086 --- /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 =~# '\v^\s*(\d{4}-\d{2}-\d{2}|pushtag|poptag|option|plugin|include)' +endfunction + +function! s:IsPosting(str) + return a:str =~# '\v^\s*[A-Z]\w+:' +endfunction + +function! s:IsMetadata(str) + 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|$)' +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 =~# '\v^\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 0000000000..4909c4bc08 --- /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 "\v(\{\{\{|\}\}\})\d?" contained +syn region beanString start='"' skip='\\"' end='"' contained +syn match beanAmount "\v[-+]?[[:digit:].,]+" nextgroup=beanCurrency contained + \ skipwhite +syn match beanCurrency "\v\w+" contained +" Account name: alphanumeric with at least one colon. +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 + \ 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="\vnote|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="\v^(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="\v\s+(txn|[*!&#?%PSTCURM])" skip="^\s" + \ end="^" keepend contained fold + \ contains=beanString,beanPost,beanComment,beanTag,beanLink,beanMeta +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 +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 diff --git a/runtime/syntax/pfmain.vim b/runtime/syntax/pfmain.vim index af58da70ef..886fd1e931 100644 --- a/runtime/syntax/pfmain.vim +++ b/runtime/syntax/pfmain.vim @@ -3,6 +3,7 @@ " Maintainer: KELEMEN Peter " Last Updates: Anton Shestakov, Hong Xu " Last Change: 2015 Feb 10 +" 2026 Jun 13 by Vim project: add smtp_destination_rate_delay #20505 " Version: 0.40 " URL: http://cern.ch/fuji/vim/syntax/pfmain.vim " Comment: Based on Postfix 2.12/3.0 postconf.5.html. @@ -497,6 +498,7 @@ syntax keyword pfmainConf smtp_defer_if_no_mx_address_found syntax keyword pfmainConf smtp_delivery_status_filter syntax keyword pfmainConf smtp_destination_concurrency_limit syntax keyword pfmainConf smtp_destination_recipient_limit +syntax keyword pfmainConf smtp_destination_rate_delay syntax keyword pfmainConf smtp_discard_ehlo_keyword_address_maps syntax keyword pfmainConf smtp_discard_ehlo_keywords syntax keyword pfmainConf smtp_dns_reply_filter @@ -1290,6 +1292,7 @@ syntax match pfmainRef "$\" syntax match pfmainRef "$\" syntax match pfmainRef "$\" syntax match pfmainRef "$\" +syntax match pfmainRef "$\" syntax match pfmainRef "$\" syntax match pfmainRef "$\" syntax match pfmainRef "$\"