Merge remote branch 'kljohann/vim' into next

This commit is contained in:
John Wiegley 2010-11-09 09:14:57 -06:00
commit 460ad6ee76
3 changed files with 338 additions and 110 deletions

View file

@ -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'

View file

@ -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
endif
endfor
return alltags
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
" 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

View file

@ -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"