"============================================================================= " What Is This: " File: metatodo.vim " Author: Vincent Berthoux " Last Change: 2009 june 28 " Version: 1.1 " " ChangeLog: " * 1.1.1:- Changed loading of files using globpath() " * 1.1 :- Taking into account "Documents and Settings" folder... " - Adding icons source from $VIM or $VIMRUNTIME " - Checking the nocompatible option (the only one required) " * 1.0 :- Original version if exists("g:__CUTETODO_VIM__") finish endif let g:__CUTETODO_VIM__ = 1 "====================================================================== " Configuration checking "====================================================================== if &compatible echom 'cuteTodoList require the nocompatible option, loading aborted' echom "To fix it add 'set nocompatible' in your .vimrc file" finish endif if has("win32") let s:ext = '.ico' else let s:ext = '.png' endif let s:path = globpath( &rtp, '/signs/priol1' . s:ext ) if s:path == '' echom "Cute Todo list can't find icons, plugin not loaded." finish endif "====================================================================== " General Options "====================================================================== if !exists("g:todo_list_buff_name") let g:todo_list_buff_name = 'Todo\ List' endif if !exists("g:todo_generate_auto") let g:todo_generate_auto = 1 endif if !exists("g:todo_list_filename") let g:todo_list_filename = '.todo.txt' endif if !exists("g:todo_list_globfilename") let g:todo_list_globfilename = '.global.todo.txt' endif if !exists("g:todo_global_path") let g:todo_global_path = expand('~') . '/' elseif g:todo_global_path =~ '[/\\]$' let g:todo_global_path = substitute( g:todo_global_path, '[/\\]\+$', '', '') endif "====================================================================== " Plugin data "====================================================================== " The todo list is stored with the following type (haskell " notation) : " DisplayName, Filename, Text :: String " Priority, SortKind :: Int " s:todo_list :: [(DisplayName, Filename, SortKind, [(Priority, Text)])] " But as viml doesn't provide tuples, they are implemented " as lists. let s:todo_list = [] " When only part of data is needed, prefer accessing it " using following indices, to prevent breaking if data structure " change. let s:todo_sort_index = 2 " Index of the SortKind thingy let s:todo_list_index = 3 " Index of the [(Prio, Text)] thingy let s:signId = 99000 let s:signCount = 0 let s:todoMinPrio = 0 let s:todoMaxPrio = 4 let s:default_priority = 2 let s:defaultSortKind = 0 exec 'sign define todopriop2 text=!! icon=' . escape( globpath( &rtp, 'signs/priop2' . s:ext ), ' \' ) exec 'sign define todopriop1 text=!1 icon=' . escape( globpath( &rtp, 'signs/priop1' . s:ext ), ' \' ) exec 'sign define todopriol1 text=_1 icon=' . escape( globpath( &rtp, 'signs/priol1' . s:ext ), ' \' ) exec 'sign define todopriol2 text=__ icon=' . escape( globpath( &rtp, 'signs/priol2' . s:ext ), ' \' ) let s:todo_inner_help_appeal = ['" Press for help' \,' ' \] let s:todo_inner_help_text = [ \ '" Help : to close' \,'" + : Increase task priority.' \,'" - : Decrease task priority.' \,'" ^ : Move the task up.' \,'" v : Move the task down.' \,'" m : Modify the task.' \,'" s : Sort the todos.' \,'" u : Update todo from buffers.' \,'" o : Jump to generated todo.' \,'" : Delete a task.' \,' ' \] "============================================================================= " Buffer support functions "============================================================================= fun! s:TodoRedraw( editLine ) "{{{ " Remove everything setl modifiable call s:TodoRemoveSign() " avoid overwriting user's buffers let tempz = getreg( 'z' ) normal ggVG"zd call setreg( 'z', tempz ) " re-render call s:TodoWriteFile( s:todo_list ) " Goto previous line in previous buffer... " faking a non-moving cursor execute 'normal ' . a:editLine . 'gg' " Hmm, resize test, not that good :-/ "execute 'resize ' . line( '$' ) call s:SaveTodoFile() endfunction "}}} fun! s:ComputeIndices() "{{{ let editLine = line( '.' ) let j = 0 if b:todo_inner_help == 0 " -1 for title " -1 for help line let linenb = editLine - 1 - len( s:todo_inner_help_appeal ) else let linenb = editLine - 1 - len( s:todo_inner_help_text ) endif for [disp, f, sortKind, todos] in s:todo_list if linenb <= len( todos ) let index = linenb - 1 return [editLine, j, index] endif let linenb = linenb - len( todos ) - 2 let j = j + 1 endfor return [editLine, -1, -1] endfunction "}}} " return -1 if no windows was open " >= 0 if cursor is now in the window fun! s:TodoGotoWin() "{{{ let bufnum = bufnr( g:todo_list_buff_name ) if bufnum >= 0 let win_num = bufwinnr( bufnum ) " We must get a real window number, or " else the buffer would have been deleted " already exe win_num . "wincmd w" return 0 endif return -1 endfunction "}}} " Remove all the priority mark on the side of the buffer. fun! s:TodoRemoveSign() "{{{ let s:signCount = s:signCount - 1 while s:signCount >= 0 let unplacer = 'sign unplace ' . (s:signCount + s:signId) exec unplacer let s:signCount = s:signCount - 1 endwhile endfunction "}}} " Save the local and global todos. fun! s:SaveTodoFile() "{{{ for [disp, filename, sortKind, todoList] in s:todo_list " The generated todolist has no filename. if filename == '' return endif let toWrite = [] for [prio,line] in todoList call add( toWrite, prio . ':' . line ) endfor call writefile( toWrite , filename ) endfor endfunction "}}} let s:classNames = ['todopriol2', 'todopriol1', '', 'todopriop1', 'todopriop2'] fun! s:AttribSign( buffId, prio, line) "{{{ let prioClass = get( s:classNames, a:prio ) if prioClass != '' let id = s:signId + s:signCount let s:signCount = s:signCount + 1 let toPlace = 'sign place ' . id \ . ' line=' . a:line \ . ' name=' . prioClass \ . ' buffer=' . a:buffId execute toPlace endif endfunction "}}} fun! s:TodoWriteFile( todol ) "{{{ let i = 0 " Prefixes used to highlight differently one task on two let prefixes = [' ', "\t"] let buffId = bufnr( '%' ) setlocal modifiable if b:todo_inner_help == 0 call append( i, s:todo_inner_help_appeal ) let i = i + len( s:todo_inner_help_appeal ) else call append( i, s:todo_inner_help_text ) let i = i + len( s:todo_inner_help_text ) endif for [disp, file, sortKind, todos] in a:todol let partBegin = i call append( i, '==== ' . disp ) let i = i + 1 for [prio,text] in todos call append( i, prefixes[i % 2] . text ) let i = i + 1 call s:AttribSign( buffId, prio, i ) endfor " We create a fold manually here if partBegin > 0 let partBegin = partBegin + 1 endif exec partBegin . ',' . i . 'fold' call append( i, ' ' ) let i = i + 1 endfor " Remove last two lines, and avoid overwritin " user's registers let tempz = getreg( 'z' ) normal G"zdd"zdd call setreg( 'z', tempz ) " Open all the folds normal zR setlocal nomodifiable endfunction "}}} fun! s:TodoParseTodo(txt) "{{{ if a:txt =~ '^\d:.*' let prio = substitute( a:txt, '^\(\d\):.*', '\1', '' ) let text = substitute( a:txt, '^\d:\(.*\)', '\1', '' ) return [prio, text] elseif a:txt =~ '^\d\+\s\+.*' let prio = substitute( a:txt, '^\(\d\+\).*', '\1', '' ) let text = substitute( a:txt, '^\d\+\s\+\(.*\)', '\1', '' ) return [prio, ' ' . text] else return [s:default_priority, a:txt] endif endfunction "}}} " Used by TodoGatherInFiles to store result in a list :] fun! s:TodoGathering() "{{{ let text = substitute( getline('.'), '.*TODO\s\+\(.*\)', '\1', '' ) let [prio, txt] = s:TodoParseTodo( text ) call add( s:spareTodos, [prio, expand('%') . '|' . line('.') . '|' .txt ] ) endfunction "}}} fun! s:TodoGatherInFiles() "{{{ if !g:todo_generate_auto return endif let curBuf = bufnr( '%' ) " avoid buffer deletion during bufdo setlocal bufhidden=hide " Ok we remove all we have, to avoid " getting a feedback loop and getting todos " from the todo window... setlocal modifiable let tempz = getreg( 'z' ) normal ggVG"zd call setreg( 'z', tempz ) let s:spareTodos = [] " Big regexp to catch TODO and avoid it in words bufdo 1,$g/^TODO\|\ATODO\A\|\ATODO$/call s:TodoGathering() execute 'buffer ' . curBuf setlocal bufhidden=wipe if len(s:todo_list) < 3 call add( s:todo_list, ['Generated', '', s:defaultSortKind, s:spareTodos]) else let s:todo_list[2] = ['Generated', '', s:defaultSortKind, s:spareTodos] endif endfunction "}}} fun! LoadTodoFile( displayName, fileName ) "{{{ if !filereadable( a:fileName ) return [a:displayName, a:fileName, s:defaultSortKind, []] endif let retLst = [] for line in readfile( a:fileName ) call add( retLst, s:TodoParseTodo( line ) ) endfor return [a:displayName, a:fileName, s:defaultSortKind, retLst] endfunction "}}} "============================================================================= " Buffer definition "============================================================================= " Set all the option for the todo buffer, avoid " modification and put hooks to manage it. function! s:PrepareTodoBuffer() set ft=NONE mapclear setlocal buftype=nofile " make sure buffer is deleted when view is closed setlocal bufhidden=wipe setlocal noswapfile setlocal nobuflisted setlocal nonumber setlocal linebreak setlocal foldcolumn=1 setlocal foldmethod=manual setlocal noexpandtab setlocal softtabstop setlocal tabstop=1 exe 'file ' . g:todo_list_buff_name setlocal statusline=%F nnoremap + :call TodoAddToPriority(1) nnoremap - :call TodoAddToPriority(-1) nnoremap ^ :call TodoSwap(-1) nnoremap v :call TodoSwap(1) nnoremap s :call TodoSort() nnoremap m :call TodoModify() nnoremap u :call UpdateTodos() nnoremap o :call TodoJumpTo() nnoremap :call TodoRemoveTask() nnoremap :call TodoToggleHelp() syntax match Title /^===.*/ syntax match DiffChange /^\t.*/ syntax match Comment /^".*/ let b:todo_inner_help = 0 endfunction fun! s:TodoToggleHelp() "{{{ if b:todo_inner_help != 0 let b:todo_inner_help = 0 else let b:todo_inner_help = 1 endif let editLine = line('.') call s:TodoRedraw( editLine ) endfunction "}}} "============================================================================= " Sorting functions "============================================================================= fun! s:TodoCompareByPrio(obj1, obj2) "{{{ let [i1, ignoredText1] = a:obj1 let [i2, ignoredText2] = a:obj2 return i1 == i2 ? 0 : i1 > i2 ? 1 : -1 endfunction "}}} fun! s:TodoCompareByInvPrio(o1, o2) "{{{ return - s:TodoCompareByPrio(a:o1, a:o2) endfunction "}}} fun! s:TodoCompareByText(obj1, obj2) "{{{ let [ignoredPrio1, i1] = a:obj1 let [ignoredPrio2, i2] = a:obj2 return i1 == i2 ? 0 : i1 > i2 ? 1 : -1 endfunction "}}} fun! s:TodoCompareByInvText(o1, o2) "{{{ return - s:TodoCompareByText( a:o1, a:o2 ) endfunction "}}} let s:sortFunctions = ["TodoCompareByPrio" \,"TodoCompareByInvPrio" \,"TodoCompareByText" \,"TodoCompareByInvText" \] fun! s:TodoSort() "{{{ let [editLine, fileNumber, index] = s:ComputeIndices() if fileNumber != -1 let sortIdx = (s:todo_list[fileNumber][s:todo_sort_index] + 1) % len( s:sortFunctions ) call sort( s:todo_list[fileNumber][s:todo_list_index], s:sortFunctions[sortIdx] ) let s:todo_list[fileNumber][s:todo_sort_index] = sortIdx call s:TodoRedraw( editLine ) endif endfunction "}}} "============================================================================= " Buffer actions "============================================================================= " Function called to trigger the todo gathering in files fun! s:UpdateTodos() "{{{ let editLine = line('.') let s:spareTodos = [] call s:TodoLoadFiles() call s:TodoGatherInFiles() call s:PrepareTodoBuffer() call s:TodoRedraw( editLine ) endfunction "}}} fun! s:TodoRemoveTask() "{{{ let [editLine, fileNumber, index] = s:ComputeIndices() if fileNumber != -1 let todos = s:todo_list[fileNumber][s:todo_list_index] call remove( todos, index ) call s:TodoRedraw( editLine ) endif endfunction "}}} " Swap two tasks, to simulate a move up or move down " in function of the a:swapRel variable. fun! s:TodoSwap(swapRel) "{{{ let [editLine, fileNumber, index] = s:ComputeIndices() if fileNumber != -1 let todos = s:todo_list[fileNumber][s:todo_list_index] let atIndex = get( todos, index ) " Avoid swapping out of bounds todos if (a:swapRel < 0 && index == 0) || (a:swapRel > 0 && index == len(todos) - 1) return endif call remove( todos, index ) call insert( todos, atIndex, index + a:swapRel ) if a:swapRel > 0 let editLine = editLine + 1 elseif a:swapRel < 0 let editLine = editLine - 1 endif call s:TodoRedraw( editLine ) endif endfunction "}}} " Add count to the priority fun! s:TodoAddToPriority( count ) "{{{ let [editLine, fileNumber, index] = s:ComputeIndices() if fileNumber != -1 let todos = s:todo_list[fileNumber][s:todo_list_index] let [prio, text] = todos[index] let newprio = prio + a:count if newprio < s:todoMinPrio || newprio > s:todoMaxPrio return endif call insert( todos, [newprio, text], index ) call remove( todos, index + 1 ) call s:TodoRedraw( editLine ) endif endfunction "}}} fun! s:TodoModify() "{{{ let [editLine, fileNumber, index] = s:ComputeIndices() if fileNumber != -1 let [dispName, filename, sortKind, todos] = s:todo_list[fileNumber] " Can't modify the generated if filename == '' return endif let newText = input( 'Todo> ', todos[index][1] ) let todos[index][1] = newText call s:TodoRedraw( editLine ) endif endfunction "}}} fun! s:TodoJumpTo() "{{{ let [editLine, fileNumber, index] = s:ComputeIndices() if fileNumber != 2 return endif let txt = s:todo_list[fileNumber][s:todo_list_index][index][1] let filename = substitute( txt, '^\([^|]\+\).*', '\1', '' ) let lineNum = substitute( txt, '^[^|]\+|\(\d\+\).*', '\1', '') let bufnum = bufnr( filename ) if bufnum >= 0 let win_num = bufwinnr( bufnum ) " If it's already shown if win_num > 0 " We must get a real window number, or " else the buffer would have been deleted " already exe win_num . "wincmd w" else " Heuristic... go left and change the buffer... wincmd l execute 'buffer ' . bufnum endif exe 'normal ' . lineNum . 'gg' endif endfunction "}}} "============================================================================= " Ex Commands implementation "============================================================================= fun! TodoCloseWindow() "{{{ let last_buffer = bufnr("%") if s:TodoGotoWin() >= 0 close endif let win_num = bufwinnr( last_buffer ) " We must get a real window number, or " else the buffer would have been deleted " already exe win_num . "wincmd w" endfunction "}}} fun! TodoWindowOpen() "{{{ " Ok, previous buffer to jump to it at final let last_buffer = bufnr("%") if s:TodoGotoWin() < 0 new call s:PrepareTodoBuffer() setl modifiable else call s:TodoRemoveSign() setl modifiable normal ggVGd endif call s:TodoWriteFile( s:todo_list ) let win_num = bufwinnr( last_buffer ) " We must get a real window number, or " else the buffer would have been deleted " already exe win_num . "wincmd w" endfunction "}}} fun! TodoAdd(level) "{{{ let text = input( 'Todo> ' ) let prio = s:default_priority " If the text begin by digits if text =~ '^\d\+\s\+.*' " then extract the said digit and transform it to int let mayPrio = substitute(text,'^\(\d\+\)\s\+.*','\1', '') + 0 " If it's in priority bounds... if s:todoMinPrio <= mayPrio && mayPrio <= s:todoMaxPrio " Take it as task priority... let text = substitute( text, '^\d\+\s\+\(.*\)', '\1', '' ) let prio = mayPrio endif endif " Now add it to the good level... (local or global) for [disp, f, sortKind, todos] in s:todo_list if disp == a:level call add( todos, [prio, text] ) endif endfor call TodoWindowOpen() call s:SaveTodoFile() endfunction "}}} fun! s:TodoLoadFiles() "{{{ let currDir = expand( '$PWD' ) let globDir = g:todo_global_path let s:todo_list = [ LoadTodoFile( 'Global', globDir . '/' . g:todo_list_globfilename ) \ , LoadTodoFile( 'Local', g:todo_list_filename ) \ ] endfunction "}}} command! TodoClose call TodoCloseWindow() command! TodoOpen call TodoWindowOpen() command! Todo call TodoAdd( 'Local' ) command! Todog call TodoAdd( 'Global' ) let s:spareTodos = [] call s:TodoLoadFiles()