diff --git a/README.markdown b/README.markdown index 2bae991..0f48e19 100644 --- a/README.markdown +++ b/README.markdown @@ -209,6 +209,26 @@ Defaults values are: For more information on the available flags see `help :sort` +## Recurrence + +By adding a `rec:` tag to your task, when you complete (`x`) or +postpone (`p`) the task, a new recurrence will be created due after +the specified amount of time. + +The format is: + `rec:[+][count][d|w|m|y]` + +Where: + d = days, w = weeks, m = months, y = years + The optional `+` specifies strict recurrence (see below) + +Examples: + * `rec:2w` - Recurs two weeks after the task is completed. + * `rec:3d` - Recurs three days after the task is completed. + * `rec:+1w` - Recurs one week from the due date (strict) + +This is a non-standard but widely adopted keyword. + ## Mappings By default todo-txt.vim sets all the mappings described in this section. To diff --git a/autoload/todo.vim b/autoload/todo.vim index c3b26da..69be5db 100644 --- a/autoload/todo.vim +++ b/autoload/todo.vim @@ -95,6 +95,7 @@ function! todo#UnMarkAsDone(status) endfunction function! todo#MarkAsDone(status) + call todo#CreateNewRecurrence(1) exec ':s/\C^(\([A-Z]\))\(.*\)/\2 pri:\1/e' if a:status!='' exec 'normal! I'.a:status.' ' @@ -359,14 +360,99 @@ function! Todo_txt_InsertSpaceIfNeeded(str) 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: + " + " + " + + 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) +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 is 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 + " 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('.') @@ -378,7 +464,7 @@ function! todo#ChangeDueDate(units, unit_type) 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 == '' + 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:' @@ -389,6 +475,10 @@ function! todo#ChangeDueDate(units, unit_type) 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) @@ -430,6 +520,7 @@ function! todo#DateAdd(year, month, day, units, unit_type) " 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 @@ -438,7 +529,7 @@ function! todo#DateAdd(year, month, day, units, unit_type) " 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 + " y years, 12 months " It is my understanding that VIM does not have date math functionality @@ -446,7 +537,7 @@ function! todo#DateAdd(year, month, day, units, unit_type) " all that scary to roll our own - we just need to watch out for leap years. " Check and clean up input - if index(["d", "m", "y"], a:unit_type) < 0 + 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 @@ -455,10 +546,13 @@ function! todo#DateAdd(year, month, day, units, unit_type) let l:y = str2nr(a:year) let l:i = str2nr(a:units) - " Years can be handled simply as 12 x months - if a:unit_type == "y" + " 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 @@ -472,7 +566,7 @@ function! todo#DateAdd(year, month, day, units, unit_type) endif endif if l:m > 12 - if l:i < 0 && l:utype == "m" + 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 @@ -508,7 +602,7 @@ function! todo#DateAdd(year, month, day, units, unit_type) " let l:d = l:daysInMonth " endif - if l:utype == "d" + if l:utype ==? "d" " Adding DAYS while l:i > 0 let l:d += 1 @@ -543,7 +637,7 @@ function! todo#DateAdd(year, month, day, units, unit_type) endif let l:i += 1 endwhile - elseif l:utype == "m" + elseif l:utype ==? "m" if l:d >= l:daysInMonth let l:wasLastDayOfMonth = 1 else diff --git a/doc/todo.txt b/doc/todo.txt index 2b27039..c58ef96 100644 --- a/doc/todo.txt +++ b/doc/todo.txt @@ -22,12 +22,13 @@ Table of Contents *TodoTxt-Contents* ~ 3. TodoTxt Files.................................|TodoTxt-Files| 4. Completion....................................|TodoTxt-Completion| 5. Hierarchical Sort.............................|TodoTxt-HierarchicalSort| - 6. Mappings......................................|TodoTxt-Mappings| - 6.1 Sort.....................................|TodoTxt-Sort| - 6.2 Priorities...............................|TodoTxt-Priorities| - 6.3 Dates....................................|TodoTxt-Dates| - 6.4 Done.txt.................................|TodoTxt-Done| - 6.5 Format...................................|TodoTxt-Format| + 6. Recurrence....................................|TodoTxt-Recurrence| + 7. Mappings......................................|TodoTxt-Mappings| + 7.1 Sort.....................................|TodoTxt-Sort| + 7.2 Priorities...............................|TodoTxt-Priorities| + 7.3 Dates....................................|TodoTxt-Dates| + 7.4 Done.txt.................................|TodoTxt-Done| + 7.5 Format...................................|TodoTxt-Format| =============================================================================== 1. Release notes *TodoTxt-ReleaseNotes* ~ @@ -219,9 +220,28 @@ Defaults values are: < For more information on the available flags see |:sort| - =============================================================================== -6. Mappings *TodoTxt-Mappings* ~ +6. Recurrence *TodoTxt-Recurrence* ~ + +By adding a "rec:" tag to your task, when you complete (`x`) or +postpone (`p`) the task, a new recurrence will be created due after +the specified amount of time. + +The format is: + `rec:[+][count][d|w|m|y]` + +Where: + d = days, w = weeks, m = months, y = years + The optional `+` specifies strict recurrence (see below) + +Examples: + * `rec:2w` - Recurs two weeks after the task is completed. + * `rec:3d` - Recurs three days after the task is completed. + * `rec:+1w` - Recurs one week from the due date (strict) + +This is a non-standard but widely adopted keyword. +=============================================================================== +7. Mappings *TodoTxt-Mappings* ~ By default todo-txt.vim set all the mappings described in this section. To prevent this behavior, add the following line to your vimrc @@ -229,7 +249,7 @@ prevent this behavior, add the following line to your vimrc let g:Todo_txt_do_not_map=1 < -6.1 Sort *TodoTxt-Sort* +7.1 Sort *TodoTxt-Sort* `s` : Sort the file by priority @@ -262,7 +282,7 @@ Possible values are : + `notoverdue`: The first task that is not overdue (requires #13) + `bottom`: The last line of the buffer -6.2 Priorities *TodoTxt-Priorities* +7.2 Priorities *TodoTxt-Priorities* `j` : Lower the priority of the current line @@ -274,7 +294,7 @@ Possible values are : `c` : Add the priority (C) to the current line -6.3 Dates *TodoTxt-Dates* +7.3 Dates *TodoTxt-Dates* `d` : Insert the current date @@ -294,7 +314,7 @@ following to your vimrc: let g:Todo_txt_prefix_creation_date=1 < -6.4 Done *TodoTxt-Done* +7.4 Done *TodoTxt-Done* `x` : Toggle mark task as done (inserts or remove current @@ -308,6 +328,6 @@ following to your vimrc: `` is \ by default, so ̀`-s` means you type \s -6.5 Format *TodoTxt-format* +7.5 Format *TodoTxt-format* `ff` : Try to fix todo.txt format diff --git a/ftplugin/todo.vim b/ftplugin/todo.vim index cd726c1..a925f8b 100644 --- a/ftplugin/todo.vim +++ b/ftplugin/todo.vim @@ -79,6 +79,7 @@ if !exists("g:Todo_txt_do_not_map") || ! g:Todo_txt_do_not_map " try fix format {{{2 nnoremap