1
2 Author:peter@peterodding.com
3 Last Change:
4 URL:http://peterodding.com/code/vim/easytags/
5
6 let s:script = expand('<sfile>:p:~')
7
8
9
10 function! easytags#autoload()
11 try
12
13 let pathname = s:resolve(expand('%:p'))
14 if pathname != ''
15 let tags_outdated = getftime(pathname) > getftime(easytags#get_tagsfile())
16 if tags_outdated || !easytags#file_has_tags(pathname)
17 call easytags#update(1, 0, [])
18 endif
19 endif
20
21 if &eventignore !~? '\<syntax\>'
22 if !exists('b:easytags_last_highlighted')
23 call easytags#highlight()
24 else
25 for tagfile in tagfiles()
26 if getftime(tagfile) > b:easytags_last_highlighted
27 call easytags#highlight()
28 break
29 endif
30 endfor
31 endif
32 let b:easytags_last_highlighted = localtime()
33 endif
34 catch
35 call xolox#warning("%s: %s (at %s)", s:script, v:exception, v:throwpoint)
36 endtry
37 endfunction
38
39 function! easytags#update(silent, filter_tags, filenames)
40 try
41 let s:cached_filenames = {}
42 let starttime = xolox#timer#start()
43 let cfile = s:check_cfile(a:silent, a:filter_tags, !empty(a:filenames))
44 let tagsfile = easytags#get_tagsfile()
45 let firstrun = !filereadable(tagsfile)
46 let cmdline = s:prep_cmdline(cfile, tagsfile, firstrun, a:filenames)
47 let output = s:run_ctags(starttime, cfile, tagsfile, firstrun, cmdline)
48 if !firstrun
49 let num_filtered = s:filter_merge_tags(a:filter_tags, tagsfile, output)
50 if cfile != ''
51 let msg = "%s: Updated tags for %s in %s."
52 call xolox#timer#stop(msg, s:script, expand('%:p:~'), starttime)
53 elseif a:0 > 0
54 let msg = "%s: Updated tags in %s."
55 call xolox#timer#stop(msg, s:script, starttime)
56 else
57 let msg = "%s: Filtered %i invalid tags in %s."
58 call xolox#timer#stop(msg, s:script, num_filtered, starttime)
59 endif
60 endif
61 return 1
62 catch
63 call xolox#warning("%s: %s (at %s)", s:script, v:exception, v:throwpoint)
64 finally
65 unlet s:cached_filenames
66 endtry
67 endfunction
68
69 function! s:check_cfile(silent, filter_tags, have_args)
70 if a:have_args
71 return ''
72 endif
73 let silent = a:silent || a:filter_tags
74 if g:easytags_autorecurse
75 let cdir = s:resolve(expand('%:p:h'))
76 if !isdirectory(cdir)
77 if silent | return '' | endif
78 throw "The directory of the current file doesn't exist yet!"
79 endif
80 return cdir
81 endif
82 let cfile = s:resolve(expand('%:p'))
83 if cfile == '' || !filereadable(cfile)
84 if silent | return '' | endif
85 throw "You'll need to save your file before using :UpdateTags!"
86 elseif g:easytags_ignored_filetypes != '' && &ft =~ g:easytags_ignored_filetypes
87 if silent | return '' | endif
88 throw "The " . string(&ft) . " file type is explicitly ignored."
89 elseif index(easytags#supported_filetypes(), &ft) == -1
90 if silent | return '' | endif
91 throw "Exuberant Ctags doesn't support the " . string(&ft) . " file type!"
92 endif
93 return cfile
94 endfunction
95
96 function! s:prep_cmdline(cfile, tagsfile, firstrun, arguments)
97 let cmdline = [g:easytags_cmd, '--fields=+l', '--c-kinds=+p', '--c++-kinds=+p']
98 if a:firstrun
99 call add(cmdline, shellescape('-f' . a:tagsfile))
100 call add(cmdline, '--sort=' . (&ic ? 'foldcase' : 'yes'))
101 else
102 call add(cmdline, '--sort=no')
103 call add(cmdline, '-f-')
104 endif
105 if g:easytags_include_members
106 call add(cmdline, '--extra=+q')
107 endif
108 let have_args = 0
109 if a:cfile != ''
110 if g:easytags_autorecurse
111 call add(cmdline, '-R')
112 call add(cmdline, shellescape(a:cfile))
113 else
114 let filetype = easytags#to_ctags_ft(&filetype)
115 call add(cmdline, shellescape('--language-force=' . filetype))
116 call add(cmdline, shellescape(a:cfile))
117 endif
118 let have_args = 1
119 else
120 for arg in a:arguments
121 if arg =~ '^-'
122 call add(cmdline, arg)
123 let have_args = 1
124 else
125 let matches = split(expand(arg), "\n")
126 if !empty(matches)
127 call map(matches, 'shellescape(s:canonicalize(v:val))')
128 call extend(cmdline, matches)
129 let have_args = 1
130 endif
131 endif
132 endfor
133 endif
134
135 return have_args ? join(cmdline) : ''
136 endfunction
137
138 function! s:run_ctags(starttime, cfile, tagsfile, firstrun, cmdline)
139 let output = []
140 if a:cmdline != ''
141 call xolox#debug("%s: Executing %s", s:script, a:cmdline)
142 try
143 let output = xolox#shell#execute(a:cmdline, 1)
144 catch /^Vim\%((\a\+)\)\=:E117/
145
146 let output = split(system(a:cmdline), "\n")
147 if v:shell_error
148 let msg = "Failed to update tags file %s: %s!"
149 throw printf(msg, fnamemodify(a:tagsfile, ':~'), strtrans(join(output, "\n")))
150 endif
151 endtry
152 if a:firstrun
153 if a:cfile != ''
154 call easytags#add_tagged_file(a:cfile)
155 call xolox#timer#stop("%s: Created tags for %s in %s.", s:script, expand('%:p:~'), a:starttime)
156 else
157 call xolox#timer#stop("%s: Created tags in %s.", s:script, a:starttime)
158 endif
159 endif
160 endif
161 return output
162 endfunction
163
164 function! s:filter_merge_tags(filter_tags, tagsfile, output)
165 let [headers, entries] = easytags#read_tagsfile(a:tagsfile)
166 call s:set_tagged_files(entries)
167 let filters = []
168 let tagged_files = s:find_tagged_files(a:output)
169 if !empty(tagged_files)
170 call add(filters, '!has_key(tagged_files, s:canonicalize(get(v:val, 1)))')
171 endif
172 if a:filter_tags
173 call add(filters, 'filereadable(get(v:val, 1))')
174 endif
175 let num_old_entries = len(entries)
176 if !empty(filters)
177 call filter(entries, join(filters, ' && '))
178 endif
179 let num_filtered = num_old_entries - len(entries)
180 call map(entries, 'join(v:val, "\t")')
181 call extend(entries, a:output)
182 if !easytags#write_tagsfile(a:tagsfile, headers, entries)
183 let msg = "Failed to write filtered tags file %s!"
184 throw printf(msg, fnamemodify(a:tagsfile, ':~'))
185 endif
186 return num_filtered
187 endfunction
188
189 function! s:find_tagged_files(new_entries)
190 let tagged_files = {}
191 for line in a:new_entries
192
193
194 if match(line, '^[^\t]\+\t[^\t]\+\t.\+$') == -1
195 throw "Exuberant Ctags returned invalid data: " . strtrans(line)
196 endif
197 let filename = matchstr(line, '^[^\t]\+\t\zs[^\t]\+')
198 if !has_key(tagged_files, filename)
199 let filename = s:canonicalize(filename)
200 let tagged_files[filename] = 1
201 call easytags#add_tagged_file(filename)
202 endif
203 endfor
204 return tagged_files
205 endfunction
206
207 function! easytags#highlight()
208 try
209 let filetype = get(s:canonical_aliases, &ft, &ft)
210 let tagkinds = get(s:tagkinds, filetype, [])
211 if exists('g:syntax_on') && !empty(tagkinds) && !exists('b:easytags_nohl')
212 let starttime = xolox#timer#start()
213 if !has_key(s:aliases, &ft)
214 let taglist = filter(taglist('.'), "get(v:val, 'language', '') ==? &ft")
215 else
216 let aliases = s:aliases[&ft]
217 let taglist = filter(taglist('.'), "has_key(aliases, tolower(get(v:val, 'language', '')))")
218 endif
219 for tagkind in tagkinds
220 let hlgroup_tagged = tagkind.hlgroup . 'Tag'
221 if hlexists(hlgroup_tagged)
222 execute 'syntax clear' hlgroup_tagged
223 else
224 execute 'highlight def link' hlgroup_tagged tagkind.hlgroup
225 endif
226 let matches = filter(copy(taglist), tagkind.filter)
227 if matches != []
228 call map(matches, 'xolox#escape#pattern(get(v:val, "name"))')
229 let pattern = tagkind.pattern_prefix . '\%(' . join(xolox#unique(matches), '\|') . '\)' . tagkind.pattern_suffix
230 let template = 'syntax match %s /%s/ containedin=ALLBUT,.*String.*,.*Comment.*'
231 let command = printf(template, hlgroup_tagged, escape(pattern, '/'))
232 try
233 execute command
234 catch /^Vim\%((\a\+)\)\=:E339/
235 let msg = "easytags.vim: Failed to highlight %i %s tags because pattern is too big! (%i KB)"
236 call xolox#warning(printf(msg, len(matches), tagkind.hlgroup, len(pattern) / 1024))
237 endtry
238 endif
239 endfor
240 redraw
241 let bufname = expand('%:p:~')
242 if bufname == ''
243 let bufname = 'unnamed buffer #' . bufnr('%')
244 endif
245 let msg = "%s: Highlighted tags in %s in %s."
246 call xolox#timer#stop(msg, s:script, bufname, starttime)
247 return 1
248 endif
249 catch
250 call xolox#warning("%s: %s (at %s)", s:script, v:exception, v:throwpoint)
251 endtry
252 endfunction
253
254
255
256 function! easytags#supported_filetypes()
257 if !exists('s:supported_filetypes')
258 let starttime = xolox#timer#start()
259 let command = g:easytags_cmd . ' --list-languages'
260 try
261 let listing = xolox#shell#execute(command, 1)
262 catch /^Vim\%((\a\+)\)\=:E117/
263
264 let listing = split(system(command), "\n")
265 if v:shell_error
266 let msg = "Failed to get supported languages! (output: %s)"
267 throw printf(msg, strtrans(join(listing, "\n")))
268 endif
269 endtry
270 let s:supported_filetypes = map(copy(listing), 's:check_filetype(listing, v:val)')
271 let msg = "%s: Retrieved %i supported languages in %s."
272 call xolox#timer#stop(msg, s:script, len(s:supported_filetypes), starttime)
273 endif
274 return s:supported_filetypes
275 endfunction
276
277 function! s:check_filetype(listing, cline)
278 if a:cline !~ '^\w\S*$'
279 let msg = "Failed to get supported languages! (output: %s)"
280 throw printf(msg, strtrans(join(a:listing, "\n")))
281 endif
282 return easytags#to_vim_ft(a:cline)
283 endfunction
284
285 function! easytags#read_tagsfile(tagsfile)
286
287 "!_TAG_FILE_SORTED"
288
289 easytags#write_tagsfile()
290 "E432: Tags file not sorted"
291 let headers = []
292 let entries = []
293 let pattern = '^\([^\t]\+\)\t\([^\t]\+\)\t\(.\+\)$'
294 for line in readfile(a:tagsfile)
295 if line =~# '^!_TAG_'
296 call add(headers, line)
297 else
298 call add(entries, matchlist(line, pattern)[1:3])
299 endif
300 endfor
301 return [headers, entries]
302 endfunction
303
304 function! easytags#write_tagsfile(tagsfile, headers, entries)
305 "foldcase"
306 let sort_order = 1
307 for line in a:headers
308 if match(line, '^!_TAG_FILE_SORTED\t2') == 0
309 let sort_order = 2
310 endif
311 endfor
312 if sort_order == 1
313 call sort(a:entries)
314 else
315 call sort(a:entries, 1)
316 endif
317 let lines = []
318 if has('win32') || has('win64')
319 writefile()
320 for line in a:headers
321 call add(lines, line . "\r")
322 endfor
323 for line in a:entries
324 call add(lines, line . "\r")
325 endfor
326 else
327 call extend(lines, a:headers)
328 call extend(lines, a:entries)
329 endif
330 return writefile(lines, a:tagsfile) == 0
331 endfunction
332
333 function! easytags#file_has_tags(filename)
334 call s:cache_tagged_files()
335 return has_key(s:tagged_files, s:resolve(a:filename))
336 endfunction
337
338 function! easytags#add_tagged_file(filename)
339 call s:cache_tagged_files()
340 let filename = s:resolve(a:filename)
341 let s:tagged_files[filename] = 1
342 endfunction
343
344 function! easytags#get_tagsfile()
345 let tagsfile = expand(g:easytags_file)
346 if filereadable(tagsfile) && filewritable(tagsfile) != 1
347 let message = "The tags file %s isn't writable!"
348 throw printf(message, fnamemodify(tagsfile, ':~'))
349 endif
350 return tagsfile
351 endfunction
352
353
354
355 function! easytags#define_tagkind(object)
356 if !has_key(a:object, 'pattern_prefix')
357 let a:object.pattern_prefix = '\C\<'
358 endif
359 if !has_key(a:object, 'pattern_suffix')
360 let a:object.pattern_suffix = '\>'
361 endif
362 if !has_key(s:tagkinds, a:object.filetype)
363 let s:tagkinds[a:object.filetype] = []
364 endif
365 call add(s:tagkinds[a:object.filetype], a:object)
366 endfunction
367
368 function! easytags#map_filetypes(vim_ft, ctags_ft)
369 call add(s:vim_filetypes, a:vim_ft)
370 call add(s:ctags_filetypes, a:ctags_ft)
371 endfunction
372
373 function! easytags#alias_filetypes(...)
374 for type in a:000
375 let s:canonical_aliases[type] = a:1
376 if !has_key(s:aliases, type)
377 let s:aliases[type] = {}
378 endif
379 endfor
380 for i in range(a:0)
381 for j in range(a:0)
382 let vimft1 = a:000[i]
383 let ctagsft1 = easytags#to_ctags_ft(vimft1)
384 let vimft2 = a:000[j]
385 let ctagsft2 = easytags#to_ctags_ft(vimft2)
386 if !has_key(s:aliases[vimft1], ctagsft2)
387 let s:aliases[vimft1][ctagsft2] = 1
388 endif
389 if !has_key(s:aliases[vimft2], ctagsft1)
390 let s:aliases[vimft2][ctagsft1] = 1
391 endif
392 endfor
393 endfor
394 endfunction
395
396 function! easytags#to_vim_ft(ctags_ft)
397 let type = tolower(a:ctags_ft)
398 let index = index(s:ctags_filetypes, type)
399 return index >= 0 ? s:vim_filetypes[index] : type
400 endfunction
401
402 function! easytags#to_ctags_ft(vim_ft)
403 let type = tolower(a:vim_ft)
404 let index = index(s:vim_filetypes, type)
405 return index >= 0 ? s:ctags_filetypes[index] : type
406 endfunction
407
408
409
410 function! s:resolve(filename)
411 if g:easytags_resolve_links
412 return resolve(a:filename)
413 else
414 return a:filename
415 endif
416 endfunction
417
418 function! s:canonicalize(filename)
419 if has_key(s:cached_filenames, a:filename)
420 return s:cached_filenames[a:filename]
421 endif
422 let canonical = s:resolve(fnamemodify(a:filename, ':p'))
423 let s:cached_filenames[a:filename] = canonical
424 return canonical
425 endif
426 endfunction
427
428 function! s:cache_tagged_files()
429 if !exists('s:tagged_files')
430 let tagsfile = easytags#get_tagsfile()
431 try
432 let [headers, entries] = easytags#read_tagsfile(tagsfile)
433 call s:set_tagged_files(entries)
434 catch /\<E484\>/
435
436 call s:set_tagged_files([])
437 endtry
438 endif
439 endfunction
440
441 function! s:set_tagged_files(entries)
442 TODOtaglist()readfile()
443
444 let s:tagged_files = {}
445 for entry in a:entries
446 let filename = get(entry, 1, '')
447 if filename != ''
448 let s:tagged_files[s:resolve(filename)] = 1
449 endif
450 endfor
451 endfunction
452
453
454
455
456 if exists('s:tagkinds')
457 finish
458 endif
459
460 let s:tagkinds = {}
461
462
463 let s:vim_filetypes = []
464 let s:ctags_filetypes = []
465 call easytags#map_filetypes('cpp', 'c++')
466 call easytags#map_filetypes('cs', 'c#')
467 call easytags#map_filetypes(exists('g:filetype_asp') ? g:filetype_asp : 'aspvbs', 'asp')
468
469
470 let s:aliases = {}
471 let s:canonical_aliases = {}
472 call easytags#alias_filetypes('c', 'cpp', 'objc', 'objcpp')
473
474
475 let s:cpo_save = &cpo
476 set cpo&vim
477
478
479
480 call easytags#define_tagkind({
481 \ 'filetype': 'lua',
482 \ 'hlgroup': 'luaFunc',
483 \ 'filter': 'get(v:val, "kind") ==# "f"'})
484
485
486
487 call easytags#define_tagkind({
488 \ 'filetype': 'c',
489 \ 'hlgroup': 'cType',
490 \ 'filter': 'get(v:val, "kind") =~# "[cgstu]"'})
491
492 call easytags#define_tagkind({
493 \ 'filetype': 'c',
494 \ 'hlgroup': 'cEnum',
495 \ 'filter': 'get(v:val, "kind") ==# "e"'})
496
497 call easytags#define_tagkind({
498 \ 'filetype': 'c',
499 \ 'hlgroup': 'cPreProc',
500 \ 'filter': 'get(v:val, "kind") ==# "d"'})
501
502 call easytags#define_tagkind({
503 \ 'filetype': 'c',
504 \ 'hlgroup': 'cFunction',
505 \ 'filter': 'get(v:val, "kind") =~# "[fp]"'})
506
507 highlight def link cEnum Identifier
508 highlight def link cFunction Function
509
510 if g:easytags_include_members
511 call easytags#define_tagkind({
512 \ 'filetype': 'c',
513 \ 'hlgroup': 'cMember',
514 \ 'filter': 'get(v:val, "kind") ==# "m"'})
515 highlight def link cMember Identifier
516 endif
517
518
519
520 call easytags#define_tagkind({
521 \ 'filetype': 'php',
522 \ 'hlgroup': 'phpFunctions',
523 \ 'filter': 'get(v:val, "kind") ==# "f"'})
524
525 call easytags#define_tagkind({
526 \ 'filetype': 'php',
527 \ 'hlgroup': 'phpClasses',
528 \ 'filter': 'get(v:val, "kind") ==# "c"'})
529
530
531
532 call easytags#define_tagkind({
533 \ 'filetype': 'vim',
534 \ 'hlgroup': 'vimAutoGroup',
535 \ 'filter': 'get(v:val, "kind") ==# "a"'})
536
537 highlight def link vimAutoGroup vimAutoEvent
538
539 call easytags#define_tagkind({
540 \ 'filetype': 'vim',
541 \ 'hlgroup': 'vimCommand',
542 \ 'filter': 'get(v:val, "kind") ==# "c"',
543 \ 'pattern_prefix': '\(\(^\|\s\):\?\)\@<=',
544 \ 'pattern_suffix': '\(!\?\(\s\|$\)\)\@='})
545
546
547 "static"
548
549
550
551 call easytags#define_tagkind({
552 \ 'filetype': 'vim',
553 \ 'hlgroup': 'vimFuncName',
554 \ 'filter': 'get(v:val, "kind") ==# "f" && get(v:val, "cmd") !~? ''<sid>\w\|\<s:\w''',
555 \ 'pattern_prefix': '\C\%(\<s:\|<[sS][iI][dD]>\)\@<!\<'})
556
557 call easytags#define_tagkind({
558 \ 'filetype': 'vim',
559 \ 'hlgroup': 'vimScriptFuncName',
560 \ 'filter': 'get(v:val, "kind") ==# "f" && get(v:val, "cmd") =~? ''<sid>\w\|\<s:\w''',
561 \ 'pattern_prefix': '\C\%(\<s:\|<[sS][iI][dD]>\)'})
562
563 highlight def link vimScriptFuncName vimFuncName
564
565
566
567 call easytags#define_tagkind({
568 \ 'filetype': 'python',
569 \ 'hlgroup': 'pythonFunction',
570 \ 'filter': 'get(v:val, "kind") ==# "f"',
571 \ 'pattern_prefix': '\%(\<def\s\+\)\@<!\<'})
572
573 call easytags#define_tagkind({
574 \ 'filetype': 'python',
575 \ 'hlgroup': 'pythonMethod',
576 \ 'filter': 'get(v:val, "kind") ==# "m"',
577 \ 'pattern_prefix': '\.\@<='})
578
579 highlight def link pythonMethodTag pythonFunction
580
581
582
583 call easytags#define_tagkind({
584 \ 'filetype': 'java',
585 \ 'hlgroup': 'javaClass',
586 \ 'filter': 'get(v:val, "kind") ==# "c"'})
587
588 call easytags#define_tagkind({
589 \ 'filetype': 'java',
590 \ 'hlgroup': 'javaMethod',
591 \ 'filter': 'get(v:val, "kind") ==# "m"'})
592
593 highlight def link javaClass Identifier
594 highlight def link javaMethod Function
595
596
597
598 "cpoptions"
599 let &cpo = s:cpo_save
600 unlet s:cpo_save
601
602