summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--contrib/vim/compiler/ledger.vim13
-rw-r--r--contrib/vim/ftplugin/ledger.vim373
-rw-r--r--contrib/vim/syntax/ledger.vim58
3 files changed, 336 insertions, 108 deletions
diff --git a/contrib/vim/compiler/ledger.vim b/contrib/vim/compiler/ledger.vim
index 33b019cb..e9e98a8e 100644
--- a/contrib/vim/compiler/ledger.vim
+++ b/contrib/vim/compiler/ledger.vim
@@ -12,20 +12,17 @@ if exists(":CompilerSet") != 2
command -nargs=* CompilerSet setlocal <args>
endif
-if ! exists("g:ledger_bin") || ! executable(g:ledger_bin)
- if executable('ledger')
- let g:ledger_bin = 'ledger'
- else
- echoerr "ledger command not found. Set g:ledger_bin or extend $PATH."
- finish
- endif
+" default value will be set in ftplugin
+if ! exists("g:ledger_bin") || empty(g:ledger_bin) || ! executable(split(g:ledger_bin, '\s')[0])
+ finish
endif
" %-G throws away blank lines, everything else is assumed to be part of a
" multi-line error message.
CompilerSet errorformat=%-G,%EWhile\ parsing\ file\ \"%f\"\\,\ line\ %l:%.%#,%ZError:\ %m,%C%.%#
+CompilerSet errorformat+=%tarning:\ \"%f\"\\,\ line\ %l:\ %m
" unfortunately there is no 'check file' command,
" so we will just use a query that returns no results. ever.
-exe 'CompilerSet makeprg='.g:ledger_bin.'\ -f\ %\ reg\ not\ ''.*''\ \>\ /dev/null'
+exe 'CompilerSet makeprg='.substitute(g:ledger_bin, ' ', '\\ ', 'g').'\ -f\ %\ reg\ not\ ''.*''\ \>\ /dev/null'
diff --git a/contrib/vim/ftplugin/ledger.vim b/contrib/vim/ftplugin/ledger.vim
index d75a6869..13602e5d 100644
--- a/contrib/vim/ftplugin/ledger.vim
+++ b/contrib/vim/ftplugin/ledger.vim
@@ -12,7 +12,7 @@ let b:did_ftplugin = 1
let b:undo_ftplugin = "setlocal ".
\ "foldmethod< foldtext< ".
- \ "include< comments< omnifunc< "
+ \ "include< comments< omnifunc< formatprg<"
" don't fill fold lines --> cleaner look
setl fillchars="fold: "
@@ -22,6 +22,21 @@ setl include=^!include
setl comments=b:;
setl omnifunc=LedgerComplete
+" set location of ledger binary for checking and auto-formatting
+if ! exists("g:ledger_bin") || empty(g:ledger_bin) || ! executable(split(g:ledger_bin, '\s')[0])
+ if executable('ledger')
+ let g:ledger_bin = 'ledger'
+ else
+ unlet g:ledger_bin
+ echoerr "ledger command not found. Set g:ledger_bin or extend $PATH ".
+ \ "to enable error checking and auto-formatting."
+ endif
+endif
+
+if exists("g:ledger_bin")
+ exe 'setl formatprg='.substitute(g:ledger_bin, ' ', '\\ ', 'g').'\ -f\ -\ print'
+endif
+
" You can set a maximal number of columns the fold text (excluding amount)
" will use by overriding g:ledger_maxwidth in your .vimrc.
" When maxwidth is zero, the amount will be displayed at the far right side
@@ -92,7 +107,7 @@ function! LedgerFoldText() "{{{1
\ '\(^\s\+\|\s\+$\)', '', 'g')
" number of columns foldtext can use
- let columns = s:get_columns(0)
+ let columns = s:get_columns()
if g:ledger_maxwidth
let columns = min([columns, g:ledger_maxwidth])
endif
@@ -107,9 +122,9 @@ function! LedgerFoldText() "{{{1
let foldtext .= repeat(' ', filen - (folen%filen))
let foldtext .= repeat(g:ledger_fillstring,
- \ s:get_columns(0)/filen)
+ \ s:get_columns()/filen)
else
- let foldtext .= repeat(' ', s:get_columns(0))
+ let foldtext .= repeat(' ', s:get_columns())
endif
" we don't use slices[:5], because that messes up multibyte characters
@@ -123,37 +138,7 @@ function! LedgerComplete(findstart, base) "{{{1
let lnum = line('.')
let line = getline('.')
let lastcol = col('.') - 2
- if line =~ '^\d' "{{{2 (date / payee / description)
- let b:compl_context = 'payee'
- return -1
- elseif line =~ '^\s\+;' "{{{2 (metadata / tags)
- let b:compl_context = 'meta-tag'
- let first_possible = matchend(line, '^\s\+;')
-
- " find first column of text to be replaced
- let firstcol = lastcol
- while firstcol >= 0
- if firstcol <= first_possible
- " Stop before the ';' don't ever include it
- let firstcol = first_possible
- break
- elseif line[firstcol] =~ ':'
- " Stop before first ':'
- let firstcol += 1
- break
- endif
-
- let firstcol -= 1
- endwhile
-
- " strip whitespace starting from firstcol
- let end_of_whitespace = matchend(line, '^\s\+', firstcol)
- if end_of_whitespace != -1
- let firstcol = end_of_whitespace
- endif
-
- return firstcol
- elseif line =~ '^\s\+' "{{{2 (account)
+ if line =~ '^\s\+[^[:blank:];]' "{{{2 (account)
let b:compl_context = 'account'
if matchend(line, '^\s\+\%(\S \S\|\S\)\+') <= lastcol
" only allow completion when in or at end of account name
@@ -176,16 +161,12 @@ function! LedgerComplete(findstart, base) "{{{1
let results = LedgerFindInTree(LedgerGetAccountHierarchy(), hierarchy)
" sort by alphabet and reverse because it will get reversed one more time
- let results = reverse(sort(results))
if g:ledger_detailed_first
- let results = sort(results, 's:sort_accounts_by_depth')
+ let results = reverse(sort(results, 's:sort_accounts_by_depth'))
+ else
+ let results = sort(results)
endif
- call add(results, a:base)
- return reverse(results)
- elseif b:compl_context == 'meta-tag' "{{{2
- unlet! b:compl_context
- let results = [a:base]
- call extend(results, sort(s:filter_items(keys(LedgerGetTags()), a:base)))
+ call insert(results, a:base)
return results
else "}}}
unlet! b:compl_context
@@ -228,52 +209,306 @@ function! LedgerGetAccountHierarchy() "{{{1
return hierarchy
endf "}}}
-function! LedgerGetTags() "{{{1
- let alltags = {}
- let metalines = s:grep_buffer('^\s\+;\s*\zs.*$')
- for line in metalines
- " (spaces at beginning are stripped by matchstr!)
- if line[0] == ':'
- " multiple tags
- for val in split(line, ':')
- if val !~ '^\s*$'
- let name = s:strip_spaces(val)
- let alltags[name] = get(alltags, name, [])
- endif
- endfor
- elseif line =~ '^.*:.*$'
- " line with tag=value
- let name = s:strip_spaces(split(line, ':')[0])
- let val = s:strip_spaces(join(split(line, ':')[1:], ':'))
- let values = get(alltags, name, [])
- call add(values, val)
- let alltags[name] = values
+function! LedgerToggleTransactionState(lnum, ...)
+ if a:0 == 1
+ let chars = a:1
+ else
+ let chars = ' *'
+ endif
+ let trans = s:transaction.from_lnum(a:lnum)
+ if empty(trans)
+ return
+ endif
+
+ let old = has_key(trans, 'state') ? trans['state'] : ' '
+ let i = stridx(chars, old) + 1
+ let new = chars[i >= len(chars) ? 0 : i]
+
+ call trans.set_state(new)
+
+ call setline(trans['head'], trans.format_head())
+endf
+
+function! LedgerSetTransactionState(lnum, char) "{{{1
+ " modifies or sets the state of the transaction at the cursor,
+ " removing the state alltogether if a:char is empty
+ let trans = s:transaction.from_lnum(a:lnum)
+ if empty(trans)
+ return
+ endif
+
+ call trans.set_state(a:char)
+
+ call setline(trans['head'], trans.format_head())
+endf "}}}
+
+function! LedgerSetDate(lnum, type, ...) "{{{1
+ let time = a:0 == 1 ? a:1 : localtime()
+ let trans = s:transaction.from_lnum(a:lnum)
+ if empty(trans)
+ return
+ endif
+
+ let formatted = strftime('%Y/%m/%d', time)
+ if has_key(trans, 'date') && ! empty(trans['date'])
+ let date = split(trans['date'], '=')
+ else
+ let date = [formatted]
+ endif
+
+ if a:type ==? 'actual'
+ let date[0] = formatted
+ elseif a:type ==? 'effective'
+ if time < 0
+ " remove effective date
+ let date = [date[0]]
+ else
+ " set effective date
+ if len(date) >= 2
+ let date[1] = formatted
+ else
+ call add(date, formatted)
+ endif
endif
+ endif
+
+ let trans['date'] = join(date, '=')
+
+ call setline(trans['head'], trans.format_head())
+endf "}}}
+
+let s:transaction = {} "{{{1
+function! s:transaction.new() dict
+ return copy(s:transaction)
+endf
+
+function! s:transaction.from_lnum(lnum) dict "{{{2
+ let [head, tail] = s:get_transaction_extents(a:lnum)
+ if ! head
+ return {}
+ endif
+
+ let trans = copy(s:transaction)
+ let trans['head'] = head
+ let trans['tail'] = tail
+
+ let parts = split(getline(head), '\s\+')
+ if parts[0] ==# '~'
+ let trans['expr'] = join(parts[1:])
+ return trans
+ elseif parts[0] !~ '^\d'
+ " this case is avoided in s:get_transaction_extents(),
+ " but we'll check anyway.
+ return {}
+ endif
+
+ for part in parts
+ if ! has_key(trans, 'date') && part =~ '^\d'
+ let trans['date'] = part
+ elseif ! has_key(trans, 'code') && part =~ '^([^)]*)$'
+ let trans['code'] = part[1:-2]
+ elseif ! has_key(trans, 'state') && part =~ '^[[:punct:]]$'
+ " the first character by itself is assumed to be the state of the transaction.
+ let trans['state'] = part
+ else
+ " everything after date/code or state belongs to the description
+ break
+ endif
+ call remove(parts, 0)
endfor
- return alltags
+
+ " FIXME: this will break comments at the end of this 'head' line
+ " they need 2 spaces in front of the semicolon
+ let trans['description'] = join(parts)
+ return trans
endf "}}}
+function! s:transaction.set_state(char) dict "{{{2
+ if has_key(self, 'state') && a:char =~ '^\s*$'
+ call remove(self, 'state')
+ else
+ let self['state'] = a:char
+ endif
+endf "}}}
+
+function! s:transaction.parse_body(...) dict "{{{2
+ if a:0 == 2
+ let head = a:1
+ let tail = a:2
+ elseif a:0 == 0
+ let head = self['head']
+ let tail = self['tail']
+ else
+ throw "wrong number of arguments for parse_body()"
+ return []
+ endif
+
+ if ! head || tail <= head
+ return []
+ endif
+
+ let lnum = head
+ let tags = {}
+ let postings = []
+ while lnum <= tail
+ let line = split(getline(lnum), '\s*\%(\t\| \);', 1)
+
+ if line[0] =~ '^\s\+[^[:blank:];]'
+ " posting
+ " FIXME: replaces original spacing in amount with single spaces
+ let parts = split(line[0], '\%(\t\| \)\s*')
+ call add(postings, {'account': parts[0], 'amount': join(parts[1:], ' ')})
+ end
+
+ " where are tags to be stored?
+ if empty(postings)
+ " they belong to the transaction
+ let tag_container = tags
+ else
+ " they belong to last posting
+ if ! has_key(postings[-1], 'tags')
+ let postings[-1]['tags'] = {}
+ endif
+ let tag_container = postings[-1]['tags']
+ endif
+
+ let comment = join(line[1:], ' ;')
+ if comment =~ '^\s*:'
+ " tags without values
+ for t in s:findall(comment, ':\zs[^:[:blank:]]\([^:]*[^:[:blank:]]\)\?\ze:')
+ let tag_container[t] = ''
+ endfor
+ elseif comment =~ '^\s*[^:[:blank:]][^:]\+:'
+ " tag with value
+ let key = matchstr(comment, '^\s*\zs[^:]\+\ze:')
+ if ! empty(key)
+ let val = matchstr(comment, ':\s*\zs.*\ze\s*$')
+ let tag_container[key] = val
+ endif
+ endif
+ let lnum += 1
+ endw
+ return [tags, postings]
+endf "}}}
+
+function! s:transaction.format_head() dict "{{{2
+ if has_key(self, 'expr')
+ return '~ '.self['expr']
+ endif
+
+ let parts = []
+ if has_key(self, 'date') | call add(parts, self['date']) | endif
+ if has_key(self, 'code') | call add(parts, '('.self['code'].')') | endif
+ if has_key(self, 'state') | call add(parts, self['state']) | endif
+ if has_key(self, 'description') | call add(parts, self['description']) | endif
+ return join(parts)
+endf "}}}
+"}}}
+
" Helper functions {{{1
+function! s:get_transactions(...) "{{{2
+ if a:0 == 2
+ let lnum = a:1
+ let end = a:2
+ elseif a:0 == 0
+ let lnum = 1
+ let end = line('$')
+ else
+ throw "wrong number of arguments for get_transactions()"
+ return []
+ endif
+
+ " safe view / position
+ let view = winsaveview()
+ let fe = &foldenable
+ set nofoldenable
+
+ let transactions = []
+ call cursor(lnum, 0)
+ while lnum && lnum <= end
+ let trans = s:transaction.from_lnum(lnum)
+ if ! empty(trans)
+ call add(transactions, trans)
+ call cursor(trans['tail'], 0)
+ endif
+ let lnum = search('^[~[:digit:]]\S\+', 'cW')
+ endw
+
+ " restore view / position
+ let &foldenable = fe
+ call winrestview(view)
+
+ return transactions
+endf "}}}
+
+function! s:get_transaction_extents(lnum) "{{{2
+ if ! (indent(a:lnum) || getline(a:lnum) =~ '^[~[:digit:]]\S\+')
+ " only do something if lnum is in a transaction
+ return [0, 0]
+ endif
+
+ " safe view / position
+ let view = winsaveview()
+ let fe = &foldenable
+ set nofoldenable
+
+ call cursor(a:lnum, 0)
+ let head = search('^[~[:digit:]]\S\+', 'bcnW')
+ let tail = search('^[^;[:blank:]]\S\+', 'nW')
+ let tail = tail > head ? tail - 1 : line('$')
+
+ " restore view / position
+ let &foldenable = fe
+ call winrestview(view)
+
+ return head ? [head, tail] : [0, 0]
+endf "}}}
+
+function! s:findall(text, rx) " {{{2
+ " returns all the matches in a string,
+ " there will be overlapping matches according to :help match()
+ let matches = []
+
+ while 1
+ let m = matchstr(a:text, a:rx, 0, len(matches)+1)
+ if empty(m)
+ break
+ endif
+
+ call add(matches, m)
+ endw
+
+ return matches
+endf "}}}
+
" return length of string with fix for multibyte characters
function! s:multibyte_strlen(text) "{{{2
return strlen(substitute(a:text, ".", "x", "g"))
endfunction "}}}
" get # of visible/usable columns in current window
-function! s:get_columns(win) "{{{2
+function! s:get_columns() " {{{2
" As long as vim doesn't provide a command natively,
" we have to compute the available columns.
" see :help todo.txt -> /Add argument to winwidth()/
- " FIXME: Although this will propably never be used with debug mode enabled
- " this should take the signs column into account (:help sign.txt)
- let columns = (winwidth(a:win) == 0 ? 80 : winwidth(a:win)) - &foldcolumn
+
+ let columns = (winwidth(0) == 0 ? 80 : winwidth(0)) - &foldcolumn
if &number
" line('w$') is the line number of the last line
let columns -= max([len(line('w$'))+1, &numberwidth])
endif
+
+ " are there any signs/is the sign column displayed?
+ redir => signs
+ silent execute 'sign place buffer='.string(bufnr("%"))
+ redir END
+ if signs =~# 'id='
+ let columns -= 2
+ endif
+
return columns
-endfunction "}}}
+endf "}}}
" remove spaces at start and end of string
function! s:strip_spaces(text) "{{{2
diff --git a/contrib/vim/syntax/ledger.vim b/contrib/vim/syntax/ledger.vim
index 61f9a2e5..d4097e2b 100644
--- a/contrib/vim/syntax/ledger.vim
+++ b/contrib/vim/syntax/ledger.vim
@@ -28,43 +28,39 @@ syntax clear
" DATE[=EDATE] [*|!] [(CODE)] DESC <-- first line of transaction
" ACCOUNT AMOUNT [; NOTE] <-- posting
-" region: a transaction containing postings
-syn region transNorm start=/^[[:digit:]~]/ skip=/^\s/ end=/^/
- \ fold keepend transparent contains=transDate,Metadata,Posting
-syn match transDate /^\d\S\+/ contained
-syn match Metadata /^\s\+;.*/ contained contains=MetadataTag
-syn match Comment /^;.*$/
+syn region ledgerTransaction start=/^[[:digit:]~]/ skip=/^\s/ end=/^/
+ \ fold keepend transparent contains=ledgerTransactionDate,ledgerMetadata,ledgerPosting
+syn match ledgerTransactionDate /^\d\S\+/ contained
+syn match ledgerPosting /^\s\+[^[:blank:];][^;]*\ze\%($\|;\)/
+ \ contained transparent contains=ledgerAccount,ledgerMetadata
" every space in an account name shall be surrounded by two non-spaces
" every account name ends with a tab, two spaces or the end of the line
-syn match Account /^\s\+\zs\%(\S \S\|\S\)\+\ze\%([ ]\{2,}\|\t\s*\|\s*$\)/ contained
-syn match Posting /^\s\+[^[:blank:];].*$/ contained transparent contains=Account,Amount
-" FIXME: add other symbols?
-let s:currency = '\([$€£¢]\|\w\+\)'
-let s:figures = '\d\+\([.,]\d\+\)*'
-let s:amount = '-\?\('.s:figures.'\s*'.s:currency.'\|'.s:currency.'\s*'.s:figures.'\)'
-exe 'syn match Amount /'.s:amount.'/ contained'
-syn match MetadataTag /:[^:]\+:/hs=s+1,he=e-1 contained
-syn match MetadataTag /;\s*\zs[^:]\+\ze:[^:]\+$/ contained
+syn match ledgerAccount /^\s\+\zs\%(\S \S\|\S\)\+\ze\%( \|\t\|\s*$\)/ contained
-syn region TagStack
- \ matchgroup=TagPush start=/^tag\>/
- \ matchgroup=TagPop end=/^pop\>/
- \ contains=TagHead,TagStack,transNorm
-syn match TagHead /\%(^tag\s\+\)\@<=\S.*$/ contained contains=tagKey transparent
-syn match TagKey /:[^:]\+:/hs=s+1,he=e-1 contained
-syn match TagKey /\%(^tag\s\+\)\@<=[^:]\+\ze:[^:]\+$/ contained
+syn match ledgerComment /^;.*$/
+" comments at eol must be preceeded by at least 2 spaces / 1 tab
+syn region ledgerMetadata start=/\%( \|\t\|^\s\+\);/ skip=/^\s\+;/ end=/^/
+ \ keepend contained contains=ledgerTag,ledgerTypedTag
+syn match ledgerTag /:[^:]\+:/hs=s+1,he=e-1 contained
+syn match ledgerTag /\%(\%(;\|^tag\)[^:]\+\)\@<=[^:]\+:\ze[^:]\+$/ contained
+syn match ledgerTypedTag /\%(\%(;\|^tag\)[^:]\+\)\@<=[^:]\+::\ze[^:]\+$/ contained
-highlight default link transDate Constant
-highlight default link Metadata Tag
-highlight default link MetadataTag Type
-highlight default link TagPop Tag
-highlight default link TagPush Tag
-highlight default link TagKey Type
-highlight default link Amount Number
-highlight default link Account Identifier
+syn region ledgerTagStack
+ \ matchgroup=ledgerTagPush start=/^tag\>/
+ \ matchgroup=ledgerTagPop end=/^pop\>/
+ \ contains=ledgerTagHead,ledgerTagStack,ledgerTransaction,ledgerComment
+syn match ledgerTagHead /\%(^tag\s\+\)\@<=\S.*$/ contained contains=ledgerTag transparent
+
+highlight default link ledgerTransactionDate Constant
+highlight default link ledgerMetadata Tag
+highlight default link ledgerTypedTag Keyword
+highlight default link ledgerTag Type
+highlight default link ledgerTagPop Tag
+highlight default link ledgerTagPush Tag
+highlight default link ledgerAccount Identifier
" syncinc is easy: search for the first transaction.
syn sync clear
-syn sync match ledgerSync grouphere transNorm "^[[:digit:]~]"
+syn sync match ledgerSync grouphere ledgerTransaction "^[[:digit:]~]"
let b:current_syntax = "ledger"