├── LICENSE ├── README.md ├── after └── syntax │ └── vimwiki.vim ├── autoload └── vimwiki_tasks.vim └── ftplugin └── vimwiki.vim /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jeroen Budts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vimwiki-tasks Vim plugin 2 | 3 | A similar (and maintained) plugin can be found here: https://github.com/tbabej/taskwiki 4 | 5 | ## NOTE 6 | This is a very early and very alpha version of the plugin. Use with caution and make a backup of 7 | your `.task` and `vimwiki` folder. You have been warned! 8 | 9 | ## Features 10 | This plugin adds some additional syntax rules to vimwiki to define a task format with due dates. It 11 | also adds highlighting for tags (`+tag`) and UUID's. The format for a task which has a due date on 12 | 2013-11-21: 13 | 14 | * [ ] This is a task with a due date (2013-11-21) 15 | 16 | It's also possible to specify a time for the due: 17 | 18 | * [ ] This is a task due at 10am (2013-11-21 10:00) 19 | 20 | Please *note* that it is officially not possible to set a due-time in taskwarrior, however by 21 | specifying the correct dateformat it however is possible since internally dates are stored as unix 22 | timestamps. So far I have not really found any side-effects of doing this. 23 | 24 | When the vimwiki file is saved all the new tasks with a due date will be added to taskwarrior. To 25 | keep the link between the task in taskwarrior and vimwiki the UUID of the task is appended to the 26 | task in vimwiki. If you have enabled Vim's `conceal` feature the UUID's will be hidden. 27 | 28 | It is also possible to add tasks without a due date into taskwarrior by ending the task in Vimwiki 29 | in `#TW`. When the vimwiki file is saved any task which ends in `#TW` will also be added to 30 | taskwarrior and the `#TW` will be replaced by the UUID. 31 | 32 | When the file is reopened in Vimwiki all the tasks which have a UUID will be synced and updated from 33 | taskwarrior info the vimwiki-file and it will be marked as modified if any updates took place. 34 | 35 | ## Installation 36 | 1. Install the vimwiki plugin for Vim 37 | 1. Install taskwarrior 38 | 1. Install this plugin 39 | 40 | ## Default values 41 | The first 10 lines of a vimwiki file will be checked for some default values which will be used for 42 | all the tasks in that vimwiki-file: 43 | 44 | * `%% Project: `: set the project for the tasks to '' 45 | * `%% Tags: +tag1 +tag2`: add these tags to every task. 46 | 47 | ## Config 48 | The following configuration options are currently available 49 | 50 | * `let g:vimwiki_tasks_annotate_origin = 0`: When `1` a reference to the vimwiki-page where the task 51 | was found will be added as an annotation 52 | * `let g:vimwiki_tasks_tags_nodue = ''`: These tags, e.g. +vimwiki +nodue, will be added to a task 53 | without a due date/time. 54 | * `let g:vimwiki_tasks_tags_duetime = ''`: These tags will be added to a task which has both a due 55 | date and time. 56 | * `let g:vimwiki_tasks_tags_duedate = ''`: These tags will be added to a task which has a due date 57 | but no due time. 58 | 59 | ## Known issues & Future plans 60 | See the issue list on Github for currently known issues and future plans. Feel free to report issues and add ideas there as well. 61 | -------------------------------------------------------------------------------- /after/syntax/vimwiki.vim: -------------------------------------------------------------------------------- 1 | syn match VimwikiTag /\v\+[a-zA-Z0-9:_-]+/ containedin=VimwikiTableRow 2 | hi link VimwikiTag VimwikiTodo 3 | 4 | syn match VimwikiDate /\v\(\d{4}-\d\d-\d\d( \d\d:\d\d)?\)/ 5 | hi link VimwikiDate VimwikiCheckBox 6 | 7 | let s:conceal = exists("+conceallevel") ? ' conceal cchar=T' : '' 8 | execute 'syn match VimwikiTaskUuid containedin=VimwikiCheckBoxDone /\v#[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/'.s:conceal 9 | hi link VimwikiTaskUuid Comment 10 | 11 | syn match TaskError /\vTASK_NOT_FOUND|TASK_DELETED/ 12 | hi link TaskError Error 13 | -------------------------------------------------------------------------------- /autoload/vimwiki_tasks.vim: -------------------------------------------------------------------------------- 1 | " TODO: task add: description in quotes? (to fix (bw)) 2 | " a list of open tasks which should be checked to see if they are completed 3 | " when the file is written 4 | let b:open_tasks = [] 5 | " the date/time when the file was read. Used to compare with last_modified 6 | " date of tasks in taskwarrior, to avoid overwriting changes from taskwarrior 7 | let b:read_time = '' 8 | 9 | function! vimwiki_tasks#write() 10 | call vimwiki_tasks#verify_taskwarrior() 11 | let l:defaults = vimwiki_tasks#get_defaults() 12 | let l:i = 1 13 | while l:i <= line('$') 14 | let l:line = getline(l:i) 15 | " check if this is a line with an open task with a due date 16 | if match(l:line, '\v\* \[[^X]\].*(\(\d{4}-\d\d-\d\d( \d\d:\d\d)?\)|#TW\s*$|#[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})') != -1 17 | let l:task = vimwiki_tasks#parse_task(l:line, l:defaults) 18 | " add the task if it does not have a uuid 19 | if l:task.uuid == "" 20 | call Task(l:task.task_args.' add '.shellescape(l:task.description).' '.JoinTags(l:task.tags_list).' '.l:task.task_meta) 21 | " find the id and the uuid of the newly created task 22 | let l:id = substitute(Task("newest limit:1 rc.verbose=nothing rc.color=off rc.defaultwidth=999 rc.report.newest.columns=id rc.report.newest.labels=ID"), "\n", "", "") 23 | let l:uuid = substitute(Task(l:id." uuid"), "\n", "", "") 24 | " add the uuid to the line and remove the #TW indicator 25 | call setline(l:i, RemoveTwIndicator(l:line)." #".l:uuid) 26 | 27 | if vimwiki_tasks#config('annotate_origin', 0) 28 | " annotate the task to reference the vimwiki file 29 | call Task(l:id.' annotate vimwiki:'.expand('%:p')) 30 | endif 31 | " see if we need to update the task in TW 32 | else 33 | let l:tw_task = vimwiki_tasks#load_task(l:task.uuid) 34 | " don't update deleted tasks 35 | if l:tw_task.status !=# 'Deleted' 36 | if l:task.description !=# l:tw_task.description || l:task.due !=# l:tw_task.due || l:task.project !=# l:defaults.project || JoinTags(l:task.tags_list) !=# JoinTags(l:tw_task.tags_list) 37 | let l:continue = 1 38 | if l:tw_task.last_modified > b:read_time 39 | let l:continue = confirm("The task was modified in taskwarrior after this file was opened. Which version do you want to keep?\nTaskwarrior: ".l:tw_task.description."\nVimwiki: ".l:task.description, "&Taskwarrior\n&Vimwiki") > 1 40 | endif 41 | if l:continue 42 | call Task(l:task.task_args.' rc.confirmation=no uuid:'.l:task.uuid. 43 | \ ' modify '.shellescape(l:task.description).' '. 44 | \ JoinTags(l:task.tags_list).' '.TagsToRemove(l:tw_task.tags_list, l:task.tags_list). 45 | \ ' '.l:task.task_meta) 46 | endif 47 | endif 48 | endif 49 | endif 50 | " check if the line is a closed task which was still open when reading the file 51 | elseif match(l:line, '\v\* \[X\].*#[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') != -1 52 | let l:task = vimwiki_tasks#parse_task(l:line, l:defaults) 53 | if index(b:open_tasks, l:task.uuid) >= 0 54 | call Task('uuid:'.l:task.uuid.' done') 55 | endif 56 | endif 57 | let l:i += 1 58 | endwhile 59 | " do a new read to sync with TW and to refresh the b:open_tasks list 60 | call vimwiki_tasks#read() 61 | endfunction 62 | 63 | function! vimwiki_tasks#read() 64 | call vimwiki_tasks#verify_taskwarrior() 65 | let b:read_time = strftime("%Y-%m-%dT%H:%M") 66 | let b:open_tasks = [] 67 | 68 | " if the file is a tasknote update it first 69 | if vimwiki_tasks#buffer_is_tasknote() 70 | call vimwiki_tasks#update_tasknote() 71 | endif 72 | 73 | " now update all the tasks in lists 74 | let l:defaults = vimwiki_tasks#get_defaults() 75 | let l:i = 1 76 | while l:i <= line('$') 77 | let l:line = getline(l:i) 78 | " if this is an open task with a uuid, check if we can update it from TW 79 | if match(l:line, '\v\* \[[^X]\].*#[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') != -1 80 | let l:task = vimwiki_tasks#parse_task(l:line, l:defaults) 81 | let l:tw_task = vimwiki_tasks#load_task(l:task.uuid) 82 | if l:tw_task.error != '' 83 | " task has errors. Notify if not already done before 84 | if match(l:line, l:tw_task.error) == -1 85 | call setline(l:i, l:line.' '.l:tw_task.error) 86 | echoerr ErrorMsg(l:tw_task.error).": ".l:line 87 | let &mod = 1 88 | endif 89 | elseif l:tw_task.status ==# 'Completed' 90 | call setline(l:i, vimwiki_tasks#build_task(l:line, l:tw_task, l:task, 1)) 91 | let &mod = 1 92 | elseif l:tw_task.status ==# 'Deleted' 93 | " Deleted is already handled above as being an error 94 | else 95 | " task is still open in TW, see if it was updated 96 | if l:task.description !=# l:tw_task.description || l:task.due !=# l:tw_task.due || JoinTags(l:task.tags_list) !=# JoinTags(l:tw_task.tags_list) 97 | " and replace it in the file 98 | call setline(l:i, vimwiki_tasks#build_task(l:line, l:tw_task, l:task)) 99 | " mark the buffer as modified 100 | let &mod = 1 101 | endif 102 | " and add the open task to the list for later reference 103 | call add(b:open_tasks, l:task.uuid) 104 | end 105 | endif 106 | let l:i += 1 107 | endwhile 108 | if vimwiki_tasks#config('update_tasklists', 1) 109 | call vimwiki_tasks#update_task_lists() 110 | endif 111 | endfunction 112 | 113 | function! vimwiki_tasks#get_defaults() 114 | let l:defaults = {'project': '', 'tags_list': []} 115 | let l:i = 1 116 | while l:i <= 10 117 | let l:line = getline(l:i) 118 | let l:project = matchstr(l:line, '\v\%\%\s*Project:\s*\zs(\w+)') 119 | if l:project != "" 120 | let l:defaults.project = l:project 121 | endif 122 | let l:tags = matchstr(l:line, '\v\%\%\s*Tags:\s*\zs(.+)\s*$') 123 | if l:tags != "" 124 | let l:defaults.tags_list = SplitTags(l:tags) 125 | endif 126 | let l:i +=1 127 | endwhile 128 | return l:defaults 129 | endfunction 130 | 131 | " a:1 boolean, 1 if the task should be marked as finished, otherwise the state 132 | " is reused from the task text 133 | function! vimwiki_tasks#build_task(line, tw_task, task, ...) 134 | " build the new task line 135 | let l:match = matchlist(a:line, '\v^(\s*)\* \[(.)\]') 136 | let l:indent = l:match[1] 137 | let l:state = l:match[2] 138 | if a:0 > 0 && a:1 == 1 139 | let l:state = 'X' 140 | endif 141 | let l:newline = l:indent."* [".l:state."] ".a:tw_task.description 142 | if len(a:tw_task.tags_list) > 0 143 | " filter the default tags out so they are not added to the line 144 | let l:tags = copy(a:tw_task.tags_list) 145 | call filter(l:tags, "!HasItem(a:task.tags_default, v:val)") 146 | " if there are still tags left, add them to the line 147 | if len(l:tags) > 0 148 | let l:newline .= ' '.JoinTags(l:tags) 149 | endif 150 | endif 151 | if a:tw_task.due != "" 152 | " change the T date/time separator into a space 153 | let l:due_printable = substitute(a:tw_task.due, 'T', " ", "") 154 | " remove the seconds as we don't use them in Vimwiki 155 | let l:due_printable = substitute(l:due_printable, '\v:00$', "", "") 156 | let l:newline .= " (".l:due_printable.")" 157 | endif 158 | let l:newline .= " #".a:tw_task.uuid 159 | return l:newline 160 | endfunction 161 | 162 | function s:HasItem(list, item) 163 | return index(a:list, a:item) != -1 164 | endfunction 165 | 166 | function! vimwiki_tasks#parse_task(line, defaults) 167 | let l:task = vimwiki_tasks#empty_task() 168 | " create the task 169 | let l:match = matchlist(a:line, '\v\* \[.\]\s+(.*)\s*') 170 | let l:task.description = l:match[1] 171 | " construct the task creation command and create 172 | let l:task.task_args = '' 173 | let l:task.task_meta = '' 174 | " add a project if necessary 175 | if has_key(a:defaults, 'project') 176 | let l:task.task_meta .= ' project:'.a:defaults.project 177 | endif 178 | " add due date if available 179 | let l:due = matchlist(a:line, '\v\((\d{4}-\d\d-\d\d)( (\d\d:\d\d))?\)') 180 | if !empty(l:due) 181 | let l:task.due_date = l:due[1] 182 | let l:task.due_time = get(l:due, 3, '00:00') 183 | if l:task.due_time == "" 184 | let l:task.due_time = '00:00' 185 | endif 186 | " add seconds so we have the correct date/time format 187 | let l:task.due_time .= ":00" 188 | " remove date in line 189 | let l:task.description = substitute(l:task.description, '\v\(\d{4}-\d\d-\d\d( \d\d:\d\d)?\)', "", "") 190 | " set the due in task_meta 191 | let l:task.due = l:task.due_date.'T'.l:task.due_time 192 | let l:task.task_meta .= ' due:'.l:task.due 193 | " set the dateformat in task_args 194 | let l:task.task_args .= ' rc.dateformat=Y-M-DTH:N:S' 195 | endif 196 | " get the uuid from the task if it is there, and remove it from the task description 197 | let l:task.uuid = matchstr(a:line, '\v#\zs([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})') 198 | if l:task.uuid != "" 199 | let l:task.description = substitute(l:task.description, '\v#[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', "", "") 200 | endif 201 | 202 | " Parse the normal tags from the description 203 | call extend(l:task.tags_list, ParseTags(l:task.description)) 204 | " and remove the tags from the description 205 | let l:task.description = StripTags(l:task.description) 206 | 207 | " Parse the default tags. Add them to the tags_list, but also add them to 208 | " the tags_default list so we can keep track of which tags where added 209 | " automatically (= should not end up in the vimwiki-line) 210 | call extend(l:task.tags_list, a:defaults.tags_list) 211 | call extend(l:task.tags_default, a:defaults.tags_list) 212 | " add the tags specific for the type of task 213 | let l:default_tags_key = '' 214 | if l:task.due == "" 215 | let l:default_tags_key = 'tags_nodue' 216 | else 217 | if get(l:due, 3, '') != "" 218 | let l:default_tags_key = 'tags_duetime' 219 | else 220 | let l:task.tags .= ' '.vimwiki_tasks#config('tags_duedate', '') 221 | let l:default_tags_key = 'tags_duedate' 222 | endif 223 | endif 224 | let l:task_tags = SplitTags(vimwiki_tasks#config(l:default_tags_key, '')) 225 | call extend(l:task.tags_list, l:task_tags) 226 | call extend(l:task.tags_default, l:task_tags) 227 | 228 | " remove any #TW at the end (= a new task without a due) 229 | let l:task.description = RemoveTwIndicator(l:task.description) 230 | 231 | " and strip any whitespace 232 | let l:task.description = Strip(l:task.description) 233 | 234 | return l:task 235 | endfunction 236 | 237 | function! vimwiki_tasks#load_task(uuid) 238 | let l:task = vimwiki_tasks#empty_task() 239 | let l:cmd = 'rc.verbose=nothing rc.defaultwidth=999 rc.dateformat.info=Y-M-DTH:N:S rc.color=off uuid:' 240 | \ .a:uuid.' info | grep "^\(ID\|UUID\|Description\|Status\|Due\|Project\|Tags\|Last modified\)"' 241 | let l:result = split(Task(l:cmd), '\n') 242 | for l:result_line in l:result 243 | let l:match = matchlist(l:result_line, '\v(Last modified|\w+)\s+(.*)') 244 | let l:key = substitute(l:match[1], '\v\s+', '_', 'g') 245 | let l:task[tolower(l:key)] = l:match[2] 246 | endfor 247 | " check for any errors 248 | if l:task.uuid == '' 249 | let l:task.error = 'TASK_NOT_FOUND' 250 | elseif l:task.status ==# 'Deleted' 251 | let l:task.error = 'TASK_DELETED' 252 | endif 253 | " get the correct modification date 254 | if l:task.last_modified != -1 255 | let l:datetime = matchstr(l:task.last_modified, '\v\S{19}') 256 | if l:datetime != '' 257 | let l:task.last_modified = l:datetime 258 | endif 259 | endif 260 | " split the tags 261 | let l:task.tags_list = SplitTags(l:task.tags) 262 | return l:task 263 | endfunction 264 | 265 | function! s:Strip(input_string) 266 | return substitute(a:input_string, '^\s*\(.\{-}\)\s*$', '\1', '') 267 | endfunction 268 | 269 | function! s:RemoveTwIndicator(input) 270 | return substitute(a:input, '\v\s?#TW\s*$', "", "") 271 | endfunction 272 | 273 | function! s:SplitTags(tagstr) 274 | let l:tags = split(a:tagstr, '\v\s+') 275 | let l:i = 0 276 | while l:i < len(l:tags) 277 | if match(l:tags[l:i], '\v^\+') == -1 278 | let l:tags[l:i] = '+'.l:tags[l:i] 279 | endif 280 | let l:i += 1 281 | endwhile 282 | return l:tags 283 | endfunction 284 | 285 | function! s:ParseTags(str) 286 | let l:tags = [] 287 | let l:i = 1 288 | while l:i != -1 289 | let l:tag = matchstr(a:str, '\v(\+\w+)', 0, l:i) 290 | if l:tag == '' 291 | let l:i = -1 292 | else 293 | call add(l:tags, l:tag) 294 | let l:i += 1 295 | endif 296 | endwhile 297 | return l:tags 298 | endfunction 299 | 300 | function! s:StripTags(str) 301 | " strip tags 302 | let l:str = substitute(a:str, '\v\+\w+', '', 'g') 303 | " strip multiple spaces 304 | let l:str = substitute(l:str, '\v\s+', ' ', 'g') 305 | " strip spaces 306 | return Strip(l:str) 307 | endfunction 308 | 309 | function! s:JoinTags(taglist) 310 | call sort(a:taglist) 311 | return join(a:taglist, ' ') 312 | endfunction 313 | 314 | function! s:TagsToRemove(old_tags, new_tags) 315 | let l:remove = [] 316 | for l:tag in a:old_tags 317 | if index(a:new_tags, l:tag) == -1 318 | call add(l:remove, l:tag) 319 | endif 320 | endfor 321 | let l:remove_str = JoinTags(l:remove) 322 | " replace the + sign by a - sign so the tags are removed by TW 323 | return substitute(l:remove_str, '\v\+', '-', 'g') 324 | endfunction 325 | 326 | function! s:System(cmd) 327 | " echom a:cmd 328 | return system(a:cmd) 329 | endfunction 330 | 331 | function! s:Task(args) 332 | " execute task with the given args + any optional args specified by the user 333 | return system('task '.vimwiki_tasks#config('task_args', '').' '.a:args) 334 | endfunction 335 | 336 | function! s:ErrorMsg(error) 337 | if a:error ==# 'TASK_DELETED' 338 | return 'Task was deleted in taskwarrior' 339 | elseif a:error ==# 'TASK_NOT_FOUND' 340 | return 'Task was not found in taskwarrior' 341 | endif 342 | return 'Unknown error' 343 | endfunction 344 | 345 | function! vimwiki_tasks#empty_task() 346 | return { 347 | \ 'id': 0, 348 | \'uuid': '', 349 | \ 'description': '', 350 | \ 'due': '', 351 | \ 'status': '', 352 | \ 'project': '', 353 | \ 'tags': '', 354 | \ 'tags_list': [], 355 | \ 'tags_default': [], 356 | \ 'error': '', 357 | \ 'last_modified': -1, 358 | \ } 359 | endfunction 360 | 361 | function! vimwiki_tasks#config(key, default) 362 | if exists('g:vimwiki_tasks_'.a:key) 363 | return g:vimwiki_tasks_{a:key} 364 | endif 365 | return a:default 366 | endfunction 367 | 368 | function! vimwiki_tasks#verify_taskwarrior() 369 | if !executable('task') 370 | throw "`task` not found or not executable" 371 | endif 372 | endfunction 373 | 374 | function! vimwiki_tasks#display_task_id(copy_to_clipboard) 375 | let l:uuid = matchstr(getline(line('.')), '\v\* \[.\].*#\zs[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') 376 | if l:uuid != '' 377 | let l:tw_task = vimwiki_tasks#load_task(l:uuid) 378 | let l:msg = "Task ID: ".l:tw_task.id 379 | if (a:copy_to_clipboard) 380 | let @+ = l:tw_task.id 381 | let l:msg .= ", copied to clipboard" 382 | endif 383 | echo l:msg 384 | else 385 | echo "Could not find a task on this line!" 386 | endif 387 | endfunction 388 | 389 | function! vimwiki_tasks#display_task_uuid(copy_to_clipboard) 390 | let l:uuid = matchstr(getline(line('.')), '\v\* \[.\].*#\zs[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') 391 | if l:uuid != '' 392 | let l:msg = "Task UUID: ".l:uuid 393 | if a:copy_to_clipboard 394 | let @+ = l:uuid 395 | let l:msg .= ", copied to clipboard" 396 | endif 397 | echo l:msg 398 | else 399 | echo "Could not find a task on this line!" 400 | endif 401 | endfunction 402 | 403 | function! s:UuidsInBuffer() 404 | let l:i = 1 405 | let l:uuids = [] 406 | while l:i <= line('$') 407 | let l:uuid = matchstr(getline(l:i), '\v\* \[.\].*#\zs[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') 408 | if l:uuid != '' 409 | call add(l:uuids, l:uuid) 410 | endif 411 | let l:i += 1 412 | endwhile 413 | return l:uuids 414 | endfunction 415 | 416 | " a:1 boolean, if 1 the tasklist filter will be inserted, otherwise not (not 417 | " that the config parameter is also taken into account, but it can be 418 | " overriden by setting a:1 to a value other than 1. This case is used during 419 | " the updating of the taskslists where the filter should not be inserted again 420 | function! vimwiki_tasks#insert_tasks(filter, bang, line, ...) 421 | echo "Loading tasks..." 422 | redraw 423 | let l:report = vimwiki_tasks#config('report', 'all') 424 | let l:cmd = l:report 425 | let l:cmd .= ' rc.report.'.l:report.'.columns=uuid rc.report.'.l:report.'.labels=UUID rc.verbose=nothing ' 426 | let l:cmd .= a:filter 427 | let l:uuids = split(Task(l:cmd), '\n') 428 | let l:empty_task = vimwiki_tasks#empty_task() 429 | let l:lines = [] 430 | let l:uuids_in_buffer = UuidsInBuffer() 431 | for l:uuid in l:uuids 432 | if a:bang == '!' || index(l:uuids_in_buffer, l:uuid) == -1 433 | let l:tw_task = vimwiki_tasks#load_task(l:uuid) 434 | let l:line = vimwiki_tasks#build_task('* [ ]', l:tw_task, l:empty_task, l:tw_task.status == 'Completed') 435 | call add(l:lines, l:line) 436 | endif 437 | endfor 438 | let l:num_added = 0 439 | if len(l:lines) > 0 440 | " XXX: check if current line is empty and replace it, otherwise append? 441 | if vimwiki_tasks#config('include_tasklist', 1) && (a:0 == 0 || a:0 > 0 && a:1 == 1) 442 | let l:num_added += 1 443 | call append(a:line, '%% TaskList: '.a:filter) 444 | endif 445 | call append(a:line + l:num_added, l:lines) 446 | " fix the indent of the new lines 447 | exec "normal! ".(len(l:lines)+1)."==" 448 | redraw " get rid of the 'XX lines indented' message 449 | " and set the '[ to the first new line 450 | normal jm[ 451 | endif 452 | echo "Inserted ".len(l:lines)." task(s)" 453 | return len(l:lines) + l:num_added 454 | endfunction 455 | 456 | " TODO: make it also work on a tasknote file 457 | " TODO: add a `:TaskModify` command (with optional confirmation) 458 | function! vimwiki_tasks#current_task_do(task_cmd) 459 | " TODO: shellescape? 460 | let l:line = getline(line('.')) 461 | let l:uuid = matchstr(l:line, '\v\* \[.\].*#\zs[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') 462 | if l:uuid != '' 463 | let l:tw_task_before = vimwiki_tasks#load_task(l:uuid) 464 | let l:cmd = "!task ".vimwiki_tasks#config('task_args', '')." ".l:uuid." ".a:task_cmd 465 | if has("gui_running") 466 | let l:cmd .= " rc.color=off rc.defaultwidth=".&columns 467 | endif 468 | execute l:cmd 469 | " and rebuild the task if the last_modified has changed 470 | let l:tw_task = vimwiki_tasks#load_task(l:uuid) 471 | if l:tw_task_before.last_modified != l:tw_task.last_modified 472 | let l:task = vimwiki_tasks#parse_task(l:line, vimwiki_tasks#get_defaults()) 473 | let l:new_line = vimwiki_tasks#build_task(l:line, l:tw_task, l:task, l:tw_task.status == 'Completed') 474 | call setline('.', l:new_line) 475 | let &mod = 1 476 | endif 477 | else 478 | echo "Could not find a task on this line!" 479 | endif 480 | endfunction 481 | 482 | function! vimwiki_tasks#update_task_lists() 483 | let l:i = 1 484 | while l:i <= line('$') 485 | let l:line = getline(l:i) 486 | let l:tasklist = matchstr(l:line, '\v\%\%\s*TaskList:\s*\zs(.*$)') 487 | if l:tasklist != '' 488 | " ok we found a tasklist, try to add new tasks 489 | let l:num_added = 0 490 | let l:num_added += vimwiki_tasks#insert_tasks(l:tasklist, '', l:i, 0) 491 | let l:i += l:num_added + 1 492 | else 493 | let l:i += 1 494 | endif 495 | endwhile 496 | endfunction 497 | 498 | function! vimwiki_tasks#load_full_task(uuid) 499 | " TODO: check how blocked and blocking tasks are presented 500 | let l:cmd = 'rc.verbose=labels rc.defaultwidth=999 rc.dateformat.info=Y-M-D\ H:N:S rc.color=off uuid:'.a:uuid.' info' 501 | let l:result = split(Task(l:cmd), '\n') 502 | let l:task_details = {'details': [], 'annotations': [], 'description': '', 'last_modified': ''} 503 | if len(l:result) > 5 " for a valid task we will have at least 5 lines or so 504 | let l:i = 3 505 | " get the width of the labels, yes this is dirty but avoids having to 506 | " depend on a JSON parser 507 | let l:width = matchstr(l:result[1], '\v^\-+') 508 | let l:width = strlen(l:width) 509 | while l:i < len(l:result) 510 | if match(l:result[l:i], '\v^\s*$') != -1 511 | " last detail line reached before the history log 512 | break 513 | else 514 | let l:match = matchlist(l:result[l:i], '\v^(.{'.l:width.'})(.*)') 515 | if match(l:match[1], '\v^\s+$') > -1 516 | call add(l:task_details.annotations, '* '.Strip(l:match[2])) 517 | elseif match(l:match[1], '\v^Description|UUID') == -1 518 | call add(l:task_details.details, l:match[1].'::'.l:match[2]) 519 | endif 520 | " store the description in a separate key 521 | if match(l:match[1], '\v^Description') != -1 522 | let l:task_details.description = l:match[2] 523 | endif 524 | " store the last modified also in a separate key 525 | if match(l:match[1], '\v^Last modified') != -1 526 | let l:task_details.last_modified = l:match[1].'::'.l:match[2] 527 | endif 528 | endif 529 | let l:i += 1 530 | endwhile 531 | endif 532 | return l:task_details 533 | endfunction 534 | 535 | function! s:TaskNotesPath() 536 | let l:path = VimwikiGet('path') 537 | let l:path .= vimwiki_tasks#config('note_path', '') 538 | if match(l:path, '\v/$') == -1 539 | let l:path .= '/' 540 | endif 541 | echom l:path 542 | return l:path 543 | endfunction 544 | 545 | function! vimwiki_tasks#open_tasknotes() 546 | " TODO: load the correct task note path from the task annotations 547 | let l:line = getline(line('.')) 548 | " TODO: make sure it works on any UUID 549 | let l:uuid = matchstr(l:line, '\v\* \[.\].*#\zs[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') 550 | if l:uuid != '' 551 | let l:tw_task = vimwiki_tasks#load_task(l:uuid) 552 | let l:taskpath = TaskNotesPath().l:uuid.".".expand('%:e') 553 | execute "edit ".l:taskpath 554 | if glob(l:taskpath, 1) == '' 555 | " file does not yet exist on disk, fill the buffer with the initial content 556 | call vimwiki_tasks#insert_task_details(l:uuid) 557 | " Insert the notes header 558 | " call append(line('$'), '') 559 | call append(line('$') - 1, CreateHeader('Notes')) 560 | " put the cursor on the last line 561 | normal G 562 | endif 563 | else 564 | echo "Could not find a task on this line!" 565 | endif 566 | endfunction 567 | 568 | function! vimwiki_tasks#insert_task_details(uuid) 569 | let l:details = vimwiki_tasks#load_full_task(a:uuid) 570 | call append(0, '%% TaskUUID: '.a:uuid) 571 | call append(1, '') 572 | call append(2, Strip(l:details.description)) 573 | normal 3gg 574 | call vimwiki#base#AddHeaderLevel() 575 | call append(3, CreateHeader('Task details')) 576 | let l:i = 4 577 | for l:detail in l:details.details 578 | call append(l:i, l:detail) 579 | let l:i += 1 580 | endfor 581 | call append(0 + l:i, '') 582 | call append(1 + l:i, CreateHeader('Annotations')) 583 | let l:i += 2 584 | for l:annotation in l:details.annotations 585 | call append(l:i, l:annotation) 586 | let l:i += 1 587 | endfor 588 | call append(l:i, '') 589 | endfunction 590 | 591 | function! vimwiki_tasks#buffer_is_tasknote() 592 | return match(getline(1), '\v^\%\% TaskUUID: [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') != -1 593 | endfunction 594 | 595 | function! vimwiki_tasks#update_tasknote() 596 | " TODO: error checking? 597 | let l:uuid = matchstr(getline(1), '\v^\%\% TaskUUID: \zs[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') 598 | let l:tw_full_task = vimwiki_tasks#load_full_task(l:uuid) 599 | let l:i = 2 600 | let l:last_modified_line = '' 601 | let l:last_header_match = 0 602 | while l:i < line('$') 603 | let l:line = getline(l:i) 604 | " find the last modified line, this will be used to check if the task 605 | " details should be updated 606 | if match(l:line, '\v^Last modified') != -1 607 | let l:last_modified_line = l:line 608 | endif 609 | " check if we already match the Annotations header 610 | if match(l:line, '\v^'.CreateHeader('Annotations')) != -1 611 | let l:last_header_match = 1 612 | endif 613 | " check if we are past the last header, then the first empty line 614 | " means the end of the generated content and the start of the actual 615 | " note which should be left untouched 616 | if l:last_header_match && match(l:line, '\v^\s*$') != -1 617 | " ok last generated line reached, break out of the loop 618 | " l:i now also contains the line number where the generated 619 | " content ends 620 | break 621 | endif 622 | let l:i += 1 623 | endwhile 624 | 625 | " update the task if it is was updated 626 | if l:last_modified_line != l:tw_full_task.last_modified 627 | " remove the old copy of the task details 628 | execute '1,'.l:i.'delete' 629 | " and insert the new updated details 630 | call vimwiki_tasks#insert_task_details(l:uuid) 631 | redraw " get rid of the 'xx fewer lines' 632 | let &mod = 1 633 | echo "Updated the task details" 634 | endif 635 | endfunction 636 | 637 | function! s:CreateHeader(header) 638 | let hdr = g:vimwiki_rxH.g:vimwiki_rxH.' '.a:header 639 | if g:vimwiki_symH 640 | let hdr .= ' '.g:vimwiki_rxH.g:vimwiki_rxH 641 | endif 642 | return hdr 643 | endfunction 644 | -------------------------------------------------------------------------------- /ftplugin/vimwiki.vim: -------------------------------------------------------------------------------- 1 | if vimwiki_tasks#config('taskwarrior_integration', 1) 2 | augroup vimwiki_tasks 3 | " when saving the file sync the tasks from vimwiki to TW 4 | autocmd! 5 | execute "autocmd BufWrite *.".expand('%:e')." call vimwiki_tasks#write()" 6 | augroup END 7 | 8 | " sync the tasks from TW to vimwiki 9 | call vimwiki_tasks#read() 10 | 11 | command! DisplayTaskID call vimwiki_tasks#display_task_id(0) 12 | command! CopyTaskID call vimwiki_tasks#display_task_id(1) 13 | command! DisplayTaskUUID call vimwiki_tasks#display_task_uuid(0) 14 | command! CopyTaskUUID call vimwiki_tasks#display_task_uuid(1) 15 | command! -nargs=1 -bang InsertTasks call vimwiki_tasks#insert_tasks(, '', line('.')) 16 | command! -nargs=1 TaskCmd call vimwiki_tasks#current_task_do() 17 | " XXX: add `:TaskModify` command? 18 | command! UpdateTaskLists call vimwiki_tasks#update_task_lists() 19 | " TODO: make OpenTaskNotes accept and optional id/uuid to open that one 20 | " instead of looking at the current cursor-line 21 | " TODO: set a default keybinding? (o ?) 22 | command! OpenTaskNotes call vimwiki_tasks#open_tasknotes() 23 | endif 24 | --------------------------------------------------------------------------------