Thursday, October 8, 2009

Nested Lists in Vim

A while back, I worked on a project called Simplist. It was a little web app for creating, managing and sharing infinitely nestable lists.

It was really handy, and I was very proud of myself, until I realized that this is something you can do in Notepad with little effort and in vim with even less.

So I created a short script to duplicate Simplist's basic features in vim:



Notice that some items are marked as completed ("x") and some have priorities (numbers). The last item is actually a list itself with sub-items and is folded.

With a few simple commands, it's pretty handy:

,n - create a [n]ew list item
,x - mark item as done ("x")
,p - mark item as in [p]rogress ("=")
,- - mark item as undone ("-")
,N - mark item with priority N where N is 1-5
,t - update item's [t]imestamp

If you combine these with a few vim keys:

>> - indent line (make it a sub-item of item above)
<< - outdent line (make it a super-item)
zo - open a folded list
zc - fold an open list

you get a pretty decent way to manage lists. I use this all the time--every day at work and for personal lists.

Here's the vim script. Just paste it into your .vimrc:

if version >= 700
autocmd BufNewFile,BufRead *.list call ListFile()
autocmd TabEnter *.list call ListFile()

" 'install' list features
function ListFile()
setlocal foldmethod=expr
setlocal foldexpr=ListFoldLevel(v:lnum)
setlocal shiftwidth=4
setlocal tabstop=4
setlocal foldtext=ListFoldLine(v:foldstart)
setlocal noshowmatch
setlocal cindent
" add [n]ew item below current
map ,n o- =ListTimestamp()^la
" mark item as [x]
map ,x mz^rxf[hdf]$a=ListTimestamp()`z
" mark item as [-]
map ,- mz^r-f[hdf]$a=ListTimestamp()`z
" mark item as = (in [p]rogress)
map ,p mz^r=f[hdf]$a=ListTimestamp()`z
" mark item with a rank
map ,1 mz^r1f[hdf]$a=ListTimestamp()`z
map ,2 mz^r2f[hdf]$a=ListTimestamp()`z
map ,3 mz^r3f[hdf]$a=ListTimestamp()`z
map ,4 mz^r4f[hdf]$a=ListTimestamp()`z
map ,5 mz^r5f[hdf]$a=ListTimestamp()`z
" add/update [t]imestamp
map ,t mz$a [^f[hd$a=ListTimestamp()`z
endfunction

" return properly formatted timestamp
function ListTimestamp()
return ' ['.strftime('%Y-%m-%d %T').']'
endfunction

" return fold line format
function ListFoldLine(linenum)
let s:count = 1
let s:spaces = ''
while s:count <= &shiftwidth
let s:spaces = s:spaces.' '
let s:count = s:count + 1
endwhile
return substitute(getline(a:linenum),"\t",s:spaces,'g')
endfunction

" foldexpr function
function ListFoldLevel(linenum)
let s:prefix = ''
let s:myline = getline(a:linenum)
let s:nextline = getline(a:linenum+1)
let s:mynumtabs = match(s:myline,"[^\t]",0)
let s:nextnumtabs = match(s:nextline,"[^\t]",0)
if s:nextnumtabs > s:mynumtabs " if this item has sub-items
let s:level = s:nextnumtabs
else " next item is either same or higher level
let s:level = s:mynumtabs
if s:nextnumtabs < s:mynumtabs " if next item has higher level, close this fold
let s:prefix = '<'
let s:level = s:nextnumtabs+1
end
endif
if a:linenum > 1
s:pline = getline(a:linenum-1)
s:pnumtabs = match(s:pline,"[^\t]",0)
if s:level < s:pnumtabs
" if this is higher level than prev, start a new fold
let s:prefix = '>'
endif
endif
return s:prefix.s:level
endfunction
endif