view autoload/gutentags.vim @ 262:dc9216fc0c0f

escape the file name in a right way, handle `tcd` in vim
author skywind3000 <skywind3000@163.com>
date Fri, 14 Feb 2020 13:12:34 +0800
parents a282d08e0e7c
children cbe7ffc327a4
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() ? ((haslocaldir() == 1) ? 'lcd' : 'tcd') : 'cd'
    endif
    execute chdir fnameescape(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

" Set a variable on exit so that we don't complain when a job gets killed.
function! gutentags#on_vim_leave_pre() abort
    let g:__gutentags_vim_is_leaving = 1
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

" }}}