Mercurial > vim-gutentags
diff 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 |
line wrap: on
line diff
--- a/autoload/gutentags.vim Sat Mar 31 18:35:46 2018 -0700 +++ b/autoload/gutentags.vim Sat Mar 31 18:42:54 2018 -0700 @@ -3,12 +3,12 @@ " Utilities {{{ function! gutentags#chdir(path) - if has('nvim') - let chdir = haslocaldir() ? 'lcd' : haslocaldir(-1, 0) ? 'tcd' : 'cd' - else - let chdir = haslocaldir() ? 'lcd' : 'cd' - endif - execute chdir a:path + if has('nvim') + let chdir = haslocaldir() ? 'lcd' : haslocaldir(-1, 0) ? 'tcd' : 'cd' + else + let chdir = haslocaldir() ? 'lcd' : 'cd' + endif + execute chdir a:path endfunction " Throw an exception message. @@ -16,10 +16,17 @@ throw "gutentags: " . a:message endfunction -" Throw an exception message and set Vim's error message variable. -function! gutentags#throwerr(message) +" Show an error message. +function! gutentags#error(message) let v:errmsg = "gutentags: " . a:message - throw v:errmsg + echoerr v:errmsg +endfunction + +" Show a warning message. +function! gutentags#warning(message) + echohl WarningMsg + echom "gutentags: " . a:message + echohl None endfunction " Prints a message if debug tracing is enabled. @@ -48,11 +55,11 @@ " Shell-slashes the path (opposite of `normalizepath`). function! gutentags#shellslash(path) - if exists('+shellslash') && !&shellslash - return substitute(a:path, '\v\\', '/', 'g') - else - return a:path - endif + if exists('+shellslash') && !&shellslash + return substitute(a:path, '\v\\', '/', 'g') + else + return a:path + endif endfunction " Gets a file path in the correct `plat` folder. @@ -65,16 +72,66 @@ return g:gutentags_res_dir . a:filename endfunction +" Generate a path for a given filename in the cache directory. +function! gutentags#get_cachefile(root_dir, filename) abort + if gutentags#is_path_rooted(a:filename) + return a:filename + endif + let l:tag_path = gutentags#stripslash(a:root_dir) . '/' . a:filename + if g:gutentags_cache_dir != "" + " Put the tag file in the cache dir instead of inside the + " project root. + let l:tag_path = g:gutentags_cache_dir . '/' . + \tr(l:tag_path, '\/: ', '---_') + let l:tag_path = substitute(l:tag_path, '/\-', '/', '') + endif + let l:tag_path = gutentags#normalizepath(l:tag_path) + return l:tag_path +endfunction + +" Makes sure a given command starts with an executable that's in the PATH. +function! gutentags#validate_cmd(cmd) abort + if !empty(a:cmd) && executable(split(a:cmd)[0]) + return a:cmd + endif + return "" +endfunction + +" Makes an appropriate command line for use with `job_start` by converting +" a list of possibly quoted arguments into a single string on Windows, or +" into a list of unquoted arguments on Unix/Mac. +if has('win32') || has('win64') + function! gutentags#make_args(cmd) abort + return join(a:cmd, ' ') + endfunction +else + function! gutentags#make_args(cmd) abort + let l:outcmd = [] + for cmdarg in a:cmd + " Thanks Vimscript... you can use negative integers for strings + " in the slice notation, but not for indexing characters :( + let l:arglen = strlen(cmdarg) + if (cmdarg[0] == '"' && cmdarg[l:arglen - 1] == '"') || + \(cmdarg[0] == "'" && cmdarg[l:arglen - 1] == "'") + call add(l:outcmd, cmdarg[1:-2]) + else + call add(l:outcmd, cmdarg) + endif + endfor + return l:outcmd + endfunction +endif + " Returns whether a path is rooted. if has('win32') || has('win64') -function! gutentags#is_path_rooted(path) abort - return len(a:path) >= 2 && ( - \a:path[0] == '/' || a:path[0] == '\' || a:path[1] == ':') -endfunction + function! gutentags#is_path_rooted(path) abort + return len(a:path) >= 2 && ( + \a:path[0] == '/' || a:path[0] == '\' || a:path[1] == ':') + endfunction else -function! gutentags#is_path_rooted(path) abort - return !empty(a:path) && a:path[0] == '/' -endfunction + function! gutentags#is_path_rooted(path) abort + return !empty(a:path) && a:path[0] == '/' + endfunction endif " }}} @@ -104,13 +161,6 @@ let s:known_projects[a:path] = l:result endfunction -function! gutentags#validate_cmd(cmd) abort - if !empty(a:cmd) && executable(split(a:cmd)[0]) - return a:cmd - endif - return "" -endfunction - function! gutentags#get_project_file_list_cmd(path) abort if type(g:gutentags_file_list_command) == type("") return gutentags#validate_cmd(g:gutentags_file_list_command) @@ -181,23 +231,6 @@ return get(s:known_projects, a:path, {}) endfunction -" Generate a path for a given filename in the cache directory. -function! gutentags#get_cachefile(root_dir, filename) abort - if gutentags#is_path_rooted(a:filename) - return a:filename - endif - let l:tag_path = gutentags#stripslash(a:root_dir) . '/' . a:filename - if g:gutentags_cache_dir != "" - " Put the tag file in the cache dir instead of inside the - " project root. - let l:tag_path = g:gutentags_cache_dir . '/' . - \tr(l:tag_path, '\/: ', '---_') - let l:tag_path = substitute(l:tag_path, '/\-', '/', '') - endif - let l:tag_path = gutentags#normalizepath(l:tag_path) - return l:tag_path -endfunction - " Setup gutentags for the current buffer. function! gutentags#setup_gutentags() abort if exists('b:gutentags_files') && !g:gutentags_debug @@ -299,51 +332,99 @@ " }}} -" Tags File Management {{{ +" Job Management {{{ " List of queued-up jobs, and in-progress jobs, per module. let s:update_queue = {} -let s:maybe_in_progress = {} +let s:update_in_progress = {} for module in g:gutentags_modules let s:update_queue[module] = [] - let s:maybe_in_progress[module] = {} + let s:update_in_progress[module] = [] endfor -" Make a given file known as being currently generated or updated. -function! gutentags#add_progress(module, file) abort - let l:abs_file = fnamemodify(a:file, ':p') - let s:maybe_in_progress[a:module][l:abs_file] = localtime() +function! gutentags#add_job(module, tags_file, data) abort + call add(s:update_in_progress[a:module], [a:tags_file, a:data]) +endfunction + +function! gutentags#find_job_index_by_tags_file(module, tags_file) abort + let l:idx = -1 + for upd_info in s:update_in_progress[a:module] + let l:idx += 1 + if upd_info[0] == a:tags_file + return l:idx + endif + endfor + return -1 +endfunction + +function! gutentags#find_job_index_by_data(module, data) abort + let l:idx = -1 + for upd_info in s:update_in_progress[a:module] + let l:idx += 1 + if upd_info[1] == a:data + return l:idx + endif + endfor + return -1 +endfunction + +function! gutentags#get_job_tags_file(module, job_idx) abort + return s:update_in_progress[a:module][a:job_idx][0] endfunction -" Get how to execute an external command depending on debug settings. -function! gutentags#get_execute_cmd() abort - if has('win32') - let l:cmd = '!start ' - if g:gutentags_background_update - let l:cmd .= '/b ' +function! gutentags#get_job_data(module, job_idx) abort + return s:update_in_progress[a:module][a:job_idx][1] +endfunction + +function! gutentags#remove_job(module, job_idx) abort + let l:tags_file = s:update_in_progress[a:module][a:job_idx][0] + call remove(s:update_in_progress[a:module], a:job_idx) + + " Run the user callback for finished jobs. + silent doautocmd User GutentagsUpdated + + " See if we had any more updates queued up for this. + let l:qu_idx = -1 + for qu_info in s:update_queue[a:module] + let l:qu_idx += 1 + if qu_info[0] == l:tags_file + break endif - return l:cmd + endfor + if l:qu_idx >= 0 + let l:qu_info = s:update_queue[a:module][l:qu_idx] + call remove(s:update_queue[a:module], l:qu_idx) + + if bufexists(l:qu_info[1]) + call gutentags#trace("Finished ".a:module." job, ". + \"running queued update for '".l:tags_file."'.") + call s:update_tags(l:qu_info[1], a:module, l:qu_info[2], 2) + else + call gutentags#trace("Finished ".a:module." job, ". + \"but skipping queued update for '".l:tags_file."' ". + \"because originating buffer doesn't exist anymore.") + endif else - return '!' + call gutentags#trace("Finished ".a:module." job.") endif endfunction -" Get the suffix for how to execute an external command. -function! gutentags#get_execute_cmd_suffix() abort - if has('win32') - return '' - else - return ' &' - endif +function! gutentags#remove_job_by_data(module, data) abort + let l:idx = gutentags#find_job_index_by_data(a:module, a:data) + call gutentags#remove_job(a:module, l:idx) endfunction +" }}} + +" Tags File Management {{{ + " (Re)Generate the tags file for the current buffer's file. function! s:manual_update_tags(bang) abort let l:bn = bufnr('%') for module in g:gutentags_modules call s:update_tags(l:bn, module, a:bang, 0) endfor - silent doautocmd User GutentagsUpdated + silent doautocmd User GutentagsUpdating endfunction " (Re)Generate the tags file for a buffer that just go saved. @@ -353,7 +434,7 @@ call s:update_tags(a:bufno, module, 0, 2) endfor endif - silent doautocmd User GutentagsUpdated + silent doautocmd User GutentagsUpdating endfunction " Update the tags file for the current buffer's file. @@ -372,12 +453,20 @@ let l:proj_dir = getbufvar(a:bufno, 'gutentags_root') " Check that there's not already an update in progress. - let l:lock_file = l:tags_file . '.lock' - if filereadable(l:lock_file) + let l:in_progress_idx = gutentags#find_job_index_by_tags_file( + \a:module, l:tags_file) + if l:in_progress_idx >= 0 if a:queue_mode == 2 - let l:idx = index(s:update_queue[a:module], l:tags_file) - if l:idx < 0 - call add(s:update_queue[a:module], l:tags_file) + let l:needs_queuing = 1 + for qu_info in s:update_queue[a:module] + if qu_info[0] == l:tags_file + let l:needs_queuing = 0 + break + endif + endfor + if l:needs_queuing + call add(s:update_queue[a:module], + \[l:tags_file, a:bufno, a:write_mode]) endif call gutentags#trace("Tag file '" . l:tags_file . \"' is already being updated. Queuing it up...") @@ -388,8 +477,10 @@ echom "gutentags: The tags file is already being updated, " . \"please try again later." else - call gutentags#throwerr("Unknown queue mode: " . a:queue_mode) + call gutentags#throw("Unknown queue mode: " . a:queue_mode) endif + + " Don't update the tags right now. return endif @@ -400,7 +491,10 @@ call gutentags#chdir(fnameescape(l:proj_dir)) try call call("gutentags#".a:module."#generate", - \[l:proj_dir, l:tags_file, a:write_mode]) + \[l:proj_dir, l:tags_file, + \ { + \ 'write_mode': a:write_mode, + \ }]) catch /^gutentags\:/ echom "Error while generating ".a:module." file:" echom v:exception @@ -428,14 +522,6 @@ endif endfunction -function! gutentags#delete_lock_files() abort - if exists('b:gutentags_files') - for tagfile in values(b:gutentags_files) - silent call delete(tagfile.'.lock') - endfor - endif -endfunction - function! gutentags#toggletrace(...) let g:gutentags_trace = !g:gutentags_trace if a:0 > 0 @@ -462,6 +548,54 @@ echom "" endfunction +function! gutentags#default_io_cb(data) abort + call gutentags#trace(a:data) +endfunction + +if has('nvim') + " Neovim job API. + function! s:nvim_job_exit_wrapper(real_cb, job, exit_code, event_type) abort + call call(a:real_cb, [a:job, a:exit_code]) + endfunction + + function! s:nvim_job_out_wrapper(real_cb, job, lines, event_type) abort + call call(a:real_cb, [a:lines]) + endfunction + + function! gutentags#build_default_job_options(module) abort + let l:job_opts = { + \'on_exit': function( + \ '<SID>nvim_job_exit_wrapper', + \ ['gutentags#'.a:module.'#on_job_exit']), + \'on_stdout': function( + \ '<SID>nvim_job_out_wrapper', + \ ['gutentags#default_io_cb']), + \'on_stderr': function( + \ '<SID>nvim_job_out_wrapper', + \ ['gutentags#default_io_cb']) + \} + return l:job_opts + endfunction + + function! gutentags#start_job(cmd, opts) abort + return jobstart(a:cmd, a:opts) + endfunction +else + " Vim8 job API. + function! gutentags#build_default_job_options(module) abort + let l:job_opts = { + \'exit_cb': 'gutentags#'.a:module.'#on_job_exit' + \'out_cb': 'gutentags#default_io_cb', + \'err_cb': 'gutentags#default_io_cb' + \} + return l:job_opts + endfunction + + function! gutentags#start_job(cmd, opts) abort + return job_start(a:cmd, a:opts) + endfunction +endif + function! gutentags#inprogress() echom "gutentags: generations in progress:" for mod_name in keys(s:maybe_in_progress) @@ -492,45 +626,26 @@ return '' endif - " Figure out what the user is customizing. + " Find any module that has a job in progress for any of this buffer's + " tags files. + let l:modules_in_progress = [] + for [module, tags_file] in items(b:gutentags_files) + let l:jobidx = gutentags#find_job_index_by_tags_file(module, tags_file) + if l:jobidx >= 0 + call add(l:modules_in_progress, module) + endif + endfor + + " Did we find any module? If not, don't print anything. + if len(l:modules_in_progress) == 0 + return '' + endif + + " W00t, stuff is happening! Let's print what. let l:gen_msg = 'TAGS' if a:0 > 0 let l:gen_msg = a:1 endif - - " To make this function as fast as possible, we first check whether the - " current buffer's tags file is 'maybe' being generated. This provides a - " nice and quick bail out for 99.9% of cases before we need to this the - " file-system to check the lock file. - let l:modules_in_progress = [] - for module in keys(b:gutentags_files) - let l:abs_tag_file = fnamemodify(b:gutentags_files[module], ':p') - let l:progress_queue = s:maybe_in_progress[module] - let l:timestamp = get(l:progress_queue, l:abs_tag_file) - if l:timestamp == 0 - continue - endif - " It's maybe generating! Check if the lock file is still there... but - " don't do it too soon after the script was originally launched, because - " there can be a race condition where we get here just before the script - " had a chance to write the lock file. - if (localtime() - l:timestamp) > 1 && - \!filereadable(l:abs_tag_file . '.lock') - call remove(l:progress_queue, l:abs_tag_file) - continue - endif - call add(l:modules_in_progress, module) - endfor - - if len(l:modules_in_progress) == 0 - return '' - endif - - " It's still there! So probably `ctags` is still running... - " (although there's a chance it crashed, or the script had a problem, and - " the lock file has been left behind... we could try and run some - " additional checks here to see if it's legitimately running, and - " otherwise delete the lock file... maybe in the future...) let l:gen_msg .= '['.join(l:modules_in_progress, ',').']' return l:gen_msg endfunction