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> command -nargs=* CompilerSet setlocal <args>
endif endif
if ! exists("g:ledger_bin") || ! executable(g:ledger_bin) " default value will be set in ftplugin
if executable('ledger') if ! exists("g:ledger_bin") || empty(g:ledger_bin) || ! executable(split(g:ledger_bin, '\s')[0])
let g:ledger_bin = 'ledger' finish
else
echoerr "ledger command not found. Set g:ledger_bin or extend $PATH."
finish
endif
endif endif
" %-G throws away blank lines, everything else is assumed to be part of a " %-G throws away blank lines, everything else is assumed to be part of a
" multi-line error message. " multi-line error message.
CompilerSet errorformat=%-G,%EWhile\ parsing\ file\ \"%f\"\\,\ line\ %l:%.%#,%ZError:\ %m,%C%.%# 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, " unfortunately there is no 'check file' command,
" so we will just use a query that returns no results. ever. " 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 ". let b:undo_ftplugin = "setlocal ".
\ "foldmethod< foldtext< ". \ "foldmethod< foldtext< ".
\ "include< comments< omnifunc< " \ "include< comments< omnifunc< formatprg<"
" don't fill fold lines --> cleaner look " don't fill fold lines --> cleaner look
setl fillchars="fold: " setl fillchars="fold: "
@ -22,6 +22,21 @@ setl include=^!include
setl comments=b:; setl comments=b:;
setl omnifunc=LedgerComplete 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) " You can set a maximal number of columns the fold text (excluding amount)
" will use by overriding g:ledger_maxwidth in your .vimrc. " will use by overriding g:ledger_maxwidth in your .vimrc.
" When maxwidth is zero, the amount will be displayed at the far right side " When maxwidth is zero, the amount will be displayed at the far right side
@ -92,7 +107,7 @@ function! LedgerFoldText() "{{{1
\ '\(^\s\+\|\s\+$\)', '', 'g') \ '\(^\s\+\|\s\+$\)', '', 'g')
" number of columns foldtext can use " number of columns foldtext can use
let columns = s:get_columns(0) let columns = s:get_columns()
if g:ledger_maxwidth if g:ledger_maxwidth
let columns = min([columns, g:ledger_maxwidth]) let columns = min([columns, g:ledger_maxwidth])
endif endif
@ -107,9 +122,9 @@ function! LedgerFoldText() "{{{1
let foldtext .= repeat(' ', filen - (folen%filen)) let foldtext .= repeat(' ', filen - (folen%filen))
let foldtext .= repeat(g:ledger_fillstring, let foldtext .= repeat(g:ledger_fillstring,
\ s:get_columns(0)/filen) \ s:get_columns()/filen)
else else
let foldtext .= repeat(' ', s:get_columns(0)) let foldtext .= repeat(' ', s:get_columns())
endif endif
" we don't use slices[:5], because that messes up multibyte characters " 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 lnum = line('.')
let line = getline('.') let line = getline('.')
let lastcol = col('.') - 2 let lastcol = col('.') - 2
if line =~ '^\d' "{{{2 (date / payee / description) if line =~ '^\s\+[^[:blank:];]' "{{{2 (account)
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)
let b:compl_context = 'account' let b:compl_context = 'account'
if matchend(line, '^\s\+\%(\S \S\|\S\)\+') <= lastcol if matchend(line, '^\s\+\%(\S \S\|\S\)\+') <= lastcol
" only allow completion when in or at end of account name " 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) let results = LedgerFindInTree(LedgerGetAccountHierarchy(), hierarchy)
" sort by alphabet and reverse because it will get reversed one more time " sort by alphabet and reverse because it will get reversed one more time
let results = reverse(sort(results))
if g:ledger_detailed_first 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 endif
call add(results, a:base) call insert(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)))
return results return results
else "}}} else "}}}
unlet! b:compl_context unlet! b:compl_context
@ -228,52 +209,306 @@ function! LedgerGetAccountHierarchy() "{{{1
return hierarchy return hierarchy
endf "}}} endf "}}}
function! LedgerGetTags() "{{{1 function! LedgerToggleTransactionState(lnum, ...)
let alltags = {} if a:0 == 1
let metalines = s:grep_buffer('^\s\+;\s*\zs.*$') let chars = a:1
for line in metalines else
" (spaces at beginning are stripped by matchstr!) let chars = ' *'
if line[0] == ':' endif
" multiple tags let trans = s:transaction.from_lnum(a:lnum)
for val in split(line, ':') if empty(trans)
if val !~ '^\s*$' return
let name = s:strip_spaces(val) endif
let alltags[name] = get(alltags, name, [])
endif let old = has_key(trans, 'state') ? trans['state'] : ' '
endfor let i = stridx(chars, old) + 1
elseif line =~ '^.*:.*$' let new = chars[i >= len(chars) ? 0 : i]
" line with tag=value
let name = s:strip_spaces(split(line, ':')[0]) call trans.set_state(new)
let val = s:strip_spaces(join(split(line, ':')[1:], ':'))
let values = get(alltags, name, []) call setline(trans['head'], trans.format_head())
call add(values, val) endf
let alltags[name] = values
endif function! LedgerSetTransactionState(lnum, char) "{{{1
endfor " modifies or sets the state of the transaction at the cursor,
return alltags " 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 "}}} 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 " 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 " return length of string with fix for multibyte characters
function! s:multibyte_strlen(text) "{{{2 function! s:multibyte_strlen(text) "{{{2
return strlen(substitute(a:text, ".", "x", "g")) return strlen(substitute(a:text, ".", "x", "g"))
endfunction "}}} endfunction "}}}
" get # of visible/usable columns in current window " 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, " As long as vim doesn't provide a command natively,
" we have to compute the available columns. " we have to compute the available columns.
" see :help todo.txt -> /Add argument to winwidth()/ " 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(0) == 0 ? 80 : winwidth(0)) - &foldcolumn
let columns = (winwidth(a:win) == 0 ? 80 : winwidth(a:win)) - &foldcolumn
if &number if &number
" line('w$') is the line number of the last line " line('w$') is the line number of the last line
let columns -= max([len(line('w$'))+1, &numberwidth]) let columns -= max([len(line('w$'))+1, &numberwidth])
endif 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 return columns
endfunction "}}} endf "}}}
" remove spaces at start and end of string " remove spaces at start and end of string
function! s:strip_spaces(text) "{{{2 function! s:strip_spaces(text) "{{{2

View file

@ -28,43 +28,39 @@ syntax clear
" DATE[=EDATE] [*|!] [(CODE)] DESC <-- first line of transaction " DATE[=EDATE] [*|!] [(CODE)] DESC <-- first line of transaction
" ACCOUNT AMOUNT [; NOTE] <-- posting " ACCOUNT AMOUNT [; NOTE] <-- posting
" region: a transaction containing postings syn region ledgerTransaction start=/^[[:digit:]~]/ skip=/^\s/ end=/^/
syn region transNorm start=/^[[:digit:]~]/ skip=/^\s/ end=/^/ \ fold keepend transparent contains=ledgerTransactionDate,ledgerMetadata,ledgerPosting
\ fold keepend transparent contains=transDate,Metadata,Posting syn match ledgerTransactionDate /^\d\S\+/ contained
syn match transDate /^\d\S\+/ contained syn match ledgerPosting /^\s\+[^[:blank:];][^;]*\ze\%($\|;\)/
syn match Metadata /^\s\+;.*/ contained contains=MetadataTag \ contained transparent contains=ledgerAccount,ledgerMetadata
syn match Comment /^;.*$/
" every space in an account name shall be surrounded by two non-spaces " 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 " 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 ledgerAccount /^\s\+\zs\%(\S \S\|\S\)\+\ze\%( \|\t\|\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 region TagStack syn match ledgerComment /^;.*$/
\ matchgroup=TagPush start=/^tag\>/ " comments at eol must be preceeded by at least 2 spaces / 1 tab
\ matchgroup=TagPop end=/^pop\>/ syn region ledgerMetadata start=/\%( \|\t\|^\s\+\);/ skip=/^\s\+;/ end=/^/
\ contains=TagHead,TagStack,transNorm \ keepend contained contains=ledgerTag,ledgerTypedTag
syn match TagHead /\%(^tag\s\+\)\@<=\S.*$/ contained contains=tagKey transparent syn match ledgerTag /:[^:]\+:/hs=s+1,he=e-1 contained
syn match TagKey /:[^:]\+:/hs=s+1,he=e-1 contained syn match ledgerTag /\%(\%(;\|^tag\)[^:]\+\)\@<=[^:]\+:\ze[^:]\+$/ contained
syn match TagKey /\%(^tag\s\+\)\@<=[^:]\+\ze:[^:]\+$/ contained syn match ledgerTypedTag /\%(\%(;\|^tag\)[^:]\+\)\@<=[^:]\+::\ze[^:]\+$/ contained
highlight default link transDate Constant syn region ledgerTagStack
highlight default link Metadata Tag \ matchgroup=ledgerTagPush start=/^tag\>/
highlight default link MetadataTag Type \ matchgroup=ledgerTagPop end=/^pop\>/
highlight default link TagPop Tag \ contains=ledgerTagHead,ledgerTagStack,ledgerTransaction,ledgerComment
highlight default link TagPush Tag syn match ledgerTagHead /\%(^tag\s\+\)\@<=\S.*$/ contained contains=ledgerTag transparent
highlight default link TagKey Type
highlight default link Amount Number highlight default link ledgerTransactionDate Constant
highlight default link Account Identifier 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. " syncinc is easy: search for the first transaction.
syn sync clear syn sync clear
syn sync match ledgerSync grouphere transNorm "^[[:digit:]~]" syn sync match ledgerSync grouphere ledgerTransaction "^[[:digit:]~]"
let b:current_syntax = "ledger" let b:current_syntax = "ledger"