view autoload/gutentags.vim @ 251:e61d20280c6c

Fix some more spaces-in-paths issues. When using the tags cache directory, the project root is passed to ctags. Escaping this path didn't work correctly when it has spaces: - We remove the quotes around it on *nix because `job_start()` doesn't like those, but then we need to escape the spaces with backslashes otherwise the script doesn't understand those parameters. - Once the escaping is gone in the script, we need to quote them but it looks like sh doesn't like double quotes in the middle of an env var or something, so we need to put the project root in a separate env var.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 25 Oct 2019 23:52:12 -0700
parents d264db0126c2
children 52be4cf89810
line wrap: on
line source

" gutentags.vim - Automatic ctags management for Vim

" 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
endfunction

" Throw an exception message.
function! gutentags#throw(message)
    throw "gutentags: " . a:message
endfunction

" Show an error message.
function! gutentags#error(message)
    let v:errmsg = "gutentags: " . a:message
    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.
function! gutentags#trace(message, ...)
    if g:gutentags_trace || (a:0 && a:1)
        let l:message = "gutentags: " . a:message
        echom l:message
    endif
endfunction

" Strips the ending slash in a path.
function! gutentags#stripslash(path)
    return fnamemodify(a:path, ':s?[/\\]$??')
endfunction

" Normalizes the slashes in a path.
function! gutentags#normalizepath(path)
    if exists('+shellslash') && &shellslash
        return substitute(a:path, '\v/', '\\', 'g')
    elseif has('win32')
        return substitute(a:path, '\v/', '\\', 'g')
    else
        return a:path
    endif
endfunction

" 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
endfunction

" Gets a file path in the correct `plat` folder.
function! gutentags#get_plat_file(filename) abort
    return g:gutentags_plat_dir . a:filename . g:gutentags_script_ext
endfunction

" Gets a file path in the resource folder.
function! gutentags#get_res_file(filename) abort
    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, '/\-', '/', '')
        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] == "'")
                " This was quoted, so there are probably things to escape.
                let l:escapedarg = cmdarg[1:-2] " substitute(cmdarg[1:-2], '\ ', '\\ ', 'g')
                call add(l:outcmd, l:escapedarg)
            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
else
    function! gutentags#is_path_rooted(path) abort
        return !empty(a:path) && a:path[0] == '/'
    endfunction
endif

" }}}

" Gutentags Setup {{{

let s:known_files = []
let s:known_projects = {}

function! s:cache_project_root(path) abort
    let l:result = {}

    for proj_info in g:gutentags_project_info
        let l:filematch = get(proj_info, 'file', '')
        if l:filematch != '' && filereadable(a:path . '/'. l:filematch)
            let l:result = copy(proj_info)
            break
        endif

        let l:globmatch = get(proj_info, 'glob', '')
        if l:globmatch != '' && glob(a:path . '/' . l:globmatch) != ''
            let l:result = copy(proj_info)
            break
        endif
    endfor

    let s:known_projects[a:path] = l:result
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)
    elseif type(g:gutentags_file_list_command) == type({})
        let l:markers = get(g:gutentags_file_list_command, 'markers', [])
        if type(l:markers) == type({})
            for [marker, file_list_cmd] in items(l:markers)
                if !empty(globpath(a:path, marker, 1))
                    return gutentags#validate_cmd(file_list_cmd)
                endif
            endfor
        endif
        return get(g:gutentags_file_list_command, 'default', "")
    endif
    return ""
endfunction

" Finds the first directory with a project marker by walking up from the given
" file path.
function! gutentags#get_project_root(path) abort
    if g:gutentags_project_root_finder != ''
        return call(g:gutentags_project_root_finder, [a:path])
    endif
    return gutentags#default_get_project_root(a:path)
endfunction

" Default implementation for finding project markers... useful when a custom
" finder (`g:gutentags_project_root_finder`) wants to fallback to the default
" behaviour.
function! gutentags#default_get_project_root(path) abort
    let l:path = gutentags#stripslash(a:path)
    let l:previous_path = ""
    let l:markers = g:gutentags_project_root[:]
    if g:gutentags_add_ctrlp_root_markers && exists('g:ctrlp_root_markers')
        for crm in g:ctrlp_root_markers
            if index(l:markers, crm) < 0
                call add(l:markers, crm)
            endif
        endfor
    endif
    while l:path != l:previous_path
        for root in l:markers
            if !empty(globpath(l:path, root, 1))
                let l:proj_dir = simplify(fnamemodify(l:path, ':p'))
                let l:proj_dir = gutentags#stripslash(l:proj_dir)
                if l:proj_dir == ''
                    call gutentags#trace("Found project marker '" . root .
                                \"' at the root of your file-system! " .
                                \" That's probably wrong, disabling " .
                                \"gutentags for this file...",
                                \1)
                    call gutentags#throw("Marker found at root, aborting.")
                endif
                for ign in g:gutentags_exclude_project_root
                    if l:proj_dir == ign
                        call gutentags#trace(
                                    \"Ignoring project root '" . l:proj_dir .
                                    \"' because it is in the list of ignored" .
                                    \" projects.")
                        call gutentags#throw("Ignore project: " . l:proj_dir)
                    endif
                endfor
                return l:proj_dir
            endif
        endfor
        let l:previous_path = l:path
        let l:path = fnamemodify(l:path, ':h')
    endwhile
    call gutentags#throw("Can't figure out what tag file to use for: " . a:path)
endfunction

" Get info on the project we're inside of.
function! gutentags#get_project_info(path) abort
    return get(s:known_projects, a:path, {})
endfunction

" Setup gutentags for the current buffer.
function! gutentags#setup_gutentags() abort
    if exists('b:gutentags_files') && !g:gutentags_debug
        " This buffer already has gutentags support.
        return
    endif

    " Don't setup gutentags for anything that's not a normal buffer
    " (so don't do anything for help buffers and quickfix windows and
    "  other such things)
    " Also don't do anything for the default `[No Name]` buffer you get
    " after starting Vim.
    if &buftype != '' || 
          \(bufname('%') == '' && !g:gutentags_generate_on_empty_buffer)
        return
    endif

    " Don't setup gutentags for things that don't need it, or that could
    " cause problems.
    if index(g:gutentags_exclude_filetypes, &filetype) >= 0
        return
    endif

    " Let the user specify custom ways to disable Gutentags.
    if g:gutentags_init_user_func != '' &&
                \!call(g:gutentags_init_user_func, [expand('%:p')])
        call gutentags#trace("Ignoring '" . bufname('%') . "' because of " .
                    \"custom user function.")
        return
    endif

    " Try and find what tags file we should manage.
    call gutentags#trace("Scanning buffer '" . bufname('%') . "' for gutentags setup...")
    try
        let l:buf_dir = expand('%:p:h', 1)
        if g:gutentags_resolve_symlinks
            let l:buf_dir = fnamemodify(resolve(expand('%:p', 1)), ':p:h')
        endif
        if !exists('b:gutentags_root')
            let b:gutentags_root = gutentags#get_project_root(l:buf_dir)
        endif
        if !len(b:gutentags_root)
            call gutentags#trace("no valid project root.. no gutentags support.")
            return
        endif
        if filereadable(b:gutentags_root . '/.notags')
            call gutentags#trace("'.notags' file found... no gutentags support.")
            return
        endif

        if !has_key(s:known_projects, b:gutentags_root)
            call s:cache_project_root(b:gutentags_root)
        endif
        if g:gutentags_trace
            let l:projnfo = gutentags#get_project_info(b:gutentags_root)
            if l:projnfo != {}
                call gutentags#trace("Setting project type to ".l:projnfo['type'])
            else
                call gutentags#trace("No specific project type.")
            endif
        endif

        let b:gutentags_files = {}
        for module in g:gutentags_modules
            call call("gutentags#".module."#init", [b:gutentags_root])
        endfor
    catch /^gutentags\:/
        call gutentags#trace("No gutentags support for this buffer.")
        return
    endtry

    " We know what tags file to manage! Now set things up.
    call gutentags#trace("Setting gutentags for buffer '".bufname('%')."'")

    " Autocommands for updating the tags on save.
    " We need to pass the buffer number to the callback function in the rare
    " case that the current buffer is changed by another `BufWritePost`
    " callback. This will let us get that buffer's variables without causing
    " errors.
    let l:bn = bufnr('%')
    execute 'augroup gutentags_buffer_' . l:bn
    execute '  autocmd!'
    execute '  autocmd BufWritePost <buffer=' . l:bn . '> call s:write_triggered_update_tags(' . l:bn . ')'
    execute 'augroup end'

    " Miscellaneous commands.
    command! -buffer -bang GutentagsUpdate :call s:manual_update_tags(<bang>0)

    " Add these tags files to the known tags files.
    for module in keys(b:gutentags_files)
        let l:tagfile = b:gutentags_files[module]
        let l:found = index(s:known_files, l:tagfile)
        if l:found < 0
            call add(s:known_files, l:tagfile)

            " Generate this new file depending on settings and stuff.
            if g:gutentags_enabled
                if g:gutentags_generate_on_missing && !filereadable(l:tagfile)
                    call gutentags#trace("Generating missing tags file: " . l:tagfile)
                    call s:update_tags(l:bn, module, 1, 1)
                elseif g:gutentags_generate_on_new
                    call gutentags#trace("Generating tags file: " . l:tagfile)
                    call s:update_tags(l:bn, module, 1, 1)
                endif
            endif
        endif
    endfor
endfunction

" }}}

"  Job Management {{{

" List of queued-up jobs, and in-progress jobs, per module.
let s:update_queue = {}
let s:update_in_progress = {}
for module in g:gutentags_modules
    let s:update_queue[module] = []
    let s:update_in_progress[module] = []
endfor

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

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
    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
        call gutentags#trace("Finished ".a:module." job.")
    endif
endfunction

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:restore_prev_trace = 0
    let l:prev_trace = g:gutentags_trace
    if &verbose > 0
        let g:gutentags_trace = 1
        let l:restore_prev_trace = 1
    endif

    try
        let l:bn = bufnr('%')
        for module in g:gutentags_modules
            call s:update_tags(l:bn, module, a:bang, 0)
        endfor
        silent doautocmd User GutentagsUpdating
    finally
        if l:restore_prev_trace
            let g:gutentags_trace = l:prev_trace
        endif
    endtry
endfunction

" (Re)Generate the tags file for a buffer that just go saved.
function! s:write_triggered_update_tags(bufno) abort
    if g:gutentags_enabled && g:gutentags_generate_on_write
        for module in g:gutentags_modules
            call s:update_tags(a:bufno, module, 0, 2)
        endfor
    endif
    silent doautocmd User GutentagsUpdating
endfunction

" Update the tags file for the current buffer's file.
" write_mode:
"   0: update the tags file if it exists, generate it otherwise.
"   1: always generate (overwrite) the tags file.
"
" queue_mode:
"   0: if an update is already in progress, report it and abort.
"   1: if an update is already in progress, abort silently.
"   2: if an update is already in progress, queue another one.
function! s:update_tags(bufno, module, write_mode, queue_mode) abort
    " Figure out where to save.
    let l:buf_gutentags_files = getbufvar(a:bufno, 'gutentags_files')
    let l:tags_file = l:buf_gutentags_files[a:module]
    let l:proj_dir = getbufvar(a:bufno, 'gutentags_root')

    " Check that there's not already an update in progress.
    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: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...")
        elseif a:queue_mode == 1
            call gutentags#trace("Tag file '" . l:tags_file .
                        \"' is already being updated. Skipping...")
        elseif a:queue_mode == 0
            echom "gutentags: The tags file is already being updated, " .
                        \"please try again later."
        else
            call gutentags#throw("Unknown queue mode: " . a:queue_mode)
        endif

        " Don't update the tags right now.
        return
    endif

    " Switch to the project root to make the command line smaller, and make
    " it possible to get the relative path of the filename to parse if we're
    " doing an incremental update.
    let l:prev_cwd = getcwd()
    call gutentags#chdir(fnameescape(l:proj_dir))
    try
        call call("gutentags#".a:module."#generate",
                    \[l:proj_dir, l:tags_file,
                    \ {
                    \   'write_mode': a:write_mode,
                    \ }])
    catch /^gutentags\:/
        echom "Error while generating ".a:module." file:"
        echom v:exception
    finally
        " Restore the current directory...
        call gutentags#chdir(fnameescape(l:prev_cwd))
    endtry
endfunction

" }}}

" Utility Functions {{{

function! gutentags#rescan(...)
    if exists('b:gutentags_files')
        unlet b:gutentags_files
    endif
    if a:0 && a:1
        let l:trace_backup = g:gutentags_trace
        let l:gutentags_trace = 1
    endif
    call gutentags#setup_gutentags()
    if a:0 && a:1
        let g:gutentags_trace = l:trace_backup
    endif
endfunction

function! gutentags#toggletrace(...)
    let g:gutentags_trace = !g:gutentags_trace
    if a:0 > 0
        let g:gutentags_trace = a:1
    endif
    if g:gutentags_trace
        echom "gutentags: Tracing is enabled."
    else
        echom "gutentags: Tracing is disabled."
    endif
    echom ""
endfunction

function! gutentags#fake(...)
    let g:gutentags_fake = !g:gutentags_fake
    if a:0 > 0
        let g:gutentags_fake = a:1
    endif
    if g:gutentags_fake
        echom "gutentags: Now faking gutentags."
    else
        echom "gutentags: Now running gutentags for real."
    endif
    echom ""
endfunction

function! gutentags#default_stdout_cb(chan, msg) abort
    call gutentags#trace('[job stdout]: '.string(a:msg))
endfunction

function! gutentags#default_stderr_cb(chan, msg) abort
    call gutentags#trace('[job stderr]: '.string(a:msg))
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:job, a:lines])
    endfunction

    function! gutentags#build_default_job_options(module) abort
       " Neovim kills jobs on exit, which is what we want.
       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_stdout_cb']),
                \'on_stderr': function(
                \    '<SID>nvim_job_out_wrapper',
                \    ['gutentags#default_stderr_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_stdout_cb',
                 \'err_cb': 'gutentags#default_stderr_cb',
                 \'stoponexit': 'term'
                 \}
        return l:job_opts
    endfunction

    function! gutentags#start_job(cmd, opts) abort
        return job_start(a:cmd, a:opts)
    endfunction
endif

" Returns which modules are currently generating something for the
" current buffer.
function! gutentags#inprogress()
    " Does this buffer have gutentags enabled?
    if !exists('b:gutentags_files')
        return []
    endif

    " 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
    return l:modules_in_progress
endfunction

" }}}

" Statusline Functions {{{

" Prints whether a tag file is being generated right now for the current
" buffer in the status line.
"
" Arguments can be passed:
" - args 1 and 2 are the prefix and suffix, respectively, of whatever output,
"   if any, is going to be produced.
"   (defaults to empty strings)
" - arg 3 is the text to be shown if tags are currently being generated.
"   (defaults to the name(s) of the modules currently generating).

function! gutentags#statusline(...) abort
    let l:modules_in_progress = gutentags#inprogress()
    if empty(l:modules_in_progress)
       return ''
    endif

    let l:prefix = ''
    let l:suffix = ''
    if a:0 > 0
       let l:prefix = a:1
    endif
    if a:0 > 1
       let l:suffix = a:2
    endif

    if a:0 > 2
       let l:genmsg = a:3
    else
       let l:genmsg = join(l:modules_in_progress, ',')
    endif

    return l:prefix.l:genmsg.l:suffix
endfunction

" Same as `gutentags#statusline`, but the only parameter is a `Funcref` or
" function name that will get passed the list of modules currently generating
" something. This formatter function should return the string to display in
" the status line.

function! gutentags#statusline_cb(fmt_cb, ...) abort
    let l:modules_in_progress = gutentags#inprogress()

    if (a:0 == 0 || !a:1) && empty(l:modules_in_progress)
       return ''
    endif

    return call(a:fmt_cb, [l:modules_in_progress])
endfunction

" }}}