1 " Vim script
  2 " Author: Peter Odding <peter@peterodding.com>
  3 " Last Change: September 6, 2010
  4 " URL: http://peterodding.com/code/vim/easytags/
  5 
  6 let s:script = expand('<sfile>:p:~')
  7 
  8 " Public interface through (automatic) commands. {{{1
  9 
 10 function! easytags#autoload() " {{{2
 11   try
 12     " Update the entries for the current file in the global tags file?
 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     " Apply highlighting of tags in global tags file to current buffer?
 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) " {{{2
 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) " {{{3
 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) " {{{3
 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   " No need to run Exuberant Ctags without any filename arguments!
135   return have_args ? join(cmdline) : ''
136 endfunction
137 
138 function! s:run_ctags(starttime, cfile, tagsfile, firstrun, cmdline) " {{{3
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       " Ignore missing shell.vim plug-in.
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) " {{{3
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) " {{{3
190   let tagged_files = {}
191   for line in a:new_entries
192     " Never corrupt the tags file by merging an invalid line
193     " (probably an error message) with the existing tags!
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() " {{{2
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 " Public supporting functions (might be useful to others). {{{1
255 
256 function! easytags#supported_filetypes() " {{{2
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       " Ignore missing shell.vim plug-in.
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) " {{{2
286   " I'm not sure whether this is by design or an implementation detail but
287   " it's possible for the "!_TAG_FILE_SORTED" header to appear after one or
288   " more tags and Vim will apparently still use the header! For this reason
289   " the easytags#write_tagsfile() function should also recognize it, otherwise
290   " Vim might complain with "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) " {{{2
305   " This function always sorts the tags file but understands "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     " Exuberant Ctags on Windows requires \r\n but Vim's writefile() doesn't add them!
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) " {{{2
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) " {{{2
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() " {{{2
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 " Public API for file-type specific dynamic syntax highlighting. {{{1
354 
355 function! easytags#define_tagkind(object) " {{{2
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) " {{{2
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(...) " {{{2
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) " {{{2
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) " {{{2
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 " Miscellaneous script-local functions. {{{1
409 
410 function! s:resolve(filename) " {{{2
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) " {{{2
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() " {{{2
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       " Ignore missing tags file.
436       call s:set_tagged_files([])
437     endtry
438   endif
439 endfunction
440 
441 function! s:set_tagged_files(entries) " {{{2
442   " TODO use taglist() instead of readfile() so that all tag files are
443   " automatically used :-)
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 " Built-in file type & tag kind definitions. {{{1
454 
455 " Don't bother redefining everything below when this script is sourced again.
456 if exists('s:tagkinds')
457   finish
458 endif
459 
460 let s:tagkinds = {}
461 
462 " Define the built-in Vim <=> Ctags file-type mappings.
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 " Define the Vim file-types that are aliased by default.
470 let s:aliases = {}
471 let s:canonical_aliases = {}
472 call easytags#alias_filetypes('c', 'cpp', 'objc', 'objcpp')
473 
474 " Enable line continuation.
475 let s:cpo_save = &cpo
476 set cpo&vim
477 
478 " Lua. {{{2
479 
480 call easytags#define_tagkind({
481       \ 'filetype': 'lua',
482       \ 'hlgroup': 'luaFunc',
483       \ 'filter': 'get(v:val, "kind") ==# "f"'})
484 
485 " C. {{{2
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 " PHP. {{{2
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 " Vim script. {{{2
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 " Exuberant Ctags doesn't mark script local functions in Vim scripts as
547 " "static". When your tags file contains search patterns this plug-in can use
548 " those search patterns to check which Vim script functions are defined
549 " globally and which script local.
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 " Python. {{{2
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 " Java. {{{2
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 " Restore "cpoptions".
599 let &cpo = s:cpo_save
600 unlet s:cpo_save
601 
602 " vim: ts=2 sw=2 et