0
|
1 " autotags.vim - Automatic ctags management for Vim
|
|
2 " Maintainer: Ludovic Chabant <http://ludovic.chabant.com>
|
|
3 " Version: 0.0.1
|
|
4
|
|
5 " Globals {{{
|
|
6
|
|
7 if !exists('g:autotags_debug')
|
|
8 let g:autotags_debug = 0
|
|
9 endif
|
|
10
|
|
11 if (exists('g:loaded_autotags') || &cp) && !g:autotags_debug
|
|
12 finish
|
|
13 endif
|
|
14 if (exists('g:loaded_autotags') && g:autotags_debug)
|
|
15 echom "Reloaded autotags."
|
|
16 endif
|
|
17 let g:loaded_autotags = 1
|
|
18
|
|
19 if !exists('g:autotags_trace')
|
3
|
20 let g:autotags_trace = 0
|
0
|
21 endif
|
|
22
|
|
23 if !exists('g:autotags_fake')
|
|
24 let g:autotags_fake = 0
|
|
25 endif
|
|
26
|
|
27 if !exists('g:autotags_background_update')
|
|
28 let g:autotags_background_update = 1
|
|
29 endif
|
|
30
|
|
31 if !exists('g:autotags_enabled')
|
|
32 let g:autotags_enabled = 1
|
|
33 endif
|
|
34
|
|
35 if !exists('g:autotags_executable')
|
|
36 let g:autotags_executable = 'ctags'
|
|
37 endif
|
|
38
|
|
39 if !exists('g:autotags_tagfile')
|
|
40 let g:autotags_tagfile = 'tags'
|
|
41 endif
|
|
42
|
|
43 if !exists('g:autotags_project_root')
|
|
44 let g:autotags_project_root = []
|
|
45 endif
|
|
46 let g:autotags_project_root += ['.git', '.hg', '.bzr', '_darcs']
|
|
47
|
3
|
48 if !exists('g:autotags_exclude')
|
|
49 let g:autotags_exclude = []
|
|
50 endif
|
|
51
|
|
52 if !exists('g:autotags_generate_on_missing')
|
|
53 let g:autotags_generate_on_missing = 1
|
|
54 endif
|
|
55
|
|
56 if !exists('g:autotags_generate_on_write')
|
|
57 let g:autotags_generate_on_write = 1
|
|
58 endif
|
|
59
|
|
60 if !exists('g:autotags_auto_set_tags')
|
|
61 let g:autotags_auto_set_tags = 1
|
|
62 endif
|
|
63
|
0
|
64 " }}}
|
|
65
|
|
66 " Utilities {{{
|
|
67
|
|
68 " Throw an exception message.
|
|
69 function! s:throw(message)
|
|
70 let v:errmsg = "autotags: " . a:message
|
|
71 throw v:errmsg
|
|
72 endfunction
|
|
73
|
|
74 " Prints a message if debug tracing is enabled.
|
|
75 function! s:trace(message, ...)
|
|
76 if g:autotags_trace || (a:0 && a:1)
|
|
77 let l:message = "autotags: " . a:message
|
|
78 echom l:message
|
|
79 endif
|
|
80 endfunction
|
|
81
|
|
82 " Strips the ending slash in a path.
|
|
83 function! s:stripslash(path)
|
|
84 return fnamemodify(a:path, ':s?[/\\]$??')
|
|
85 endfunction
|
|
86
|
|
87 " Normalizes the slashes in a path.
|
|
88 function! s:normalizepath(path)
|
|
89 if exists('+shellslash') && &shellslash
|
|
90 return substitute(a:path, '\v/', '\\', 'g')
|
|
91 elseif has('win32')
|
|
92 return substitute(a:path, '\v/', '\\', 'g')
|
|
93 else
|
|
94 return a:path
|
|
95 endif
|
|
96 endfunction
|
|
97
|
|
98 " Shell-slashes the path (opposite of `normalizepath`).
|
|
99 function! s:shellslash(path)
|
|
100 if exists('+shellslash') && !&shellslash
|
|
101 return substitute(a:path, '\v\\', '/', 'g')
|
|
102 else
|
|
103 return a:path
|
|
104 endif
|
|
105 endfunction
|
|
106
|
|
107 " }}}
|
|
108
|
|
109 " Autotags Setup {{{
|
|
110
|
|
111 " Finds the tag file path for the given current directory
|
|
112 " (typically the directory of the file being edited)
|
|
113 function! s:get_tagfile_for(path) abort
|
|
114 let l:path = s:stripslash(a:path)
|
|
115 let l:previous_path = ""
|
|
116 while l:path != l:previous_path
|
|
117 for root in g:autotags_project_root
|
|
118 if getftype(l:path . '/' . root) != ""
|
|
119 return simplify(fnamemodify(l:path, ':p') . g:autotags_tagfile)
|
|
120 endif
|
|
121 endfor
|
|
122 let l:previous_path = l:path
|
|
123 let l:path = fnamemodify(l:path, ':h')
|
|
124 endwhile
|
|
125 call s:throw("Can't figure out what tag file to use for: " . a:path)
|
|
126 endfunction
|
|
127
|
|
128 " Setup autotags for the current buffer.
|
|
129 function! s:setup_autotags() abort
|
3
|
130 if exists('b:autotags_file') && !g:autotags_debug
|
|
131 " This buffer already has autotags support.
|
0
|
132 return
|
|
133 endif
|
3
|
134
|
|
135 " Try and file what tags file we should manage.
|
|
136 call s:trace("Scanning buffer '" . bufname('%') . "' for autotags setup...")
|
0
|
137 try
|
|
138 let b:autotags_file = s:get_tagfile_for(expand('%:h'))
|
|
139 catch /^autotags\:/
|
3
|
140 call s:trace("Can't figure out what tag file to use... no autotags support.")
|
0
|
141 return
|
|
142 endtry
|
|
143
|
3
|
144 " We know what tags file to manage! Now set things up.
|
0
|
145 call s:trace("Setting autotags for buffer '" . bufname('%') . "' with tagfile: " . b:autotags_file)
|
|
146
|
3
|
147 " Set the tags file for Vim to use.
|
|
148 if g:autotags_auto_set_tags
|
|
149 execute 'setlocal tags^=' . b:autotags_file
|
|
150 endif
|
|
151
|
|
152 " Autocommands for updating the tags on save.
|
0
|
153 let l:bn = bufnr('%')
|
|
154 execute 'augroup autotags_buffer_' . l:bn
|
|
155 execute ' autocmd!'
|
3
|
156 execute ' autocmd BufWritePost <buffer=' . l:bn . '> call s:write_triggered_update_tags()'
|
0
|
157 execute 'augroup end'
|
|
158
|
3
|
159 " Miscellaneous commands.
|
0
|
160 command! -buffer -bang AutotagsUpdate :call s:manual_update_tags(<bang>0)
|
3
|
161
|
|
162 " If the tags file doesn't exist, start generating it now.
|
|
163 if g:autotags_generate_on_missing && !filereadable(b:autotags_file)
|
|
164 call s:trace("Generating missing tags file: " . b:autotags_file)
|
|
165 call s:update_tags(1, 0)
|
|
166 endif
|
0
|
167 endfunction
|
|
168
|
|
169 augroup autotags_detect
|
|
170 autocmd!
|
|
171 autocmd BufNewFile,BufReadPost * call s:setup_autotags()
|
|
172 autocmd VimEnter * if expand('<amatch>')==''|call s:setup_autotags()|endif
|
|
173 augroup end
|
|
174
|
|
175 " }}}
|
|
176
|
|
177 " Tags File Management {{{
|
|
178
|
|
179 let s:runner_exe = expand('<sfile>:h:h') . '/plat/unix/update_tags.sh'
|
|
180 if has('win32')
|
|
181 let s:runner_exe = expand('<sfile>:h:h') . '\plat\win32\update_tags.cmd'
|
|
182 endif
|
|
183
|
|
184 let s:update_queue = []
|
3
|
185 let s:maybe_in_progress = []
|
0
|
186
|
|
187 " Get how to execute an external command depending on debug settings.
|
|
188 function! s:get_execute_cmd() abort
|
|
189 if has('win32')
|
|
190 let l:cmd = '!start '
|
|
191 if g:autotags_background_update
|
|
192 let l:cmd .= '/b '
|
|
193 endif
|
|
194 return l:cmd
|
|
195 else
|
|
196 return '!'
|
|
197 endif
|
|
198 endfunction
|
|
199
|
3
|
200 " Get the suffix for how to execute an external command.
|
|
201 function! s:get_execute_cmd_suffix() abort
|
|
202 if has('win32')
|
|
203 return ''
|
|
204 else
|
|
205 return ' &'
|
|
206 endif
|
|
207 endfunction
|
|
208
|
0
|
209 " (Re)Generate the tags file for the current buffer's file.
|
|
210 function! s:manual_update_tags(bang) abort
|
|
211 call s:update_tags(a:bang, 0)
|
|
212 endfunction
|
|
213
|
3
|
214 " (Re)Generate the tags file for a buffer that just go saved.
|
|
215 function! s:write_triggered_update_tags() abort
|
|
216 if g:autotags_enabled && g:autotags_generate_on_write
|
|
217 call s:update_tags(0, 1)
|
|
218 endif
|
|
219 endfunction
|
|
220
|
0
|
221 " Update the tags file for the current buffer's file.
|
|
222 " write_mode:
|
|
223 " 0: update the tags file if it exists, generate it otherwise.
|
|
224 " 1: always generate (overwrite) the tags file.
|
|
225 "
|
|
226 " queue_mode:
|
|
227 " 0: if an update is already in progress, report it and abort.
|
|
228 " 1: if an update is already in progress, queue another one.
|
|
229 "
|
|
230 " An additional argument specifies where to write the tags file. If nothing
|
|
231 " is specified, it will go to the autotags-defined file.
|
|
232 function! s:update_tags(write_mode, queue_mode, ...) abort
|
|
233 " Figure out where to save.
|
|
234 if a:0 == 1
|
|
235 let l:tags_file = a:1
|
|
236 else
|
|
237 let l:tags_file = b:autotags_file
|
|
238 endif
|
|
239
|
|
240 " Check that there's not already an update in progress.
|
|
241 let l:lock_file = l:tags_file . '.lock'
|
|
242 if filereadable(l:lock_file)
|
|
243 if a:queue_mode == 1
|
|
244 let l:idx = index(s:update_queue, l:tags_file)
|
|
245 if l:idx < 0
|
|
246 call add(s:update_queue, l:tags_file)
|
|
247 endif
|
|
248 call s:trace("Tag file '" . l:tags_file . "' is already being updated. Queuing it up...")
|
|
249 call s:trace("")
|
|
250 else
|
|
251 echom "autotags: The tags file is already being updated, please try again later."
|
|
252 echom ""
|
|
253 endif
|
|
254 return
|
|
255 endif
|
|
256
|
|
257 " Switch to the project root to make the command line smaller, and make
|
|
258 " it possible to get the relative path of the filename to parse if we're
|
|
259 " doing an incremental update.
|
|
260 let l:prev_cwd = getcwd()
|
|
261 let l:work_dir = fnamemodify(l:tags_file, ':h')
|
|
262 execute "chdir " . l:work_dir
|
|
263
|
|
264 try
|
|
265 " Build the command line.
|
|
266 let l:cmd = s:get_execute_cmd() . s:runner_exe
|
3
|
267 let l:cmd .= ' -e "' . g:autotags_executable . '"'
|
|
268 let l:cmd .= ' -t "' . fnamemodify(l:tags_file, ':t') . '"'
|
0
|
269 if a:write_mode == 0 && filereadable(l:tags_file)
|
|
270 " CTags specifies paths relative to the tags file with a `./`
|
|
271 " prefix, so we need to specify the same prefix otherwise it will
|
|
272 " think those are different files and we'll end up with duplicate
|
|
273 " entries.
|
|
274 let l:rel_path = s:normalizepath('./' . expand('%:.'))
|
3
|
275 let l:cmd .= ' -s "' . l:rel_path . '"'
|
0
|
276 endif
|
3
|
277 for ign in split(&wildignore, ',')
|
|
278 let l:cmd .= ' -x ' . ign
|
|
279 endfor
|
|
280 for exc in g:autotags_exclude
|
|
281 let l:cmd .= ' -x ' . exc
|
|
282 endfor
|
0
|
283 if g:autotags_trace
|
3
|
284 if has('win32')
|
|
285 let l:cmd .= ' -l "' . fnamemodify(l:tags_file, ':t') . '.log"'
|
|
286 else
|
|
287 let l:cmd .= ' > "' . fnamemodify(l:tags_file, ':t') . '.log"'
|
|
288 endif
|
0
|
289 endif
|
3
|
290 let l:cmd .= s:get_execute_cmd_suffix()
|
|
291
|
|
292 " Run the background process.
|
0
|
293 call s:trace("Running: " . l:cmd)
|
|
294 call s:trace("In: " . l:work_dir)
|
|
295 if !g:autotags_fake
|
3
|
296 " Flag this tags file as being in progress
|
|
297 call add(s:maybe_in_progress, fnamemodify(l:tags_file, ':p'))
|
|
298
|
0
|
299 if !g:autotags_trace
|
|
300 silent execute l:cmd
|
|
301 else
|
|
302 execute l:cmd
|
|
303 endif
|
|
304 else
|
|
305 call s:trace("(fake... not actually running)")
|
|
306 endif
|
|
307 call s:trace("")
|
|
308 finally
|
|
309 " Restore the current directory...
|
|
310 execute "chdir " . l:prev_cwd
|
|
311 endtry
|
|
312 endfunction
|
|
313
|
|
314 " }}}
|
|
315
|
|
316 " Manual Tagfile Generation {{{
|
|
317
|
|
318 function! s:generate_tags(bang, ...) abort
|
|
319 call s:update_tags(1, 0, a:1)
|
|
320 endfunction
|
|
321
|
|
322 command! -bang -nargs=1 -complete=file AutotagsGenerate :call s:generate_tags(<bang>0, <f-args>)
|
|
323
|
|
324 " }}}
|
|
325
|
3
|
326 " Toggles and Miscellaneous Commands {{{
|
0
|
327
|
|
328 command! AutotagsToggleEnabled :let g:autotags_enabled=!g:autotags_enabled
|
|
329 command! AutotagsToggleTrace :call autotags#trace()
|
|
330 command! AutotagsToggleFake :call autotags#fake()
|
|
331 command! AutotagsUnlock :call delete(b:autotags_file . '.lock')
|
|
332
|
|
333 " }}}
|
|
334
|
|
335 " Autoload Functions {{{
|
|
336
|
|
337 function! autotags#rescan(...)
|
|
338 if exists('b:autotags_file')
|
|
339 unlet b:autotags_file
|
|
340 endif
|
|
341 if a:0 && a:1
|
|
342 let l:trace_backup = g:autotags_trace
|
|
343 let l:autotags_trace = 1
|
|
344 endif
|
|
345 call s:setup_autotags()
|
|
346 if a:0 && a:1
|
|
347 let g:autotags_trace = l:trace_backup
|
|
348 endif
|
|
349 endfunction
|
|
350
|
|
351 function! autotags#trace(...)
|
|
352 let g:autotags_trace = !g:autotags_trace
|
|
353 if a:0 > 0
|
|
354 let g:autotags_trace = a:1
|
|
355 endif
|
|
356 if g:autotags_trace
|
|
357 echom "autotags: Tracing is enabled."
|
|
358 else
|
|
359 echom "autotags: Tracing is disabled."
|
|
360 endif
|
|
361 echom ""
|
|
362 endfunction
|
|
363
|
|
364 function! autotags#fake(...)
|
|
365 let g:autotags_fake = !g:autotags_fake
|
|
366 if a:0 > 0
|
|
367 let g:autotags_fake = a:1
|
|
368 endif
|
|
369 if g:autotags_fake
|
|
370 echom "autotags: Now faking autotags."
|
|
371 else
|
|
372 echom "autotags: Now running autotags for real."
|
|
373 endif
|
|
374 echom ""
|
|
375 endfunction
|
|
376
|
3
|
377 function! autotags#inprogress()
|
|
378 echom "autotags: generations in progress:"
|
|
379 for mip in s:maybe_in_progress
|
|
380 echom mip
|
|
381 endfor
|
|
382 echom ""
|
|
383 endfunction
|
|
384
|
0
|
385 " }}}
|
|
386
|
3
|
387 " Statusline Functions {{{
|
|
388
|
|
389 " Prints whether a tag file is being generated right now for the current
|
|
390 " buffer in the status line.
|
|
391 "
|
|
392 " Arguments can be passed:
|
|
393 " - args 1 and 2 are the prefix and suffix, respectively, of whatever output,
|
|
394 " if any, is going to be produced.
|
|
395 " (defaults to empty strings)
|
|
396 " - arg 3 is the text to be shown if tags are currently being generated.
|
|
397 " (defaults to 'TAGS')
|
|
398 "
|
|
399 function! autotags#statusline(...) abort
|
|
400 if !exists('b:autotags_file')
|
|
401 " This buffer doesn't have autotags.
|
|
402 return ''
|
|
403 endif
|
|
404
|
|
405 " Figure out what the user is customizing.
|
|
406 let l:gen_msg = 'TAGS'
|
|
407 if a:0 >= 0
|
|
408 let l:gen_msg = a:1
|
|
409 endif
|
|
410
|
|
411 " To make this function as fast as possible, we first check whether the
|
|
412 " current buffer's tags file is 'maybe' being generated. This provides a
|
|
413 " nice and quick bail out for 99.9% of cases before we need to this the
|
|
414 " file-system to check the lock file.
|
|
415 let l:abs_tag_file = fnamemodify(b:autotags_file, ':p')
|
|
416 let l:found = index(s:maybe_in_progress, l:abs_tag_file)
|
|
417 if l:found < 0
|
|
418 return ''
|
|
419 endif
|
|
420 " It's maybe generating! Check if the lock file is still there.
|
|
421 if !filereadable(l:abs_tag_file . '.lock')
|
|
422 call remove(s:maybe_in_progress, l:found)
|
|
423 return ''
|
|
424 endif
|
|
425 " It's still there! So probably `ctags` is still running...
|
|
426 " (although there's a chance it crashed, or the script had a problem, and
|
|
427 " the lock file has been left behind... we could try and run some
|
|
428 " additional checks here to see if it's legitimately running, and
|
|
429 " otherwise delete the lock file... maybe in the future...)
|
|
430 return l:gen_msg
|
|
431 endfunction
|
|
432
|
|
433 " }}}
|
|
434
|