ctags.vim (14124B)
1 " Ctags module for Gutentags 2 3 " Global Options {{{ 4 5 let g:gutentags_ctags_executable = get(g:, 'gutentags_ctags_executable', 'ctags') 6 let g:gutentags_ctags_tagfile = get(g:, 'gutentags_ctags_tagfile', 'tags') 7 let g:gutentags_ctags_auto_set_tags = get(g:, 'gutentags_ctags_auto_set_tags', 1) 8 9 let g:gutentags_ctags_options_file = get(g:, 'gutentags_ctags_options_file', '.gutctags') 10 let g:gutentags_ctags_check_tagfile = get(g:, 'gutentags_ctags_check_tagfile', 0) 11 let g:gutentags_ctags_extra_args = get(g:, 'gutentags_ctags_extra_args', []) 12 let g:gutentags_ctags_post_process_cmd = get(g:, 'gutentags_ctags_post_process_cmd', '') 13 14 let g:gutentags_ctags_exclude = get(g:, 'gutentags_ctags_exclude', []) 15 let g:gutentags_ctags_exclude_wildignore = get(g:, 'gutentags_ctags_exclude_wildignore', 1) 16 17 " Backwards compatibility. 18 function! s:_handleOldOptions() abort 19 let l:renamed_options = { 20 \'gutentags_exclude': 'gutentags_ctags_exclude', 21 \'gutentags_tagfile': 'gutentags_ctags_tagfile', 22 \'gutentags_auto_set_tags': 'gutentags_ctags_auto_set_tags' 23 \} 24 for key in keys(l:renamed_options) 25 if exists('g:'.key) 26 let newname = l:renamed_options[key] 27 echom "gutentags: Option 'g:'".key." has been renamed to ". 28 \"'g:'".newname." Please update your vimrc." 29 let g:[newname] = g:[key] 30 endif 31 endfor 32 endfunction 33 call s:_handleOldOptions() 34 " }}} 35 36 " Gutentags Module Interface {{{ 37 38 let s:did_check_exe = 0 39 let s:runner_exe = '"' . gutentags#get_plat_file('update_tags') . '"' 40 let s:unix_redir = (&shellredir =~# '%s') ? &shellredir : &shellredir . ' %s' 41 let s:wildignores_options_path = '' 42 let s:last_wildignores = '' 43 44 function! gutentags#ctags#init(project_root) abort 45 " Figure out the path to the tags file. 46 " Check the old name for this option, too, before falling back to the 47 " globally defined name. 48 let l:tagfile = getbufvar("", 'gutentags_ctags_tagfile', 49 \getbufvar("", 'gutentags_tagfile', 50 \g:gutentags_ctags_tagfile)) 51 let b:gutentags_files['ctags'] = gutentags#get_cachefile( 52 \a:project_root, l:tagfile) 53 54 " Set the tags file for Vim to use. 55 if g:gutentags_ctags_auto_set_tags 56 if has('win32') || has('win64') 57 execute 'setlocal tags^=' . fnameescape(b:gutentags_files['ctags']) 58 else 59 " spaces must be literally escaped in tags path 60 let l:literal_space_escaped = substitute(fnameescape(b:gutentags_files['ctags']), '\ ', '\\\\ ', 'g') 61 execute 'setlocal tags^=' . l:literal_space_escaped 62 endif 63 endif 64 65 " Check if the ctags executable exists. 66 if s:did_check_exe == 0 67 if g:gutentags_enabled && executable(expand(g:gutentags_ctags_executable, 1)) == 0 68 let g:gutentags_enabled = 0 69 echoerr "Executable '".g:gutentags_ctags_executable."' can't be found. " 70 \."Gutentags will be disabled. You can re-enable it by " 71 \."setting g:gutentags_enabled back to 1." 72 endif 73 let s:did_check_exe = 1 74 endif 75 endfunction 76 77 function! gutentags#ctags#generate(proj_dir, tags_file, gen_opts) abort 78 let l:write_mode = a:gen_opts['write_mode'] 79 80 let l:tags_file_exists = filereadable(a:tags_file) 81 82 " If the tags file exists, we may want to do a sanity check to prevent 83 " weird errors that are hard to troubleshoot. 84 if l:tags_file_exists && g:gutentags_ctags_check_tagfile 85 let l:first_lines = readfile(a:tags_file, '', 1) 86 if len(l:first_lines) == 0 || stridx(l:first_lines[0], '!_TAG_') != 0 87 call gutentags#throw( 88 \"File ".a:tags_file." doesn't appear to be ". 89 \"a ctags file. Please delete it and run ". 90 \":GutentagsUpdate!.") 91 return 92 endif 93 endif 94 95 " Get a tags file path relative to the current directory, which 96 " happens to be the project root in this case. 97 " Since the given tags file path is absolute, and since Vim won't 98 " change the path if it is not inside the current directory, we 99 " know that the tags file is "local" (i.e. inside the project) 100 " if the path was shortened (an absolute path will always be 101 " longer than a true relative path). 102 let l:tags_file_relative = fnamemodify(a:tags_file, ':.') 103 let l:tags_file_is_local = len(l:tags_file_relative) < len(a:tags_file) 104 let l:use_tag_relative_opt = 0 105 106 if empty(g:gutentags_cache_dir) && l:tags_file_is_local 107 " If we don't use the cache directory, we can pass relative paths 108 " around. 109 " 110 " Note that if we don't do this and pass a full path for the project 111 " root, some `ctags` implementations like Exhuberant Ctags can get 112 " confused if the paths have spaces -- but not if you're *in* the root 113 " directory, for some reason... (which will be the case, we're running 114 " the jobs from the project root). 115 let l:actual_proj_dir = '.' 116 let l:actual_tags_file = l:tags_file_relative 117 118 let l:tags_file_dir = fnamemodify(l:actual_tags_file, ':h') 119 if l:tags_file_dir != '.' 120 " Ok so now the tags file is stored in a subdirectory of the 121 " project root, instead of at the root. This happens if, say, 122 " someone set `gutentags_ctags_tagfile` to `.git/tags`, which 123 " seems to be fairly popular. 124 " 125 " By default, `ctags` writes paths relative to the current 126 " directory (the project root) but in this case we need it to 127 " be relative to the tags file (e.g. adding `../` in front of 128 " everything if the tags file is `.git/tags`). 129 " 130 " Thankfully most `ctags` implementations support an option 131 " just for this. 132 let l:use_tag_relative_opt = 1 133 endif 134 else 135 " else: the tags file goes in a cache directory, so we need to specify 136 " all the paths absolutely for `ctags` to do its job correctly. 137 let l:actual_proj_dir = a:proj_dir 138 let l:actual_tags_file = a:tags_file 139 endif 140 141 " Build the command line. 142 let l:cmd = [s:runner_exe] 143 let l:cmd += ['-e', '"' . s:get_ctags_executable(a:proj_dir) . '"'] 144 let l:cmd += ['-t', '"' . l:actual_tags_file . '"'] 145 let l:cmd += ['-p', '"' . l:actual_proj_dir . '"'] 146 if l:write_mode == 0 && l:tags_file_exists 147 let l:cur_file_path = expand('%:p') 148 if empty(g:gutentags_cache_dir) && l:tags_file_is_local 149 let l:cur_file_path = fnamemodify(l:cur_file_path, ':.') 150 endif 151 let l:cmd += ['-s', '"' . l:cur_file_path . '"'] 152 else 153 let l:file_list_cmd = gutentags#get_project_file_list_cmd(l:actual_proj_dir) 154 if !empty(l:file_list_cmd) 155 if match(l:file_list_cmd, '///') > 0 156 let l:suffopts = split(l:file_list_cmd, '///') 157 let l:suffoptstr = l:suffopts[1] 158 let l:file_list_cmd = l:suffopts[0] 159 if l:suffoptstr == 'absolute' 160 let l:cmd += ['-A'] 161 endif 162 endif 163 let l:cmd += ['-L', '"' . l:file_list_cmd. '"'] 164 endif 165 endif 166 if empty(get(l:, 'file_list_cmd', '')) 167 " Pass the Gutentags recursive options file before the project 168 " options file, so that users can override --recursive. 169 " Omit --recursive if this project uses a file list command. 170 let l:cmd += ['-o', '"' . gutentags#get_res_file('ctags_recursive.options') . '"'] 171 endif 172 if l:use_tag_relative_opt 173 let l:cmd += ['-O', shellescape("--tag-relative=yes")] 174 endif 175 for extra_arg in g:gutentags_ctags_extra_args 176 let l:cmd += ['-O', shellescape(extra_arg)] 177 endfor 178 if !empty(g:gutentags_ctags_post_process_cmd) 179 let l:cmd += ['-P', shellescape(g:gutentags_ctags_post_process_cmd)] 180 endif 181 let l:proj_options_file = a:proj_dir . '/' . 182 \g:gutentags_ctags_options_file 183 if filereadable(l:proj_options_file) 184 let l:proj_options_file = s:process_options_file( 185 \a:proj_dir, l:proj_options_file) 186 let l:cmd += ['-o', '"' . l:proj_options_file . '"'] 187 endif 188 if g:gutentags_ctags_exclude_wildignore 189 call s:generate_wildignore_options() 190 if !empty(s:wildignores_options_path) 191 let l:cmd += ['-x', shellescape('@'.s:wildignores_options_path, 1)] 192 endif 193 endif 194 for exc in g:gutentags_ctags_exclude 195 let l:cmd += ['-x', '"' . exc . '"'] 196 endfor 197 if g:gutentags_pause_after_update 198 let l:cmd += ['-c'] 199 endif 200 if g:gutentags_trace 201 let l:cmd += ['-l', '"' . l:actual_tags_file . '.log"'] 202 endif 203 let l:cmd = gutentags#make_args(l:cmd) 204 205 call gutentags#trace("Running: " . string(l:cmd)) 206 call gutentags#trace("In: " . getcwd()) 207 if !g:gutentags_fake 208 let l:job_opts = gutentags#build_default_job_options('ctags') 209 let l:job = gutentags#start_job(l:cmd, l:job_opts) 210 call gutentags#add_job('ctags', a:tags_file, l:job) 211 else 212 call gutentags#trace("(fake... not actually running)") 213 endif 214 endfunction 215 216 function! gutentags#ctags#on_job_exit(job, exit_val) abort 217 let [l:tags_file, l:job_data] = gutentags#remove_job_by_data('ctags', a:job) 218 219 if a:exit_val != 0 && !g:__gutentags_vim_is_leaving 220 call gutentags#warning("ctags job failed, returned: ". 221 \string(a:exit_val)) 222 endif 223 if has('win32') && g:__gutentags_vim_is_leaving 224 " The process got interrupted because Vim is quitting. 225 " Remove the tags and lock files on Windows because there's no `trap` 226 " statement in update script. 227 try | call delete(l:tags_file) | endtry 228 try | call delete(l:tags_file.'.temp') | endtry 229 try | call delete(l:tags_file.'.lock') | endtry 230 endif 231 endfunction 232 233 " }}} 234 235 " Utilities {{{ 236 237 " Get final ctags executable depending whether a filetype one is defined 238 function! s:get_ctags_executable(proj_dir) abort 239 "Only consider the main filetype in cases like 'python.django' 240 let l:ftype = get(split(&filetype, '\.'), 0, '') 241 let l:proj_info = gutentags#get_project_info(a:proj_dir) 242 let l:type = get(l:proj_info, 'type', l:ftype) 243 let exepath = exists('g:gutentags_ctags_executable_{l:type}') 244 \ ? g:gutentags_ctags_executable_{l:type} : g:gutentags_ctags_executable 245 return expand(exepath, 1) 246 endfunction 247 248 function! s:generate_wildignore_options() abort 249 if s:last_wildignores == &wildignore 250 " The 'wildignore' setting didn't change since last time we did this, 251 " but check if file still exist (could have been deleted if temp file) 252 if filereadable(s:wildignores_options_path) 253 call gutentags#trace("Wildignore options file is up to date.") 254 return 255 endif 256 call gutentags#trace("Wildignore options file is not readable.") 257 endif 258 259 if s:wildignores_options_path == '' 260 if empty(g:gutentags_cache_dir) 261 let s:wildignores_options_path = tempname() 262 else 263 let s:wildignores_options_path = 264 \gutentags#stripslash(g:gutentags_cache_dir). 265 \'/_wildignore.options' 266 endif 267 endif 268 269 call gutentags#trace("Generating wildignore options: ".s:wildignores_options_path) 270 let l:opt_lines = [] 271 for ign in split(&wildignore, ',') 272 call add(l:opt_lines, ign) 273 endfor 274 call writefile(l:opt_lines, s:wildignores_options_path) 275 let s:last_wildignores = &wildignore 276 endfunction 277 278 function! s:process_options_file(proj_dir, path) abort 279 if empty(g:gutentags_cache_dir) 280 " If we're not using a cache directory to store tag files, we can 281 " use the options file straight away. 282 return a:path 283 endif 284 285 " See if we need to process the options file. 286 let l:do_process = 0 287 let l:proj_dir = gutentags#stripslash(a:proj_dir) 288 let l:out_path = gutentags#get_cachefile(l:proj_dir, 'options') 289 if !filereadable(l:out_path) 290 call gutentags#trace("Processing options file '".a:path."' because ". 291 \"it hasn't been processed yet.") 292 let l:do_process = 1 293 elseif getftime(a:path) > getftime(l:out_path) 294 call gutentags#trace("Processing options file '".a:path."' because ". 295 \"it has changed.") 296 let l:do_process = 1 297 endif 298 if l:do_process == 0 299 " Nothing's changed, return the existing processed version of the 300 " options file. 301 return l:out_path 302 endif 303 304 " We have to process the options file. Right now this only means capturing 305 " all the 'exclude' rules, and rewrite them to make them absolute. 306 " 307 " This is because since `ctags` is run with absolute paths (because we 308 " want the tag file to be in a cache directory), it will do its path 309 " matching with absolute paths too, so the exclude rules need to be 310 " absolute. 311 let l:lines = readfile(a:path) 312 let l:outlines = [] 313 for line in l:lines 314 let l:exarg_idx = matchend(line, '\v^\-\-exclude=') 315 if l:exarg_idx < 0 316 call add(l:outlines, line) 317 continue 318 endif 319 320 " Don't convert things that don't look like paths. 321 let l:exarg = strpart(line, l:exarg_idx + 1) 322 let l:do_convert = 1 323 if l:exarg[0] == '@' " Manifest file path 324 let l:do_convert = 0 325 endif 326 if stridx(l:exarg, '/') < 0 && stridx(l:exarg, '\\') < 0 " Filename 327 let l:do_convert = 0 328 endif 329 if l:do_convert == 0 330 call add(l:outlines, line) 331 continue 332 endif 333 334 let l:fullp = l:proj_dir . gutentags#normalizepath('/'.l:exarg) 335 let l:ol = '--exclude='.l:fullp 336 call add(l:outlines, l:ol) 337 endfor 338 339 call writefile(l:outlines, l:out_path) 340 return l:out_path 341 endfunction 342 343 " }}}