123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705 |
- " 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] == "'")
- 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
- 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_io_cb(chan, msg) abort
- call gutentags#trace('[job output]: '.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_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',
- \'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
-
- " }}}
|