comparison autoload/gutentags.vim @ 202:b50b6d0f82dd

Refactor for Vim8/Neovim job support. - Refactor all modules' `generate` methods to use a vaguely generic job API wrapper that works for both Vim8 and Neovim jobs. - Make the `statusline` method use new `User` autocommands driven by the job-started/ended callbacks. - Remove all the lock-file-related stuff. - Better error/warning messages. - Move a few things around.
author Ludovic Chabant <ludovic@chabant.com>
date Sat, 31 Mar 2018 18:42:54 -0700
parents f7a417234dea
children 6e96ddda0fd3
comparison
equal deleted inserted replaced
201:b41385032f86 202:b50b6d0f82dd
1 " gutentags.vim - Automatic ctags management for Vim 1 " gutentags.vim - Automatic ctags management for Vim
2 2
3 " Utilities {{{ 3 " Utilities {{{
4 4
5 function! gutentags#chdir(path) 5 function! gutentags#chdir(path)
6 if has('nvim') 6 if has('nvim')
7 let chdir = haslocaldir() ? 'lcd' : haslocaldir(-1, 0) ? 'tcd' : 'cd' 7 let chdir = haslocaldir() ? 'lcd' : haslocaldir(-1, 0) ? 'tcd' : 'cd'
8 else 8 else
9 let chdir = haslocaldir() ? 'lcd' : 'cd' 9 let chdir = haslocaldir() ? 'lcd' : 'cd'
10 endif 10 endif
11 execute chdir a:path 11 execute chdir a:path
12 endfunction 12 endfunction
13 13
14 " Throw an exception message. 14 " Throw an exception message.
15 function! gutentags#throw(message) 15 function! gutentags#throw(message)
16 throw "gutentags: " . a:message 16 throw "gutentags: " . a:message
17 endfunction 17 endfunction
18 18
19 " Throw an exception message and set Vim's error message variable. 19 " Show an error message.
20 function! gutentags#throwerr(message) 20 function! gutentags#error(message)
21 let v:errmsg = "gutentags: " . a:message 21 let v:errmsg = "gutentags: " . a:message
22 throw v:errmsg 22 echoerr v:errmsg
23 endfunction
24
25 " Show a warning message.
26 function! gutentags#warning(message)
27 echohl WarningMsg
28 echom "gutentags: " . a:message
29 echohl None
23 endfunction 30 endfunction
24 31
25 " Prints a message if debug tracing is enabled. 32 " Prints a message if debug tracing is enabled.
26 function! gutentags#trace(message, ...) 33 function! gutentags#trace(message, ...)
27 if g:gutentags_trace || (a:0 && a:1) 34 if g:gutentags_trace || (a:0 && a:1)
46 endif 53 endif
47 endfunction 54 endfunction
48 55
49 " Shell-slashes the path (opposite of `normalizepath`). 56 " Shell-slashes the path (opposite of `normalizepath`).
50 function! gutentags#shellslash(path) 57 function! gutentags#shellslash(path)
51 if exists('+shellslash') && !&shellslash 58 if exists('+shellslash') && !&shellslash
52 return substitute(a:path, '\v\\', '/', 'g') 59 return substitute(a:path, '\v\\', '/', 'g')
53 else 60 else
54 return a:path 61 return a:path
55 endif 62 endif
56 endfunction 63 endfunction
57 64
58 " Gets a file path in the correct `plat` folder. 65 " Gets a file path in the correct `plat` folder.
59 function! gutentags#get_plat_file(filename) abort 66 function! gutentags#get_plat_file(filename) abort
60 return g:gutentags_plat_dir . a:filename . g:gutentags_script_ext 67 return g:gutentags_plat_dir . a:filename . g:gutentags_script_ext
63 " Gets a file path in the resource folder. 70 " Gets a file path in the resource folder.
64 function! gutentags#get_res_file(filename) abort 71 function! gutentags#get_res_file(filename) abort
65 return g:gutentags_res_dir . a:filename 72 return g:gutentags_res_dir . a:filename
66 endfunction 73 endfunction
67 74
75 " Generate a path for a given filename in the cache directory.
76 function! gutentags#get_cachefile(root_dir, filename) abort
77 if gutentags#is_path_rooted(a:filename)
78 return a:filename
79 endif
80 let l:tag_path = gutentags#stripslash(a:root_dir) . '/' . a:filename
81 if g:gutentags_cache_dir != ""
82 " Put the tag file in the cache dir instead of inside the
83 " project root.
84 let l:tag_path = g:gutentags_cache_dir . '/' .
85 \tr(l:tag_path, '\/: ', '---_')
86 let l:tag_path = substitute(l:tag_path, '/\-', '/', '')
87 endif
88 let l:tag_path = gutentags#normalizepath(l:tag_path)
89 return l:tag_path
90 endfunction
91
92 " Makes sure a given command starts with an executable that's in the PATH.
93 function! gutentags#validate_cmd(cmd) abort
94 if !empty(a:cmd) && executable(split(a:cmd)[0])
95 return a:cmd
96 endif
97 return ""
98 endfunction
99
100 " Makes an appropriate command line for use with `job_start` by converting
101 " a list of possibly quoted arguments into a single string on Windows, or
102 " into a list of unquoted arguments on Unix/Mac.
103 if has('win32') || has('win64')
104 function! gutentags#make_args(cmd) abort
105 return join(a:cmd, ' ')
106 endfunction
107 else
108 function! gutentags#make_args(cmd) abort
109 let l:outcmd = []
110 for cmdarg in a:cmd
111 " Thanks Vimscript... you can use negative integers for strings
112 " in the slice notation, but not for indexing characters :(
113 let l:arglen = strlen(cmdarg)
114 if (cmdarg[0] == '"' && cmdarg[l:arglen - 1] == '"') ||
115 \(cmdarg[0] == "'" && cmdarg[l:arglen - 1] == "'")
116 call add(l:outcmd, cmdarg[1:-2])
117 else
118 call add(l:outcmd, cmdarg)
119 endif
120 endfor
121 return l:outcmd
122 endfunction
123 endif
124
68 " Returns whether a path is rooted. 125 " Returns whether a path is rooted.
69 if has('win32') || has('win64') 126 if has('win32') || has('win64')
70 function! gutentags#is_path_rooted(path) abort 127 function! gutentags#is_path_rooted(path) abort
71 return len(a:path) >= 2 && ( 128 return len(a:path) >= 2 && (
72 \a:path[0] == '/' || a:path[0] == '\' || a:path[1] == ':') 129 \a:path[0] == '/' || a:path[0] == '\' || a:path[1] == ':')
73 endfunction 130 endfunction
74 else 131 else
75 function! gutentags#is_path_rooted(path) abort 132 function! gutentags#is_path_rooted(path) abort
76 return !empty(a:path) && a:path[0] == '/' 133 return !empty(a:path) && a:path[0] == '/'
77 endfunction 134 endfunction
78 endif 135 endif
79 136
80 " }}} 137 " }}}
81 138
82 " Gutentags Setup {{{ 139 " Gutentags Setup {{{
100 break 157 break
101 endif 158 endif
102 endfor 159 endfor
103 160
104 let s:known_projects[a:path] = l:result 161 let s:known_projects[a:path] = l:result
105 endfunction
106
107 function! gutentags#validate_cmd(cmd) abort
108 if !empty(a:cmd) && executable(split(a:cmd)[0])
109 return a:cmd
110 endif
111 return ""
112 endfunction 162 endfunction
113 163
114 function! gutentags#get_project_file_list_cmd(path) abort 164 function! gutentags#get_project_file_list_cmd(path) abort
115 if type(g:gutentags_file_list_command) == type("") 165 if type(g:gutentags_file_list_command) == type("")
116 return gutentags#validate_cmd(g:gutentags_file_list_command) 166 return gutentags#validate_cmd(g:gutentags_file_list_command)
179 " Get info on the project we're inside of. 229 " Get info on the project we're inside of.
180 function! gutentags#get_project_info(path) abort 230 function! gutentags#get_project_info(path) abort
181 return get(s:known_projects, a:path, {}) 231 return get(s:known_projects, a:path, {})
182 endfunction 232 endfunction
183 233
184 " Generate a path for a given filename in the cache directory.
185 function! gutentags#get_cachefile(root_dir, filename) abort
186 if gutentags#is_path_rooted(a:filename)
187 return a:filename
188 endif
189 let l:tag_path = gutentags#stripslash(a:root_dir) . '/' . a:filename
190 if g:gutentags_cache_dir != ""
191 " Put the tag file in the cache dir instead of inside the
192 " project root.
193 let l:tag_path = g:gutentags_cache_dir . '/' .
194 \tr(l:tag_path, '\/: ', '---_')
195 let l:tag_path = substitute(l:tag_path, '/\-', '/', '')
196 endif
197 let l:tag_path = gutentags#normalizepath(l:tag_path)
198 return l:tag_path
199 endfunction
200
201 " Setup gutentags for the current buffer. 234 " Setup gutentags for the current buffer.
202 function! gutentags#setup_gutentags() abort 235 function! gutentags#setup_gutentags() abort
203 if exists('b:gutentags_files') && !g:gutentags_debug 236 if exists('b:gutentags_files') && !g:gutentags_debug
204 " This buffer already has gutentags support. 237 " This buffer already has gutentags support.
205 return 238 return
297 endfor 330 endfor
298 endfunction 331 endfunction
299 332
300 " }}} 333 " }}}
301 334
302 " Tags File Management {{{ 335 " Job Management {{{
303 336
304 " List of queued-up jobs, and in-progress jobs, per module. 337 " List of queued-up jobs, and in-progress jobs, per module.
305 let s:update_queue = {} 338 let s:update_queue = {}
306 let s:maybe_in_progress = {} 339 let s:update_in_progress = {}
307 for module in g:gutentags_modules 340 for module in g:gutentags_modules
308 let s:update_queue[module] = [] 341 let s:update_queue[module] = []
309 let s:maybe_in_progress[module] = {} 342 let s:update_in_progress[module] = []
310 endfor 343 endfor
311 344
312 " Make a given file known as being currently generated or updated. 345 function! gutentags#add_job(module, tags_file, data) abort
313 function! gutentags#add_progress(module, file) abort 346 call add(s:update_in_progress[a:module], [a:tags_file, a:data])
314 let l:abs_file = fnamemodify(a:file, ':p') 347 endfunction
315 let s:maybe_in_progress[a:module][l:abs_file] = localtime() 348
316 endfunction 349 function! gutentags#find_job_index_by_tags_file(module, tags_file) abort
317 350 let l:idx = -1
318 " Get how to execute an external command depending on debug settings. 351 for upd_info in s:update_in_progress[a:module]
319 function! gutentags#get_execute_cmd() abort 352 let l:idx += 1
320 if has('win32') 353 if upd_info[0] == a:tags_file
321 let l:cmd = '!start ' 354 return l:idx
322 if g:gutentags_background_update 355 endif
323 let l:cmd .= '/b ' 356 endfor
324 endif 357 return -1
325 return l:cmd 358 endfunction
359
360 function! gutentags#find_job_index_by_data(module, data) abort
361 let l:idx = -1
362 for upd_info in s:update_in_progress[a:module]
363 let l:idx += 1
364 if upd_info[1] == a:data
365 return l:idx
366 endif
367 endfor
368 return -1
369 endfunction
370
371 function! gutentags#get_job_tags_file(module, job_idx) abort
372 return s:update_in_progress[a:module][a:job_idx][0]
373 endfunction
374
375 function! gutentags#get_job_data(module, job_idx) abort
376 return s:update_in_progress[a:module][a:job_idx][1]
377 endfunction
378
379 function! gutentags#remove_job(module, job_idx) abort
380 let l:tags_file = s:update_in_progress[a:module][a:job_idx][0]
381 call remove(s:update_in_progress[a:module], a:job_idx)
382
383 " Run the user callback for finished jobs.
384 silent doautocmd User GutentagsUpdated
385
386 " See if we had any more updates queued up for this.
387 let l:qu_idx = -1
388 for qu_info in s:update_queue[a:module]
389 let l:qu_idx += 1
390 if qu_info[0] == l:tags_file
391 break
392 endif
393 endfor
394 if l:qu_idx >= 0
395 let l:qu_info = s:update_queue[a:module][l:qu_idx]
396 call remove(s:update_queue[a:module], l:qu_idx)
397
398 if bufexists(l:qu_info[1])
399 call gutentags#trace("Finished ".a:module." job, ".
400 \"running queued update for '".l:tags_file."'.")
401 call s:update_tags(l:qu_info[1], a:module, l:qu_info[2], 2)
402 else
403 call gutentags#trace("Finished ".a:module." job, ".
404 \"but skipping queued update for '".l:tags_file."' ".
405 \"because originating buffer doesn't exist anymore.")
406 endif
326 else 407 else
327 return '!' 408 call gutentags#trace("Finished ".a:module." job.")
328 endif 409 endif
329 endfunction 410 endfunction
330 411
331 " Get the suffix for how to execute an external command. 412 function! gutentags#remove_job_by_data(module, data) abort
332 function! gutentags#get_execute_cmd_suffix() abort 413 let l:idx = gutentags#find_job_index_by_data(a:module, a:data)
333 if has('win32') 414 call gutentags#remove_job(a:module, l:idx)
334 return '' 415 endfunction
335 else 416
336 return ' &' 417 " }}}
337 endif 418
338 endfunction 419 " Tags File Management {{{
339 420
340 " (Re)Generate the tags file for the current buffer's file. 421 " (Re)Generate the tags file for the current buffer's file.
341 function! s:manual_update_tags(bang) abort 422 function! s:manual_update_tags(bang) abort
342 let l:bn = bufnr('%') 423 let l:bn = bufnr('%')
343 for module in g:gutentags_modules 424 for module in g:gutentags_modules
344 call s:update_tags(l:bn, module, a:bang, 0) 425 call s:update_tags(l:bn, module, a:bang, 0)
345 endfor 426 endfor
346 silent doautocmd User GutentagsUpdated 427 silent doautocmd User GutentagsUpdating
347 endfunction 428 endfunction
348 429
349 " (Re)Generate the tags file for a buffer that just go saved. 430 " (Re)Generate the tags file for a buffer that just go saved.
350 function! s:write_triggered_update_tags(bufno) abort 431 function! s:write_triggered_update_tags(bufno) abort
351 if g:gutentags_enabled && g:gutentags_generate_on_write 432 if g:gutentags_enabled && g:gutentags_generate_on_write
352 for module in g:gutentags_modules 433 for module in g:gutentags_modules
353 call s:update_tags(a:bufno, module, 0, 2) 434 call s:update_tags(a:bufno, module, 0, 2)
354 endfor 435 endfor
355 endif 436 endif
356 silent doautocmd User GutentagsUpdated 437 silent doautocmd User GutentagsUpdating
357 endfunction 438 endfunction
358 439
359 " Update the tags file for the current buffer's file. 440 " Update the tags file for the current buffer's file.
360 " write_mode: 441 " write_mode:
361 " 0: update the tags file if it exists, generate it otherwise. 442 " 0: update the tags file if it exists, generate it otherwise.
370 let l:buf_gutentags_files = getbufvar(a:bufno, 'gutentags_files') 451 let l:buf_gutentags_files = getbufvar(a:bufno, 'gutentags_files')
371 let l:tags_file = l:buf_gutentags_files[a:module] 452 let l:tags_file = l:buf_gutentags_files[a:module]
372 let l:proj_dir = getbufvar(a:bufno, 'gutentags_root') 453 let l:proj_dir = getbufvar(a:bufno, 'gutentags_root')
373 454
374 " Check that there's not already an update in progress. 455 " Check that there's not already an update in progress.
375 let l:lock_file = l:tags_file . '.lock' 456 let l:in_progress_idx = gutentags#find_job_index_by_tags_file(
376 if filereadable(l:lock_file) 457 \a:module, l:tags_file)
458 if l:in_progress_idx >= 0
377 if a:queue_mode == 2 459 if a:queue_mode == 2
378 let l:idx = index(s:update_queue[a:module], l:tags_file) 460 let l:needs_queuing = 1
379 if l:idx < 0 461 for qu_info in s:update_queue[a:module]
380 call add(s:update_queue[a:module], l:tags_file) 462 if qu_info[0] == l:tags_file
463 let l:needs_queuing = 0
464 break
465 endif
466 endfor
467 if l:needs_queuing
468 call add(s:update_queue[a:module],
469 \[l:tags_file, a:bufno, a:write_mode])
381 endif 470 endif
382 call gutentags#trace("Tag file '" . l:tags_file . 471 call gutentags#trace("Tag file '" . l:tags_file .
383 \"' is already being updated. Queuing it up...") 472 \"' is already being updated. Queuing it up...")
384 elseif a:queue_mode == 1 473 elseif a:queue_mode == 1
385 call gutentags#trace("Tag file '" . l:tags_file . 474 call gutentags#trace("Tag file '" . l:tags_file .
386 \"' is already being updated. Skipping...") 475 \"' is already being updated. Skipping...")
387 elseif a:queue_mode == 0 476 elseif a:queue_mode == 0
388 echom "gutentags: The tags file is already being updated, " . 477 echom "gutentags: The tags file is already being updated, " .
389 \"please try again later." 478 \"please try again later."
390 else 479 else
391 call gutentags#throwerr("Unknown queue mode: " . a:queue_mode) 480 call gutentags#throw("Unknown queue mode: " . a:queue_mode)
392 endif 481 endif
482
483 " Don't update the tags right now.
393 return 484 return
394 endif 485 endif
395 486
396 " Switch to the project root to make the command line smaller, and make 487 " Switch to the project root to make the command line smaller, and make
397 " it possible to get the relative path of the filename to parse if we're 488 " it possible to get the relative path of the filename to parse if we're
398 " doing an incremental update. 489 " doing an incremental update.
399 let l:prev_cwd = getcwd() 490 let l:prev_cwd = getcwd()
400 call gutentags#chdir(fnameescape(l:proj_dir)) 491 call gutentags#chdir(fnameescape(l:proj_dir))
401 try 492 try
402 call call("gutentags#".a:module."#generate", 493 call call("gutentags#".a:module."#generate",
403 \[l:proj_dir, l:tags_file, a:write_mode]) 494 \[l:proj_dir, l:tags_file,
495 \ {
496 \ 'write_mode': a:write_mode,
497 \ }])
404 catch /^gutentags\:/ 498 catch /^gutentags\:/
405 echom "Error while generating ".a:module." file:" 499 echom "Error while generating ".a:module." file:"
406 echom v:exception 500 echom v:exception
407 finally 501 finally
408 " Restore the current directory... 502 " Restore the current directory...
426 if a:0 && a:1 520 if a:0 && a:1
427 let g:gutentags_trace = l:trace_backup 521 let g:gutentags_trace = l:trace_backup
428 endif 522 endif
429 endfunction 523 endfunction
430 524
431 function! gutentags#delete_lock_files() abort
432 if exists('b:gutentags_files')
433 for tagfile in values(b:gutentags_files)
434 silent call delete(tagfile.'.lock')
435 endfor
436 endif
437 endfunction
438
439 function! gutentags#toggletrace(...) 525 function! gutentags#toggletrace(...)
440 let g:gutentags_trace = !g:gutentags_trace 526 let g:gutentags_trace = !g:gutentags_trace
441 if a:0 > 0 527 if a:0 > 0
442 let g:gutentags_trace = a:1 528 let g:gutentags_trace = a:1
443 endif 529 endif
459 else 545 else
460 echom "gutentags: Now running gutentags for real." 546 echom "gutentags: Now running gutentags for real."
461 endif 547 endif
462 echom "" 548 echom ""
463 endfunction 549 endfunction
550
551 function! gutentags#default_io_cb(data) abort
552 call gutentags#trace(a:data)
553 endfunction
554
555 if has('nvim')
556 " Neovim job API.
557 function! s:nvim_job_exit_wrapper(real_cb, job, exit_code, event_type) abort
558 call call(a:real_cb, [a:job, a:exit_code])
559 endfunction
560
561 function! s:nvim_job_out_wrapper(real_cb, job, lines, event_type) abort
562 call call(a:real_cb, [a:lines])
563 endfunction
564
565 function! gutentags#build_default_job_options(module) abort
566 let l:job_opts = {
567 \'on_exit': function(
568 \ '<SID>nvim_job_exit_wrapper',
569 \ ['gutentags#'.a:module.'#on_job_exit']),
570 \'on_stdout': function(
571 \ '<SID>nvim_job_out_wrapper',
572 \ ['gutentags#default_io_cb']),
573 \'on_stderr': function(
574 \ '<SID>nvim_job_out_wrapper',
575 \ ['gutentags#default_io_cb'])
576 \}
577 return l:job_opts
578 endfunction
579
580 function! gutentags#start_job(cmd, opts) abort
581 return jobstart(a:cmd, a:opts)
582 endfunction
583 else
584 " Vim8 job API.
585 function! gutentags#build_default_job_options(module) abort
586 let l:job_opts = {
587 \'exit_cb': 'gutentags#'.a:module.'#on_job_exit'
588 \'out_cb': 'gutentags#default_io_cb',
589 \'err_cb': 'gutentags#default_io_cb'
590 \}
591 return l:job_opts
592 endfunction
593
594 function! gutentags#start_job(cmd, opts) abort
595 return job_start(a:cmd, a:opts)
596 endfunction
597 endif
464 598
465 function! gutentags#inprogress() 599 function! gutentags#inprogress()
466 echom "gutentags: generations in progress:" 600 echom "gutentags: generations in progress:"
467 for mod_name in keys(s:maybe_in_progress) 601 for mod_name in keys(s:maybe_in_progress)
468 for mib in keys(s:maybe_in_progress[mod_name]) 602 for mib in keys(s:maybe_in_progress[mod_name])
490 if !exists('b:gutentags_files') 624 if !exists('b:gutentags_files')
491 " This buffer doesn't have gutentags. 625 " This buffer doesn't have gutentags.
492 return '' 626 return ''
493 endif 627 endif
494 628
495 " Figure out what the user is customizing. 629 " Find any module that has a job in progress for any of this buffer's
630 " tags files.
631 let l:modules_in_progress = []
632 for [module, tags_file] in items(b:gutentags_files)
633 let l:jobidx = gutentags#find_job_index_by_tags_file(module, tags_file)
634 if l:jobidx >= 0
635 call add(l:modules_in_progress, module)
636 endif
637 endfor
638
639 " Did we find any module? If not, don't print anything.
640 if len(l:modules_in_progress) == 0
641 return ''
642 endif
643
644 " W00t, stuff is happening! Let's print what.
496 let l:gen_msg = 'TAGS' 645 let l:gen_msg = 'TAGS'
497 if a:0 > 0 646 if a:0 > 0
498 let l:gen_msg = a:1 647 let l:gen_msg = a:1
499 endif 648 endif
500
501 " To make this function as fast as possible, we first check whether the
502 " current buffer's tags file is 'maybe' being generated. This provides a
503 " nice and quick bail out for 99.9% of cases before we need to this the
504 " file-system to check the lock file.
505 let l:modules_in_progress = []
506 for module in keys(b:gutentags_files)
507 let l:abs_tag_file = fnamemodify(b:gutentags_files[module], ':p')
508 let l:progress_queue = s:maybe_in_progress[module]
509 let l:timestamp = get(l:progress_queue, l:abs_tag_file)
510 if l:timestamp == 0
511 continue
512 endif
513 " It's maybe generating! Check if the lock file is still there... but
514 " don't do it too soon after the script was originally launched, because
515 " there can be a race condition where we get here just before the script
516 " had a chance to write the lock file.
517 if (localtime() - l:timestamp) > 1 &&
518 \!filereadable(l:abs_tag_file . '.lock')
519 call remove(l:progress_queue, l:abs_tag_file)
520 continue
521 endif
522 call add(l:modules_in_progress, module)
523 endfor
524
525 if len(l:modules_in_progress) == 0
526 return ''
527 endif
528
529 " It's still there! So probably `ctags` is still running...
530 " (although there's a chance it crashed, or the script had a problem, and
531 " the lock file has been left behind... we could try and run some
532 " additional checks here to see if it's legitimately running, and
533 " otherwise delete the lock file... maybe in the future...)
534 let l:gen_msg .= '['.join(l:modules_in_progress, ',').']' 649 let l:gen_msg .= '['.join(l:modules_in_progress, ',').']'
535 return l:gen_msg 650 return l:gen_msg
536 endfunction 651 endfunction
537 652
538 " }}} 653 " }}}