850 lines
29 KiB
VimL
850 lines
29 KiB
VimL
" File: autoload/todo.vim
|
||
" Description: Todo.txt sorting plugin
|
||
" Author: David Beniamine <david@beniamine.net>, Peter (fretep) <githib.5678@9ox.net>
|
||
" Licence: Vim licence
|
||
" Website: http://github.com/dbeniamine/todo.txt.vim
|
||
|
||
" These two variables are parameters for the successive calls the vim sort
|
||
" '' means no flags
|
||
" '! i' means reverse and ignore case
|
||
" for more information on flags, see :help sort
|
||
if (! exists("g:Todo_txt_first_level_sort_mode"))
|
||
let g:Todo_txt_first_level_sort_mode='i'
|
||
endif
|
||
if (! exists("g:Todo_txt_second_level_sort_mode"))
|
||
let g:Todo_txt_second_level_sort_mode='i'
|
||
endif
|
||
if (! exists("g:Todo_txt_third_level_sort_mode"))
|
||
let g:Todo_txt_third_level_sort_mode='i'
|
||
endif
|
||
|
||
|
||
" Functions {{{1
|
||
|
||
|
||
function! todo#GetCurpos()
|
||
if exists("*getcurpos")
|
||
return getcurpos()
|
||
endif
|
||
return getpos('.')
|
||
endfunction
|
||
|
||
function! todo#PrioritizeIncrease()
|
||
normal! 0f)h
|
||
endfunction
|
||
|
||
function! todo#PrioritizeDecrease()
|
||
normal! 0f)h
|
||
endfunction
|
||
|
||
function! todo#PrioritizeAdd (priority)
|
||
let oldpos=todo#GetCurpos()
|
||
let line=getline('.')
|
||
if line !~ '^([A-F])'
|
||
:call todo#PrioritizeAddAction(a:priority)
|
||
let oldpos[2]+=4
|
||
else
|
||
exec ':s/^([A-F])/('.a:priority.')/'
|
||
endif
|
||
call setpos('.',oldpos)
|
||
endfunction
|
||
|
||
function! todo#PrioritizeAddAction (priority)
|
||
execute "normal! mq0i(".a:priority.") \<esc>`q"
|
||
execute "delmarks q"
|
||
endfunction
|
||
|
||
function! todo#RemovePriority()
|
||
:s/^(\w)\s\+//ge
|
||
endfunction
|
||
|
||
function! todo#PrependDate()
|
||
if (getline(".") =~ '\v^\(')
|
||
execute "normal! 0f)a\<space>\<esc>l\"=strftime(\"%Y-%m-%d\")\<esc>P"
|
||
else
|
||
normal! I=strftime("%Y-%m-%d ")
|
||
endif
|
||
endfunction
|
||
|
||
function todo#SaveRegisters()
|
||
let s:last_search=@/
|
||
endfunction
|
||
|
||
function todo#RestoreRegisters()
|
||
let @/=s:last_search
|
||
endfunction
|
||
|
||
function! todo#ToggleMarkAsDone(status)
|
||
call todo#SaveRegisters()
|
||
if (getline(".") =~ '\C^x\s*\d\{4\}')
|
||
:call todo#UnMarkAsDone(a:status)
|
||
else
|
||
:call todo#MarkAsDone(a:status)
|
||
endif
|
||
call todo#RestoreRegisters()
|
||
endfunction
|
||
|
||
function! todo#FixFormat()
|
||
" Remove heading space
|
||
silent! %s/\C^\s*//
|
||
" Remove priority from done tasks
|
||
silent! %s/\C^x (\([A-Z]\)) \(.*\)/x \2 pri:\1/
|
||
endfunction
|
||
|
||
function! todo#UnMarkAsDone(status)
|
||
if a:status==''
|
||
let pat=''
|
||
else
|
||
let pat=' '.a:status
|
||
endif
|
||
exec ':s/\C^x\s*\d\{4}-\d\{1,2}-\d\{1,2}'.pat.'\s*//g'
|
||
silent s/\C\(.*\) pri:\([A-Z]\)/(\2) \1/e
|
||
endfunction
|
||
|
||
function! todo#MarkAsDone(status)
|
||
call todo#CreateNewRecurrence(1)
|
||
if get(g:, 'TodoTxtStripDoneItemPriority', 0)
|
||
exec ':s/\C^(\([A-Z]\))\(.*\)/\2/e'
|
||
else
|
||
exec ':s/\C^(\([A-Z]\))\(.*\)/\2 pri:\1/e'
|
||
endif
|
||
if a:status!=''
|
||
exec 'normal! I'.a:status.' '
|
||
endif
|
||
call todo#PrependDate()
|
||
if (getline(".") =~ '^ ')
|
||
normal! gIx
|
||
else
|
||
normal! Ix
|
||
endif
|
||
endfunction
|
||
|
||
function! todo#MarkAllAsDone()
|
||
:g!/^x /:call todo#MarkAsDone('')
|
||
endfunction
|
||
|
||
function! s:AppendToFile(file, lines)
|
||
let l:lines = []
|
||
|
||
" Place existing tasks in done.txt at the beggining of the list.
|
||
if filereadable(a:file)
|
||
call extend(l:lines, readfile(a:file))
|
||
endif
|
||
|
||
" Append new completed tasks to the list.
|
||
call extend(l:lines, a:lines)
|
||
|
||
" Write to file.
|
||
call writefile(l:lines, a:file)
|
||
endfunction
|
||
|
||
function! todo#RemoveCompleted()
|
||
" Check if we can write to done.txt before proceeding.
|
||
let l:target_dir = expand('%:p:h')
|
||
if exists("g:TodoTxtForceDoneName")
|
||
let l:done=g:TodoTxtForceDoneName
|
||
else
|
||
let l:done=substitute(substitute(expand('%:t'),'todo','done',''),'Todo','Done','')
|
||
endif
|
||
let l:done_file = l:target_dir.'/'.l:done
|
||
echo "Writing to ".l:done_file
|
||
if !filewritable(l:done_file) && !filewritable(l:target_dir)
|
||
echoerr "Can't write to file '".l:done_file."'"
|
||
return
|
||
endif
|
||
|
||
let l:completed = []
|
||
:g/^x /call add(l:completed, getline(line(".")))|d
|
||
call s:AppendToFile(l:done_file, l:completed)
|
||
endfunction
|
||
|
||
function! todo#Sort(type)
|
||
" vim :sort is usually stable
|
||
" we sort first on contexts, then on projects and then on priority
|
||
let g:Todo_fold_char='x'
|
||
let oldcursor=todo#GetCurpos()
|
||
if(a:type != "")
|
||
exec ':sort /.\{-}\ze'.a:type.'/'
|
||
elseif expand('%')=~'[Dd]one.*.txt'
|
||
" FIXME: Put some unit tests around this, and fix case sensitivity if ignorecase is set.
|
||
silent! %s/\(x\s*\d\{4}\)-\(\d\{2}\)-\(\d\{2}\)/\1\2\3/g
|
||
sort n /^x\s*/
|
||
silent! %s/\(x\s*\d\{4}\)\(\d\{2}\)/\1-\2-/g
|
||
else
|
||
silent normal gg
|
||
let l:first=search('^\s*x')
|
||
if l:first != 0
|
||
sort /^./r
|
||
" at this point done tasks are at the end
|
||
let l:first=search('^\s*x')
|
||
let l:last=search('^\s*x','b')
|
||
let l:diff=l:last-l:first+1
|
||
" Cut the done lines
|
||
silent execute ':'.l:first.'d a '.l:diff
|
||
endif
|
||
silent sort /@[a-zA-Z]*/ r
|
||
silent sort /+[a-zA-Z]*/ r
|
||
silent sort /\v\([A-Z]\)/ r
|
||
"Now tasks without priority are at beggining, move them to the end
|
||
silent normal gg
|
||
let l:firstP=search('^\s*([A-Z])', 'cn')
|
||
if l:firstP > 1
|
||
let num=l:firstP-1
|
||
" Sort normal
|
||
silent execute ':1 d b'.num
|
||
silent normal G"bp
|
||
endif
|
||
if l:first != 0
|
||
silent normal G"ap
|
||
silent execute ':'.l:first.','.l:last.'sort /@[a-zA-Z]*/ r'
|
||
silent execute ':'.l:first.','.l:last.'sort /+[a-zA-Z]*/ r'
|
||
silent execute ':'.l:first.','.l:last.'sort /\v([A-Z])/ r'
|
||
endif
|
||
endif
|
||
call setpos('.', oldcursor)
|
||
endfunction
|
||
|
||
function! todo#SortDue()
|
||
" Check how many lines have a due:date on them
|
||
let l:tasksWithDueDate = 0
|
||
silent! %global/\v\c<due:\d{4}-\d{2}-\d{2}>/let l:tasksWithDueDate += 1
|
||
if l:tasksWithDueDate == 0
|
||
" No tasks with a due:date: No need to modify the buffer at all
|
||
" Also means we don't need to cater for no matches on searches below
|
||
return
|
||
endif
|
||
" FIXME: There is a small chance that due:\d{8} might legitimately exist in the buffer
|
||
" We modify due:yyyy-mm-dd to yyyymmdd which would then mean we would alter the buffer
|
||
" in an unexpected way, altering user data. Not sure how to deal with this at the moment.
|
||
" I'm going to throw an exception, and if this is a problem we can revisit.
|
||
silent %global/\v\c<due:\d{8}>/throw "Text matching 'due:\\d\\{8\\}' exists in the buffer, this function cannot sort your buffer"
|
||
" Turn the due:date from due:yyyy-mm-dd to due:yyyymmdd so we can do a numeric sort
|
||
silent! %substitute/\v<(due:\d{4})\-(\d{2})\-(\d{2})>/\1\2\3/ei
|
||
" Sort all the lines with due: by numeric yyyymmdd, they will end up in ascending order at the bottom of the buffer
|
||
sort in /\v\c<due:\ze\d{8}>/
|
||
" Determine the line number of the first task with a due:date
|
||
let l:firstLineWithDue = line("$") - l:tasksWithDueDate + 1
|
||
" Put the sorted lines at the beginning of the file
|
||
if l:firstLineWithDue > 1
|
||
" ...but only if the whole file didn't get sorted.
|
||
execute "silent " . l:firstLineWithDue . ",$move 0"
|
||
endif
|
||
" Change the due:yyyymmdd back to due:yyyy-mm-dd.
|
||
silent! %substitute/\v<(due:\d{4})(\d{2})(\d{2})>/\1-\2-\3/ei
|
||
silent global/\C^x /move$
|
||
" Let's check a global for a user preference on the cursor position.
|
||
if exists("g:TodoTxtSortDueDateCursorPos")
|
||
if g:TodoTxtSortDueDateCursorPos ==? "top"
|
||
normal gg
|
||
elseif g:TodoTxtSortDueDateCursorPos ==? "lastdue" || g:TodoTxtSortDueDateCursorPos ==? "notoverdue"
|
||
silent normal G
|
||
" Sorry for the crazy RegExp. The next command should put cursor at at the top of the completed tasks,
|
||
" or the bottom of the buffer. This is done by searching backwards for any line not starting with
|
||
" "x " (x, space) which is important to distinguish from "xample task" for instance, which the more
|
||
" simple "^[^x]" would match. More info: ":help /\@!". Be sure to enforce case sensitivity on "x".
|
||
:silent! ?\v\C^(x )@!?+1
|
||
let l:overduePat = todo#GetDateRegexForPastDates()
|
||
let l:lastwrapscan = &wrapscan
|
||
set nowrapscan
|
||
try
|
||
if g:TodoTxtSortDueDateCursorPos ==? "lastdue"
|
||
" This searches backwards for the last due task
|
||
:?\v\c<due:\d{4}\-\d{2}\-\d{2}>
|
||
" Try a forward search in case the last line of the buffer was a due:date task, don't match done
|
||
" Be sure to enforce case sensitivity on "x" while allowing mixed case on "due:"
|
||
:silent! /\v\C^(x )@!&.*<[dD][uU][eE]:\d{4}\-\d{2}\-\d{2}>
|
||
elseif g:TodoTxtSortDueDateCursorPos ==? "notoverdue"
|
||
" This searches backwards for the last overdue task, and positions the cursor on the following line
|
||
execute ":?\\v\\c<due:" . l:overduePat . ">?+1"
|
||
endif
|
||
catch
|
||
" Might fail if there are no active (or overdue) due:date tasks. Requires nowrapscan
|
||
" This code path always means we want to be at the top of the buffer
|
||
normal gg
|
||
finally
|
||
let &wrapscan = l:lastwrapscan
|
||
endtry
|
||
elseif g:TodoTxtSortDueDateCursorPos ==? "bottom"
|
||
silent normal G
|
||
endif
|
||
else
|
||
" Default: Top of the document
|
||
normal gg
|
||
endif
|
||
" TODO: add time sorting (YYYY-MM-DD HH:MM)
|
||
endfunction
|
||
|
||
" This is a Hierarchical sort designed for todo.txt todo lists, however it
|
||
" might be used for other files types
|
||
" At the first level, lines are sorted by the word right after the first
|
||
" occurence of a:symbol, there must be no space between the symbol and the
|
||
" word. At the second level, the same kind of sort is done based on
|
||
" a:symbolsub, is a:symbol==' ', the second sort doesn't occurs
|
||
" Therefore, according to todo.txt syntaxt, if
|
||
" a:symbol is a '+' it sort by the first project
|
||
" a:symbol is an '@' it sort by the first context
|
||
" The last level of sort is done directly on the line, so according to
|
||
" todo.txt syntax, it means by priority. This sort is done if and only if the
|
||
" las argument is not 0
|
||
function! todo#HierarchicalSort(symbol, symbolsub, dolastsort)
|
||
if v:statusmsg =~ '--No lines in buffer--'
|
||
"Empty buffer do nothing
|
||
return
|
||
endif
|
||
let g:Todo_fold_char=a:symbol
|
||
"if the sort modes doesn't start by '!' it must start with a space
|
||
let l:sortmode=Todo_txt_InsertSpaceIfNeeded(g:Todo_txt_first_level_sort_mode)
|
||
let l:sortmodesub=Todo_txt_InsertSpaceIfNeeded(g:Todo_txt_second_level_sort_mode)
|
||
let l:sortmodefinal=Todo_txt_InsertSpaceIfNeeded(g:Todo_txt_third_level_sort_mode)
|
||
|
||
" Count the number of lines
|
||
let l:position= todo#GetCurpos()
|
||
execute "silent normal G"
|
||
let l:linecount=getpos(".")[1]
|
||
if(exists("g:Todo_txt_debug"))
|
||
echo "Linescount: ".l:linecount
|
||
endif
|
||
execute "silent normal gg"
|
||
|
||
" Get all the groups names
|
||
let l:groups=GetGroups(a:symbol,1,l:linecount)
|
||
if(exists("g:Todo_txt_debug"))
|
||
echo "Groups: "
|
||
echo l:groups
|
||
echo 'execute sort'.l:sortmode.' /.\{-}\ze'.a:symbol.'/'
|
||
endif
|
||
" Sort by groups
|
||
execute 'sort'.l:sortmode.' /.\{-}\ze'.a:symbol.'/'
|
||
for l:g in l:groups
|
||
let l:pat=a:symbol.l:g.'.*$'
|
||
if(exists("g:Todo_txt_debug"))
|
||
echo l:pat
|
||
endif
|
||
normal gg
|
||
" Find the beginning of the group
|
||
let l:groupBegin=search(l:pat,'c')
|
||
" Find the end of the group
|
||
let l:groupEnd=search(l:pat,'b')
|
||
|
||
" I'm too lazy to sort groups of one line
|
||
if(l:groupEnd==l:groupBegin)
|
||
continue
|
||
endif
|
||
if a:dolastsort
|
||
if( a:symbolsub!='')
|
||
" Sort by subgroups
|
||
let l:subgroups=GetGroups(a:symbolsub,l:groupBegin,l:groupEnd)
|
||
" Go before the first line of the group
|
||
" Sort the group using the second symbol
|
||
for l:sg in l:subgroups
|
||
normal gg
|
||
let l:pat=a:symbol.l:g.'.*'.a:symbolsub.l:sg.'.*$\|'.a:symbolsub.l:sg.'.*'.a:symbol.l:g.'.*$'
|
||
" Find the beginning of the subgroup
|
||
let l:subgroupBegin=search(l:pat,'c')
|
||
" Find the end of the subgroup
|
||
let l:subgroupEnd=search(l:pat,'b')
|
||
" Sort by priority
|
||
execute l:subgroupBegin.','.l:subgroupEnd.'sort'.l:sortmodefinal
|
||
endfor
|
||
else
|
||
" Sort by priority
|
||
if(exists("g:Todo_txt_debug"))
|
||
echo 'execute '.l:groupBegin.','.l:groupEnd.'sort'.l:sortmodefinal
|
||
endif
|
||
execute l:groupBegin.','.l:groupEnd.'sort'.l:sortmodefinal
|
||
endif
|
||
endif
|
||
endfor
|
||
" Restore the cursor position
|
||
call setpos('.', position)
|
||
endfunction
|
||
|
||
" Returns the list of groups starting by a:symbol between lines a:begin and
|
||
" a:end
|
||
function! GetGroups(symbol,begin, end)
|
||
let l:curline=a:begin
|
||
let l:groups=[]
|
||
while l:curline <= a:end
|
||
let l:curproj=strpart(matchstr(getline(l:curline),a:symbol.'\S*'),1)
|
||
if l:curproj != "" && index(l:groups,l:curproj) == -1
|
||
let l:groups=add(l:groups , l:curproj)
|
||
endif
|
||
let l:curline += 1
|
||
endwhile
|
||
return l:groups
|
||
endfunction
|
||
|
||
" Insert a space if needed (the first char isn't '!' or ' ') in front of
|
||
" sort parameters
|
||
function! Todo_txt_InsertSpaceIfNeeded(str)
|
||
let l:c=strpart(a:str,1,1)
|
||
if( l:c != '!' && l:c !=' ')
|
||
return " ".a:str
|
||
endif
|
||
retur a:str
|
||
endfunction
|
||
|
||
" function todo#CreateNewRecurrence {{{2
|
||
function! todo#CreateNewRecurrence(triggerOnNonStrict)
|
||
" Given a line with a rec:timespan, create a new task based off the
|
||
" recurrence and move the recurring tasks due:date to the next occurrence.
|
||
"
|
||
" This is implemented by a few other systems, so we will try to be as
|
||
" compatible as possible with the existing specifications.
|
||
"
|
||
" Other example implementations:
|
||
" <http://swiftodoapp.com/>
|
||
" <https://github.com/bram85/todo.txt-tools/wiki/Recurrence>
|
||
"
|
||
|
||
let l:currentline = getline('.')
|
||
|
||
" Don't operate on complete tasks
|
||
if l:currentline =~# '^x '
|
||
return
|
||
endif
|
||
|
||
let l:rec_date_rex = '\v\c(^|\s)rec:(\+)?(\d+)([dwmy])(\s|$)'
|
||
let l:rec_parts = matchlist(l:currentline, l:rec_date_rex)
|
||
" Don't operate on tasks without a valid "rec:" keyword.
|
||
if empty(l:rec_parts)
|
||
" If a "rec:" keyword exists, but it didn't match our expectations, warn
|
||
" the user, and abort whatever is happening otherwise a recurring task
|
||
" might be marked complete without a new recurrence being created.
|
||
if l:currentline =~? '\v\c(^|\s)rec:'
|
||
throw "Recurrence pattern is invalid. Aborting operation."
|
||
endif
|
||
return
|
||
endif
|
||
|
||
" Operations like postponing a task should not trigger the task to be
|
||
" duplicated, non-strict mode allows the changing of the due date.
|
||
let l:is_strict = l:rec_parts[2] ==# "+"
|
||
if ! a:triggerOnNonStrict && ! l:is_strict
|
||
return
|
||
endif
|
||
|
||
let l:units = str2nr(l:rec_parts[3])
|
||
if l:units < 1
|
||
let l:units = 1
|
||
endif
|
||
let l:unit_type = l:rec_parts[4]
|
||
" If we had a space on both sides of the "rec:" that we are removing, then
|
||
" we need to insert a space, otherwise, not.
|
||
if l:rec_parts[1] ==# ' ' && l:rec_parts[5] ==# ' '
|
||
let l:replace_string = ' '
|
||
else
|
||
let l:replace_string = ''
|
||
endif
|
||
|
||
" New task should have the rec: keyword stripped
|
||
let l:newline = substitute(l:currentline, l:rec_date_rex, l:replace_string, '')
|
||
" Insert above current line
|
||
let l:new_task_line_num = line('.')
|
||
if append(l:new_task_line_num - 1, l:newline) != 0
|
||
throw "Failed at append line"
|
||
endif
|
||
|
||
" At this point, we need to change the due date of the recurring task.
|
||
" Modes:
|
||
" Strict mode: From the existing due date
|
||
" Non-Strict mode: From the current date
|
||
" So, we don't need to do anything for strict mode. Non-strict mode requires
|
||
" setting the current date.
|
||
if l:is_strict
|
||
call todo#ChangeDueDate(l:units, l:unit_type, '')
|
||
else
|
||
call todo#ChangeDueDate(l:units, l:unit_type, strftime('%Y-%m-%d'))
|
||
endif
|
||
|
||
" Move onto the copied task
|
||
call cursor(l:new_task_line_num, col('.'))
|
||
if l:new_task_line_num != line('.')
|
||
throw "Failed to move cursor"
|
||
endif
|
||
endfunction
|
||
|
||
" function todo#ChangeDueDate {{{2
|
||
function! todo#ChangeDueDate(units, unit_type, from_reference)
|
||
" Change the due:date on the current line by a number of days, months or
|
||
" years
|
||
"
|
||
" units The number of unit_type to add or subtract, integer
|
||
" values only
|
||
" unit_type May be one of 'd' (days), 'm' (months) or 'y' (years),
|
||
" as handled by todo#DateAdd
|
||
" from_reference Allows passing a different date to base the calculation
|
||
" on, ignoring the existing due date in the line. Leave as
|
||
" an empty string to use the due:date in the line,
|
||
" otherwise a date as a string in the form "YYYY-MM-DD".
|
||
|
||
let l:currentline = getline('.')
|
||
|
||
" Don't operate on complete tasks
|
||
if l:currentline =~# '^x '
|
||
return
|
||
endif
|
||
|
||
let l:dueDateRex = '\v\c(^|\s)due:\zs\d{4}\-\d{2}\-\d{2}\ze(\s|$)'
|
||
|
||
let l:duedate = matchstr(l:currentline, l:dueDateRex)
|
||
if l:duedate ==# ''
|
||
" No due date on current line, then add the due date as an offset from
|
||
" current date. I.e. a v:count of 1 is due tomorrow, etc
|
||
if l:currentline =~? '\v\c(^|\s)due:'
|
||
" Has an invalid due: keyword, so don't add another, and don't
|
||
" change the line
|
||
return
|
||
endif
|
||
let l:duedate = strftime('%Y-%m-%d')
|
||
let l:currentline .= ' due:' . l:duedate
|
||
endif
|
||
" If a valid reference has been passed, let's use it.
|
||
if a:from_reference =~# '\v^\d{4}\-\d{2}\-\d{2}$'
|
||
let l:duedate = a:from_reference
|
||
endif
|
||
|
||
let l:duedate = todo#DateStringAdd(l:duedate, v:count1 * a:units, a:unit_type)
|
||
|
||
if setline('.', substitute(l:currentline, l:dueDateRex, l:duedate, '')) != 0
|
||
throw "Failed to set line"
|
||
endif
|
||
endfunction "}}}
|
||
|
||
" General date calculation functions {{{1
|
||
|
||
" function todo#GetDaysInMonth {{{2
|
||
function! todo#GetDaysInMonth(month, year)
|
||
" Given a month and year, returns the number of days in the month, taking
|
||
" leap years into consideration.
|
||
|
||
if index([1, 3, 5, 7, 8, 10, 12], a:month) >= 0
|
||
return 31
|
||
elseif index([4, 6, 9, 11], a:month) >= 0
|
||
return 30
|
||
else
|
||
" February, leap year fun.
|
||
if a:year % 4 != 0
|
||
return 28
|
||
elseif a:year % 100 != 0
|
||
return 29
|
||
elseif a:year % 400 != 0
|
||
return 28
|
||
else
|
||
return 29
|
||
endif
|
||
endif
|
||
endfunction
|
||
|
||
" function todo#DateAdd {{{2
|
||
function! todo#DateAdd(year, month, day, units, unit_type)
|
||
" Add or subtract days, months or years from a date
|
||
"
|
||
" Date must be passed in components of year, month and day, all integers
|
||
" units is the number of unit_type to add or subtract, integer values only
|
||
" unit_type may be one of:
|
||
" d days
|
||
" w weeks, 7 days
|
||
" m months, keeps the day of the month static except in the case
|
||
" that the day is the last day in the month or the day is higher
|
||
" than the number of days in the resultant month, where the result
|
||
" will stick to the end of the month. Examples:
|
||
" 2017-01-15 +1m 2017-02-15 +1m 2017-03-15 +1m 2017-04-15
|
||
" 2017-01-31 +1m 2017-02-28 +1m 2017-03-31 +1m 2017-04-30
|
||
" 2017-01-30 +1m 2017-02-28 +1m 2017-03-31
|
||
" 2017-01-30 +2m 2017-03-30
|
||
" y years, 12 months
|
||
|
||
|
||
" It is my understanding that VIM does not have date math functionality
|
||
" built in. Given we only have to deal with dates, and not times, it isn't
|
||
" all that scary to roll our own - we just need to watch out for leap years.
|
||
|
||
" Check and clean up input
|
||
if index(["d", "D", "w", "W", "m", "M", "y", "Y"], a:unit_type) < 0
|
||
throw 'Invalid unit "'. a:unit_type . '" passed to todo#DateAdd()'
|
||
endif
|
||
|
||
let l:d = str2nr(a:day)
|
||
let l:m = str2nr(a:month)
|
||
let l:y = str2nr(a:year)
|
||
let l:i = str2nr(a:units)
|
||
|
||
" Years can be handled simply as 12 x months, weeks as 7 x days
|
||
if a:unit_type ==? "y"
|
||
let l:utype = "m"
|
||
let l:i = l:i * 12
|
||
elseif a:unit_type ==? "w"
|
||
let l:utype = "d"
|
||
let l:i = l:i * 7
|
||
else
|
||
let l:utype = a:unit_type
|
||
endif
|
||
|
||
" Check and clean up input
|
||
if l:m < 1
|
||
if l:m == 0
|
||
let l:m = str2nr(strftime('%m'))
|
||
else
|
||
let l:m = 1
|
||
endif
|
||
endif
|
||
if l:m > 12
|
||
if l:i < 0 && l:utype ==? "m"
|
||
" Subtracting an invalid (high) month
|
||
" See comments for passing a high day below. Same reason for this.
|
||
let l:m = 13
|
||
else
|
||
let l:m = 12
|
||
endif
|
||
endif
|
||
if l:y < 1900 " See end of function for rationale
|
||
if l:y == 0
|
||
let l:y = str2nr(strftime('%Y'))
|
||
else
|
||
let l:y = 1900
|
||
endif
|
||
endif
|
||
|
||
" Grab number of days in the month specified
|
||
let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y)
|
||
|
||
" Check and clean up input
|
||
if l:d < 1
|
||
if l:d == 0
|
||
let l:d = str2nr(strftime('%d'))
|
||
else
|
||
let l:d = 1
|
||
endif
|
||
endif
|
||
" Allow passing a high day, this allows subtraction to be more sane when
|
||
" the day is out of bounds, i.e. 2017-04-80 should probably come out as
|
||
" 2017-04-30 not 2017-04-29. Addition deals with days being out of
|
||
" bounds (high) fine, and if days are untouched, out of bounds user
|
||
" input is caught at the end of the function.
|
||
" if l:d > l:daysInMonth
|
||
" let l:d = l:daysInMonth
|
||
" endif
|
||
|
||
if l:utype ==? "d"
|
||
" Adding DAYS
|
||
while l:i > 0
|
||
let l:d += 1
|
||
if l:d > l:daysInMonth
|
||
let l:d = 1
|
||
let l:m += 1
|
||
if l:m > 12
|
||
let l:m = 1
|
||
let l:y += 1
|
||
endif
|
||
let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y)
|
||
endif
|
||
let l:i -= 1
|
||
endwhile
|
||
" Subtracting DAYS
|
||
while l:i < 0
|
||
let l:d -= 1
|
||
if l:d < 1
|
||
let l:m -= 1
|
||
if l:m < 1
|
||
if l:y > 1900
|
||
let l:m = 12
|
||
let l:y -= 1
|
||
else
|
||
let l:d = 1
|
||
let l:m = 1
|
||
break
|
||
endif
|
||
endif
|
||
let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y)
|
||
let l:d = l:daysInMonth
|
||
endif
|
||
let l:i += 1
|
||
endwhile
|
||
elseif l:utype ==? "m"
|
||
if l:d >= l:daysInMonth
|
||
let l:wasLastDayOfMonth = 1
|
||
else
|
||
let l:wasLastDayOfMonth = 0
|
||
endif
|
||
" Adding MONTHS
|
||
while l:i > 0
|
||
let l:m += 1
|
||
if l:m > 12
|
||
let l:m = 1
|
||
let l:y += 1
|
||
endif
|
||
let l:i -= 1
|
||
endwhile
|
||
" Subtracting MONTHS
|
||
while l:i < 0
|
||
let l:m -= 1
|
||
if l:m < 1
|
||
if l:y > 1900
|
||
let l:m = 12
|
||
let l:y -= 1
|
||
else
|
||
let l:m = 1
|
||
endif
|
||
endif
|
||
let l:i += 1
|
||
endwhile
|
||
let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y)
|
||
if l:wasLastDayOfMonth
|
||
let l:d = l:daysInMonth
|
||
endif
|
||
endif
|
||
|
||
" Enforce some limits beyond which, I don't want to support.
|
||
if l:y < 1900
|
||
" Seeing as the date is going to be converted back to a string, dates
|
||
" less that 1000 are bound to cause bugs. Given this is an app for tasks
|
||
" you are doing in the here and now, I'm not supporting way back in the
|
||
" past.
|
||
let l:y = 1900
|
||
let l:daysInMonth = todo#GetDaysInMonth(l:m, l:y)
|
||
endif
|
||
" If we mess with the year (just above), or the user passes a day higher
|
||
" than the month, catch it here.
|
||
if l:d > l:daysInMonth
|
||
let l:d = l:daysInMonth
|
||
endif
|
||
return [l:y, l:m, l:d]
|
||
endfunction
|
||
|
||
" function todo#DateStringAdd {{{2
|
||
function! todo#DateStringAdd(date, units, unit_type)
|
||
" A very thin overload of todo#DateAdd() that takes and returns the date as
|
||
" a string rather than in [year, month, day] component form.
|
||
"
|
||
" Date must be passed in "YYYY-MM-DD" format, and is returned in this form
|
||
" also.
|
||
|
||
let [l:year, l:month, l:day] = todo#ParseDate(a:date)
|
||
let [l:year, l:month, l:day] = todo#DateAdd(l:year, l:month, l:day, a:units, a:unit_type)
|
||
let l:resulting_date = printf('%04d', l:year) . '-' . printf('%02d', l:month) . '-' . printf('%02d', l:day)
|
||
return l:resulting_date
|
||
endfunction
|
||
|
||
" function todo#ParseDate {{{2
|
||
function! todo#ParseDate(datestring)
|
||
" Given a date as a string in the format "YYYY-MM-DD", split the date into a
|
||
" list [year, month, day]
|
||
"
|
||
" Does not check if the date is valid other than being digits. Will throw an
|
||
" exception if the text does not match the expected date format.
|
||
|
||
if a:datestring !~? '\v^(\d{4})\-(\d{2})\-(\d{2})$'
|
||
throw "Invalid date passed '" . a:datestring . "'."
|
||
endif
|
||
let l:year = str2nr(strpart(a:datestring, 0, 4))
|
||
let l:month = str2nr(strpart(a:datestring, 5, 2))
|
||
let l:day = str2nr(strpart(a:datestring, 8, 2))
|
||
return [l:year, l:month, l:day]
|
||
endfunction "}}}
|
||
|
||
" Completion {{{1
|
||
|
||
" Simple keyword completion on all buffers {{{2
|
||
function! TodoKeywordComplete(base)
|
||
" Search for matches
|
||
let res = []
|
||
for bufnr in range(1,bufnr('$'))
|
||
let lines=getbufline(bufnr,1,"$")
|
||
for line in lines
|
||
if line =~ a:base
|
||
" init temporary item
|
||
let item={}
|
||
let item.word=substitute(line,'.*\('.a:base.'\S*\).*','\1',"")
|
||
call add(res,item)
|
||
endif
|
||
endfor
|
||
endfor
|
||
return res
|
||
endfunction
|
||
|
||
" Convert an item to the completion format and add it to the completion list
|
||
fun! TodoAddToCompletionList(list,item,opp)
|
||
" Create the definitive item
|
||
let resitem={}
|
||
let resitem.word=a:item.word
|
||
let resitem.info=a:opp=='+'?"Projects":"Contexts"
|
||
let resitem.info.=": ".join(a:item.related, ", ")
|
||
\."\nBuffers: ".join(a:item.buffers, ", ")
|
||
call add(a:list,resitem)
|
||
endfun
|
||
|
||
fun! TodoCopyTempItem(item)
|
||
let ret={}
|
||
let ret.word=a:item.word
|
||
if has_key(a:item, "related")
|
||
let ret.related=[a:item.related]
|
||
else
|
||
let ret.related=[]
|
||
endif
|
||
let ret.buffers=[a:item.buffers]
|
||
return ret
|
||
endfun
|
||
|
||
" Intelligent completion for projects and Contexts {{{2
|
||
fun! todo#Complete(findstart, base)
|
||
if a:findstart
|
||
let line = getline('.')
|
||
let start = col('.') - 1
|
||
while start > 0 && line[start - 1] !~ '\s'
|
||
let start -= 1
|
||
endwhile
|
||
return start
|
||
else
|
||
if a:base !~ '^+' && a:base !~ '^@'
|
||
return TodoKeywordComplete(a:base)
|
||
endif
|
||
" Opposite sign
|
||
let opp=a:base=~'+'?'@':'+'
|
||
" Search for matchs
|
||
let res = []
|
||
for bufnr in range(1,bufnr('$'))
|
||
let lines=getbufline(bufnr,1,"$")
|
||
for line in lines
|
||
if line =~ " ".a:base
|
||
" init temporary item
|
||
let item={}
|
||
let item.word=substitute(line,'.*\('.a:base.'\S*\).*','\1',"")
|
||
let item.buffers=bufname(bufnr)
|
||
if line =~ '.*\s\('.opp.'\S\S*\).*'
|
||
let item.related=substitute(line,'.*\s\('.opp.'\S\S*\).*','\1',"")
|
||
endif
|
||
call add(res,item)
|
||
endif
|
||
endfor
|
||
endfor
|
||
call sort(res)
|
||
" Here all results are sorted in res, but we need to merge them
|
||
let ret=[]
|
||
if res != []
|
||
let curitem=TodoCopyTempItem(res[0])
|
||
for it in res
|
||
if curitem.word==it.word
|
||
" Merge results
|
||
if has_key(it, "related") && index(curitem.related,it.related) <0
|
||
call add(curitem.related,it.related)
|
||
endif
|
||
if index(curitem.buffers,it.buffers) <0
|
||
call add(curitem.buffers,it.buffers)
|
||
endif
|
||
else
|
||
" Add to list
|
||
call TodoAddToCompletionList(ret,curitem,opp)
|
||
" Init new item from it
|
||
let curitem=TodoCopyTempItem(it)
|
||
endif
|
||
endfor
|
||
" Don't forget to add the list item
|
||
call TodoAddToCompletionList(ret,curitem,opp)
|
||
endif
|
||
return ret
|
||
endif
|
||
endfun
|
||
|
||
" vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab foldmethod=marker
|