changeset 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 b41385032f86
children 6e96ddda0fd3
files autoload/gutentags.vim autoload/gutentags/cscope.vim autoload/gutentags/ctags.vim autoload/gutentags/gtags_cscope.vim doc/gutentags.txt plat/unix/update_tags.sh plugin/gutentags.vim
diffstat 7 files changed, 358 insertions(+), 327 deletions(-) [+]
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
--- a/autoload/gutentags/cscope.vim	Sat Mar 31 18:35:46 2018 -0700
+++ b/autoload/gutentags/cscope.vim	Sat Mar 31 18:42:54 2018 -0700
@@ -40,64 +40,46 @@
     endif
 endfunction
 
-function! gutentags#cscope#command_terminated(job_id, data, event) abort
-    if a:data == 0
-        if index(s:added_dbs, self.db_file) < 0
-            call add(s:added_dbs, self.db_file)
-            silent! execute 'cs add ' . fnameescape(s:db_file)
-        else
-            execute 'cs reset'
-        endif
-    endif
-endfunction
-
-function! gutentags#cscope#generate(proj_dir, tags_file, write_mode) abort
-    let l:cmd = gutentags#get_execute_cmd() . s:runner_exe
-    let l:cmd .= ' -e ' . g:gutentags_cscope_executable
-    let l:cmd .= ' -p ' . a:proj_dir
-    let l:cmd .= ' -f ' . a:tags_file
+function! gutentags#cscope#generate(proj_dir, tags_file, gen_opts) abort
+    let l:cmd = [s:runner_exe]
+    let l:cmd += ['-e', g:gutentags_cscope_executable]
+    let l:cmd += ['-p', a:proj_dir]
+    let l:cmd += ['-f', a:tags_file]
     let l:file_list_cmd =
         \ gutentags#get_project_file_list_cmd(a:proj_dir)
     if !empty(l:file_list_cmd)
-        let l:cmd .= ' -L "' . l:file_list_cmd . '"'
+        let l:cmd += ['-L', '"' . l:file_list_cmd . '"']
     endif
-    if g:gutentags_trace
-        if has('win32')
-            let l:cmd .= ' -l "' . a:tags_file . '.log"'
-        else
-            let l:cmd .= ' ' . printf(s:unix_redir, '"' . a:tags_file . '.log"')
-        endif
-    else
-        if !has('win32')
-            let l:cmd .= ' ' . printf(s:unix_redir, '/dev/null')
-        endif
-    endif
-    let l:cmd .= ' '
-    let l:cmd .= gutentags#get_execute_cmd_suffix()
+    let l:cmd = gutentags#make_args(l:cmd)
 
-    call gutentags#trace("Running: " . l:cmd)
+    call gutentags#trace("Running: " . string(l:cmd))
     call gutentags#trace("In:      " . getcwd())
     if !g:gutentags_fake
-        if !(has('nvim') && exists('*jobwait'))
-            if !g:gutentags_trace
-                silent execute l:cmd
-            else
-                execute l:cmd
-            endif
-        else
-            let job_dict = { 'db_file': a:tags_file, 'on_exit' : function('gutentags#cscope#command_terminated') }
-            let job_cmd = [ s:runner_exe,
-                        \ '-e', g:gutentags_cscope_executable,
-                        \ '-p', a:proj_dir,
-                        \ '-f', a:tags_file ]
-            let job_id = jobstart(job_cmd, job_dict)
-        endif
-
-        call gutentags#add_progress('cscope', a:tags_file)
+		let l:job_opts = gutentags#build_default_job_options('cscope')
+        let l:job = gutentags#start_job(l:cmd, l:job_opts)
+        call gutentags#add_job('cscope', a:tags_file, l:job)
     else
         call gutentags#trace("(fake... not actually running)")
     endif
-    call gutentags#trace("")
+endfunction
+
+function! gutentags#cscope#on_job_exit(job, exit_val) abort
+    let l:job_idx = gutentags#find_job_index_by_data('cscope', a:job)
+    let l:dbfile_path = gutentags#get_job_tags_file('cscope', l:job_idx)
+    call gutentags#remove_job('cscope', l:job_idx)
+
+    if a:exit_val == 0
+        if index(s:added_dbs, l:dbfile_path) < 0
+            call add(s:added_dbs, l:dbfile_path)
+            silent! execute 'cs add ' . fnameescape(l:dbfile_path)
+        else
+            execute 'cs reset'
+        endif
+    else
+        call gutentags#warning(
+                    \"gutentags: cscope job failed, returned: ".
+                    \string(a:exit_val))
+    endif
 endfunction
 
 " }}}
--- a/autoload/gutentags/ctags.vim	Sat Mar 31 18:35:46 2018 -0700
+++ b/autoload/gutentags/ctags.vim	Sat Mar 31 18:42:54 2018 -0700
@@ -66,7 +66,9 @@
     endif
 endfunction
 
-function! gutentags#ctags#generate(proj_dir, tags_file, write_mode) abort
+function! gutentags#ctags#generate(proj_dir, tags_file, gen_opts) abort
+    let l:write_mode = a:gen_opts['write_mode']
+
     let l:tags_file_exists = filereadable(a:tags_file)
     let l:tags_file_relative = fnamemodify(a:tags_file, ':.')
     let l:tags_file_is_local = len(l:tags_file_relative) < len(a:tags_file)
@@ -74,7 +76,7 @@
     if l:tags_file_exists && g:gutentags_ctags_check_tagfile
         let l:first_lines = readfile(a:tags_file, '', 1)
         if len(l:first_lines) == 0 || stridx(l:first_lines[0], '!_TAG_') != 0
-            call gutentags#throwerr(
+            call gutentags#throw(
                         \"File ".a:tags_file." doesn't appear to be ".
                         \"a ctags file. Please delete it and run ".
                         \":GutentagsUpdate!.")
@@ -101,16 +103,16 @@
     endif
 
     " Build the command line.
-    let l:cmd = gutentags#get_execute_cmd() . s:runner_exe
-    let l:cmd .= ' -e "' . s:get_ctags_executable(a:proj_dir) . '"'
-    let l:cmd .= ' -t "' . l:actual_tags_file . '"'
-    let l:cmd .= ' -p "' . l:actual_proj_dir . '"'
-    if a:write_mode == 0 && l:tags_file_exists
+    let l:cmd = [s:runner_exe]
+    let l:cmd += ['-e', '"' . s:get_ctags_executable(a:proj_dir) . '"']
+    let l:cmd += ['-t', '"' . l:actual_tags_file . '"']
+    let l:cmd += ['-p', '"' . l:actual_proj_dir . '"']
+    if l:write_mode == 0 && l:tags_file_exists
         let l:cur_file_path = expand('%:p')
         if empty(g:gutentags_cache_dir) && l:tags_file_is_local
             let l:cur_file_path = fnamemodify(l:cur_file_path, ':.')
         endif
-        let l:cmd .= ' -s "' . l:cur_file_path . '"'
+        let l:cmd += ['-s', '"' . l:cur_file_path . '"']
     else
         let l:file_list_cmd = gutentags#get_project_file_list_cmd(l:actual_proj_dir)
         if !empty(l:file_list_cmd)
@@ -119,71 +121,65 @@
                 let l:suffoptstr = l:suffopts[1]
                 let l:file_list_cmd = l:suffopts[0]
                 if l:suffoptstr == 'absolute'
-                    let l:cmd .= ' -A'
+                    let l:cmd += ['-A']
                 endif
             endif
-            let l:cmd .= ' -L ' . '"' . l:file_list_cmd. '"'
+            let l:cmd += ['-L', '"' . l:file_list_cmd. '"']
         endif
     endif
     if empty(get(l:, 'file_list_cmd', ''))
         " Pass the Gutentags recursive options file before the project
         " options file, so that users can override --recursive.
         " Omit --recursive if this project uses a file list command.
-        let l:cmd .= ' -o "' . gutentags#get_res_file('ctags_recursive.options') . '"'
+        let l:cmd += ['-o', '"' . gutentags#get_res_file('ctags_recursive.options') . '"']
     endif
     if !empty(g:gutentags_ctags_extra_args)
-        let l:cmd .= ' -O '.shellescape(join(g:gutentags_ctags_extra_args))
+        let l:cmd += ['-O', shellescape(join(g:gutentags_ctags_extra_args))]
     endif
     if !empty(g:gutentags_ctags_post_process_cmd)
-        let l:cmd .= ' -P '.shellescape(g:gutentags_ctags_post_process_cmd)
+        let l:cmd += ['-P', shellescape(g:gutentags_ctags_post_process_cmd)]
     endif
     let l:proj_options_file = a:proj_dir . '/' .
                 \g:gutentags_ctags_options_file
     if filereadable(l:proj_options_file)
         let l:proj_options_file = s:process_options_file(
                     \a:proj_dir, l:proj_options_file)
-        let l:cmd .= ' -o "' . l:proj_options_file . '"'
+        let l:cmd += ['-o', '"' . l:proj_options_file . '"']
     endif
     if g:gutentags_ctags_exclude_wildignore
         for ign in split(&wildignore, ',')
-            let l:cmd .= ' -x ' . shellescape(ign, 1)
+            let l:cmd += ['-x', shellescape(ign, 1)]
         endfor
     endif
     for exc in g:gutentags_ctags_exclude
-        let l:cmd .= ' -x ' . '"' . exc . '"'
+        let l:cmd += ['-x', '"' . exc . '"']
     endfor
     if g:gutentags_pause_after_update
-        let l:cmd .= ' -c'
+        let l:cmd += ['-c']
     endif
     if g:gutentags_trace
-        if has('win32')
-            let l:cmd .= ' -l "' . l:actual_tags_file . '.log"'
-        else
-            let l:cmd .= ' ' . printf(s:unix_redir, '"' . l:actual_tags_file . '.log"')
-        endif
-    else
-        if !has('win32')
-            let l:cmd .= ' ' . printf(s:unix_redir, '/dev/null')
-        endif
+        let l:cmd += ['-l', '"' . l:actual_tags_file . '.log"']
     endif
-    let l:cmd .= gutentags#get_execute_cmd_suffix()
+    let l:cmd = gutentags#make_args(l:cmd)
 
-    call gutentags#trace("Running: " . l:cmd)
+    call gutentags#trace("Running: " . string(l:cmd))
     call gutentags#trace("In:      " . getcwd())
     if !g:gutentags_fake
-        " Run the background process.
-        if !g:gutentags_trace
-            silent execute l:cmd
-        else
-            execute l:cmd
-        endif
-
-        " Flag this tags file as being in progress
-        call gutentags#add_progress('ctags', a:tags_file)
+		let l:job_opts = gutentags#build_default_job_options('ctags')
+        let l:job = gutentags#start_job(l:cmd, l:job_opts)
+        call gutentags#add_job('ctags', a:tags_file, l:job)
     else
         call gutentags#trace("(fake... not actually running)")
     endif
-    call gutentags#trace("")
+endfunction
+
+function! gutentags#ctags#on_job_exit(job, exit_val) abort
+    call gutentags#remove_job_by_data('ctags', a:job)
+
+    if a:exit_val != 0
+        call gutentags#warning("gutentags: ctags job failed, returned: ".
+                    \string(a:exit_val))
+    endif
 endfunction
 
 " }}}
--- a/autoload/gutentags/gtags_cscope.vim	Sat Mar 31 18:35:46 2018 -0700
+++ b/autoload/gutentags/gtags_cscope.vim	Sat Mar 31 18:42:54 2018 -0700
@@ -32,7 +32,6 @@
 " Gutentags Module Interface {{{
 
 let s:added_db_files = {}
-let s:job_db_files = []
 
 function! s:add_db(db_file) abort
 	if filereadable(a:db_file)
@@ -70,118 +69,54 @@
 	let $GTAGSDBPATH = l:db_path
 	let $GTAGSROOT = a:project_root
 
-	if g:gutentags_auto_add_gtags_cscope && !has_key(s:added_db_files, l:db_file)
+	if g:gutentags_auto_add_gtags_cscope && 
+				\!has_key(s:added_db_files, l:db_file)
 		let s:added_db_files[l:db_file] = 0
 		call s:add_db(l:db_file)
 	endif
 endfunction
 
-function! gutentags#gtags_cscope#on_job_out(job, data) abort
-	call gutentags#trace(a:data)
-endfunction
-
-function! gutentags#gtags_cscope#on_job_exit(job, exit_val) abort
-	if a:exit_val != 0
-		echom "gutentags: gtags-cscope job failed :("
-		return
-	endif
-	if g:gutentags_auto_add_gtags_cscope
-		let l:idx = 0
-		let l:db_file = ''
-		for item in s:job_db_files
-			if item[0] == a:job
-				let l:db_file = item[1]
-				break
-			endif
-			let l:idx += 1
-		endfor
-		if l:db_file != ''
-			call s:add_db(l:db_file)
-			call remove(s:job_db_files, l:idx)
-		endif
-	endif
-endfunction
-
-function! s:get_unix_cmd(for_job, proj_options, db_path) abort
-	" Vim's `job_start` gets confused with quoted arguments on Unix,
-	" prefers lists.
-	if a:for_job
-		let l:cmd = [g:gutentags_gtags_executable] + a:proj_options
-		let l:cmd += ['--incremental', a:db_path]
-		return l:cmd
-	else
-		let l:cmd = gutentags#get_execute_cmd()
-		let l:cmd .= '"' . g:gutentags_gtags_executable . '"'
-		let l:cmd .= ' ' . join(a:proj_options, ' ')
-		let l:cmd .= ' --incremental '
-		let l:cmd .= ' "' . a:db_path . '" '
-		let l:cmd .= gutentags#get_execute_cmd_suffix()
-		return l:cmd
-	endif
-endfunction
-
-function! s:get_win32_cmd(for_job, proj_options, db_path) abort
-	" Win32 prefers strings either way.
-	let l:cmd = ''
-	if !a:for_job
-		let l:cmd = gutentags#get_execute_cmd()
-	endif
-	let l:cmd .= '"' . g:gutentags_gtags_executable . '"'
-	let l:cmd .= ' ' . join(a:proj_options, ' ')
-	let l:cmd .= ' --incremental '
-	let l:cmd .= ' "' . a:db_path . '"'
-	if !a:for_job
-		let l:cmd .= ' '
-		let l:cmd .= gutentags#get_execute_cmd_suffix()
-	endif
-	return l:cmd
-endfunction
-
-function! gutentags#gtags_cscope#generate(proj_dir, db_file, write_mode) abort
+function! gutentags#gtags_cscope#generate(proj_dir, tags_file, gen_opts) abort
 	" gtags doesn't honour GTAGSDBPATH and GTAGSROOT, so PWD and dbpath
 	" have to be set
-	let l:db_path = fnamemodify(a:db_file, ':p:h')
+	let l:db_path = fnamemodify(a:tags_file, ':p:h')
 
 	let l:proj_options_file = a:proj_dir . '/' . g:gutentags_gtags_options_file
-	let l:proj_options = []
+
+	let l:cmd = ['"'.g:gutentags_gtags_executable.'"']
 	if filereadable(l:proj_options_file)
 		let l:proj_options = readfile(l:proj_options_file)
+		let l:cmd += l:proj_options
 	endif
-
-	let l:use_jobs = has('job')
-	if has('win32')
-		let l:cmd = s:get_win32_cmd(l:use_jobs, l:proj_options, l:db_path)
-	else
-		let l:cmd = s:get_unix_cmd(l:use_jobs, l:proj_options, l:db_path)
-	endif
+	let l:cmd += ['--incremental', '"'.l:db_path.'"']
+    let l:cmd = gutentags#make_args(l:cmd)
 
 	call gutentags#trace("Running: " . string(l:cmd))
 	call gutentags#trace("In:      " . getcwd())
 	if !g:gutentags_fake
-		if l:use_jobs
-			let l:job_opts = {
-						\'exit_cb': 'gutentags#gtags_cscope#on_job_exit',
-						\'out_cb': 'gutentags#gtags_cscope#on_job_out',
-						\'err_cb': 'gutentags#gtags_cscope#on_job_out'
-						\}
-			let l:job = job_start(l:cmd, job_opts)
-			call add(s:job_db_files, [l:job, a:db_file])
-		else
-			if !g:gutentags_trace
-				silent execute l:cmd
-			else
-				execute l:cmd
-			endif
-			if g:gutentags_auto_add_gtags_cscope
-				call s:add_db(a:db_file)
-			endif
-		endif
-
-		call gutentags#add_progress('gtags_cscope', l:db_path)
+		let l:job_opts = gutentags#build_default_job_options('gtags_cscope')
+		let l:job = gutentags#start_job(l:cmd, l:job_opts)
+        call gutentags#add_job('gtags_cscope', a:tags_file, l:job)
 	else
 		call gutentags#trace("(fake... not actually running)")
 	endif
 	call gutentags#trace("")
 endfunction
 
+function! gutentags#gtags_cscope#on_job_exit(job, exit_val) abort
+    let l:job_idx = gutentags#find_job_index_by_data('gtags_cscope', a:job)
+    let l:dbfile_path = gutentags#get_job_tags_file('gtags_cscope', l:job_idx)
+    call gutentags#remove_job('gtags_cscope', l:job_idx)
+
+	if g:gutentags_auto_add_gtags_cscope
+		call s:add_db(l:dbfile_path)
+	endif
+
+	if a:exit_val != 0
+        call gutentags#warning(
+					\"gutentags: gtags-cscope job failed, returned: ".
+                    \string(a:exit_val))
+	endif
+endfunction
+
 " }}}
--- a/doc/gutentags.txt	Sat Mar 31 18:35:46 2018 -0700
+++ b/doc/gutentags.txt	Sat Mar 31 18:42:54 2018 -0700
@@ -172,26 +172,18 @@
                         Gutentags redirect the output of the tag generation
                         script to a `.log` file in the project root.
 
-                                                *:GutentagsUnlock*
-:GutentagsUnlock
-                        Gutentags uses a `.lock` file to know when tag
-                        generation is running. If something goes wrong with
-                        that process, that lock file could be left behind. You
-                        could just remove it manually from the root of your
-                        project, but you can also run |:GutentagsUnlock| so
-                        that Vim does it for you.
-                        If you find that you need to use this more than a
-                        couple times ever, there's probably a bug with
-                        Gutentags, or something otherwise wrong or unexpected
-                        with your system. Please file a bug.
-
 
 Gutentags also has some user auto-commands (see |User| and |:doautocmd|):
 
+                                                *GutentagsUpdating*
+GutentagsUpdating
+                        This auto-command is triggered when a background
+                        update job has started.
+
                                                 *GutentagsUpdated*
 GutentagsUpdated
                         This auto-command is triggered when a background
-                        update job has been started.
+                        update job has finished.
 
 
 =============================================================================
@@ -206,17 +198,18 @@
 with the following function: >
         :set statusline+=%{gutentags#statusline()}
 
-This won't print anything unless Gutentags figures that `ctags` is running in
-the background. This is done by checking a `.lock` file next to the tag file,
-but there's also some optimization before that to not slow down Vim. Note that
-the `.lock` file can sometimes be left around incorrectly by the background
-process, and you may need to clean it up. See |:GutentagsUnlock|.
+Because Gutentags runs the tag generation in the background, the statusline
+indicator might stay there even after the background process has ended. It
+would only go away when Vim decides to refresh the statusline. You can force
+refresh it in a callback on |GutentagsUpdating| and |GutentagsUpdated|.
 
-When Gutentags thinks `ctags` is still running, it will print the string
-"TAGS" by default. You can customize it: >
-        :set statusline+=%{gutentags#statusline('[Generating...]')}
+For instance, with the `lightline` plugin:
 
-This will print the string "[Generating...]" when tags are being generated.
+        augroup MyGutentagsStatusLineRefresher
+            autocmd!
+            autocmd User GutentagsUpdating call lightline#update()
+            autocmd User GutentagsUpdated call lightline#update()
+        augroup END
 
 
 =============================================================================
--- a/plat/unix/update_tags.sh	Sat Mar 31 18:35:46 2018 -0700
+++ b/plat/unix/update_tags.sh	Sat Mar 31 18:42:54 2018 -0700
@@ -7,6 +7,7 @@
 CTAGS_ARGS=
 TAGS_FILE=tags
 PROJECT_ROOT=
+LOG_FILE=
 FILE_LIST_CMD=
 FILE_LIST_CMD_IS_ABSOLUTE=0
 UPDATED_SOURCE=
@@ -21,6 +22,7 @@
     echo "    -e [exe=ctags]: The ctags executable to run"
     echo "    -t [file=tags]: The path to the ctags file to update"
     echo "    -p [dir=]:      The path to the project root"
+    echo "    -l [file=]:     The path to a log file"
     echo "    -L [cmd=]:      The file list command to run"
     echo "    -A:             Specifies that the file list command returns "
     echo "                    absolute paths"
@@ -34,7 +36,7 @@
 }
 
 
-while getopts "h?e:x:t:p:L:s:o:O:P:cA" opt; do
+while getopts "h?e:x:t:p:l:L:s:o:O:P:cA" opt; do
     case $opt in
         h|\?)
             ShowUsage
@@ -52,6 +54,9 @@
         p)
             PROJECT_ROOT=$OPTARG
             ;;
+        l)
+            LOG_FILE=$OPTARG
+            ;;
         L)
             FILE_LIST_CMD=$OPTARG
             ;;
--- a/plugin/gutentags.vim	Sat Mar 31 18:35:46 2018 -0700
+++ b/plugin/gutentags.vim	Sat Mar 31 18:42:54 2018 -0700
@@ -1,6 +1,6 @@
 " gutentags.vim - Automatic ctags management for Vim
 " Maintainer:   Ludovic Chabant <http://ludovic.chabant.com>
-" Version:      0.0.1
+" Version:      2.0.0
 
 " Globals {{{
 
@@ -8,8 +8,13 @@
     finish
 endif
 
-if v:version < 704
-    echoerr "gutentags: this plugin requires vim >= 7.4."
+if v:version < 800
+    echoerr "gutentags: this plugin requires vim >= 8.0."
+    finish
+endif
+
+if !(has('job') || (has('nvim') && exists('*jobwait')))
+    echoerr "gutentags: this plugin requires the job API from Vim8 or Neovim."
     finish
 endif
 
@@ -53,6 +58,8 @@
 let g:gutentags_generate_on_empty_buffer = get(g:, 'gutentags_generate_on_empty_buffer', 0)
 let g:gutentags_file_list_command = get(g:, 'gutentags_file_list_command', '')
 
+let g:gutentags_use_jobs = get(g:, 'gutentags_use_jobs', has('job'))
+
 if !exists('g:gutentags_cache_dir')
     let g:gutentags_cache_dir = ''
 elseif !empty(g:gutentags_cache_dir)
@@ -92,8 +99,6 @@
 
 " Toggles and Miscellaneous Commands {{{
 
-command! GutentagsUnlock :call gutentags#delete_lock_files()
-
 if g:gutentags_define_advanced_commands
     command! GutentagsToggleEnabled :let g:gutentags_enabled=!g:gutentags_enabled
     command! GutentagsToggleTrace   :call gutentags#toggletrace()