From 27c29aa6a6b558b2f917a0c661fb4804bcdeb05e Mon Sep 17 00:00:00 2001 From: Peter Odding Date: Sun, 22 Jun 2014 03:22:36 +0200 Subject: Support for synchronous + asynchronous tags file updates (huge refactoring) See also pull request #49 for my previous and failed attempt: https://github.com/xolox/vim-easytags/pull/49 --- autoload/xolox/easytags.vim | 582 +++++----------------------------- autoload/xolox/easytags/filetypes.vim | 122 +++++++ autoload/xolox/easytags/update.vim | 268 ++++++++++++++++ autoload/xolox/easytags/utils.vim | 20 ++ 4 files changed, 483 insertions(+), 509 deletions(-) create mode 100644 autoload/xolox/easytags/filetypes.vim create mode 100644 autoload/xolox/easytags/update.vim create mode 100644 autoload/xolox/easytags/utils.vim (limited to 'autoload') diff --git a/autoload/xolox/easytags.vim b/autoload/xolox/easytags.vim index fd6127c..213175d 100644 --- a/autoload/xolox/easytags.vim +++ b/autoload/xolox/easytags.vim @@ -1,9 +1,9 @@ " Vim script " Author: Peter Odding -" Last Change: June 16, 2014 +" Last Change: June 22, 2014 " URL: http://peterodding.com/code/vim/easytags/ -let g:xolox#easytags#version = '3.4.4' +let g:xolox#easytags#version = '3.5' " Plug-in initialization. {{{1 @@ -115,50 +115,19 @@ endfunction " Public interface through (automatic) commands. {{{1 -" The localtime() when the CursorHold event last fired. -let s:last_automatic_run = 0 - function! xolox#easytags#autoload(event) " {{{2 try let do_update = xolox#misc#option#get('easytags_auto_update', 1) let do_highlight = xolox#misc#option#get('easytags_auto_highlight', 1) && &eventignore !~? '\' " Don't execute this function for unsupported file types (doesn't load " the list of file types if updates and highlighting are both disabled). - if (do_update || do_highlight) && !empty(xolox#easytags#select_supported_filetypes(&ft)) - if a:event =~? 'cursorhold' - " Only for the CursorHold automatic command: check for unreasonable - " &updatetime values. The minimum value 4000 is kind of arbitrary - " (apart from being Vim's default) so I made it configurable. - let updatetime_min = xolox#misc#option#get('easytags_updatetime_min', 4000) - if &updatetime < updatetime_min - if s:last_automatic_run == 0 - " Warn once about the low &updatetime value? - if xolox#misc#option#get('easytags_updatetime_warn', 1) - call xolox#misc#msg#warn("easytags.vim %s: The 'updatetime' option has an unreasonably low value, so I'll start compensating (see the easytags_updatetime_min option).", g:xolox#easytags#version) - endif - let s:last_automatic_run = localtime() - else - let next_scheduled_run = s:last_automatic_run + max([1, updatetime_min / 1000]) - if localtime() < next_scheduled_run - " It's not our time yet; wait for the next event. - call xolox#misc#msg#debug("easytags.vim %s: Skipping this beat of 'updatetime' to compensate for low value.", g:xolox#easytags#version) - " Shortcut to break out of xolox#easytags#autoload(). - return - else - call xolox#misc#msg#debug("easytags.vim %s: This is our beat of 'updatetime'!", g:xolox#easytags#version) - let s:last_automatic_run = localtime() - endif - endif - endif - endif + if (do_update || do_highlight) && !empty(xolox#easytags#filetypes#canonicalize(&filetype)) " Update entries for current file in tags file? if do_update - let pathname = s:resolve(expand('%:p')) - if pathname != '' - let tags_outdated = getftime(pathname) > getftime(xolox#easytags#get_tagsfile()) - if tags_outdated || !xolox#easytags#file_has_tags(pathname) - call xolox#easytags#update(1, 0, []) - endif + let buffer_read = (a:event =~? 'BufReadPost') + let buffer_written = (a:event =~? 'BufWritePost') + if buffer_written || (buffer_read && xolox#misc#option#get('easytags_always_enabled', 0)) + call xolox#easytags#update(1, 0, []) endif endif " Apply highlighting of tags to current buffer? @@ -182,40 +151,39 @@ function! xolox#easytags#autoload(event) " {{{2 endfunction function! xolox#easytags#update(silent, filter_tags, filenames) " {{{2 + let async = xolox#misc#option#get('easytags_async', 0) try - let context = s:create_context() let have_args = !empty(a:filenames) let starttime = xolox#misc#timer#start() let cfile = s:check_cfile(a:silent, a:filter_tags, have_args) let tagsfile = xolox#easytags#get_tagsfile() - let firstrun = !filereadable(tagsfile) - let cmdline = s:prep_cmdline(cfile, tagsfile, firstrun, a:filenames, context) - let [output, has_updates] = s:run_ctags(starttime, cfile, tagsfile, firstrun, cmdline) - if !firstrun - if !has_updates - return 1 - endif - if have_args && !empty(g:easytags_by_filetype) - " TODO Get the headers from somewhere?! - call s:save_by_filetype(a:filter_tags, [], output, context) - else - let num_filtered = s:filter_merge_tags(a:filter_tags, tagsfile, output, context) - endif - if cfile != '' - let msg = "easytags.vim %s: Updated tags for %s in %s." - call xolox#misc#timer#stop(msg, g:xolox#easytags#version, expand('%:p:~'), starttime) - elseif have_args - let msg = "easytags.vim %s: Updated tags in %s." - call xolox#misc#timer#stop(msg, g:xolox#easytags#version, starttime) - else - let msg = "easytags.vim %s: Filtered %i invalid tags in %s." - call xolox#misc#timer#stop(msg, g:xolox#easytags#version, num_filtered, starttime) - endif + let command_line = s:prep_cmdline(cfile, tagsfile, a:filenames) + if empty(command_line) + return endif - " When :UpdateTags was executed manually we'll refresh the dynamic - " syntax highlighting so that new tags are immediately visible. - if !a:silent && xolox#misc#option#get('easytags_auto_highlight', 1) - HighlightTags + " Pack all of the information required to update the tags in + " a Vim dictionary which is easy to serialize to a string. + let params = {} + let params['command'] = command_line + let params['ctags_version'] = g:easytags_ctags_version + let params['default_filetype'] = xolox#easytags#filetypes#canonicalize(&filetype) + let params['filter_tags'] = a:filter_tags || async + let params['have_args'] = have_args + if !empty(g:easytags_by_filetype) + let params['directory'] = xolox#misc#path#absolute(g:easytags_by_filetype) + let params['filetypes'] = g:xolox#easytags#filetypes#ctags_to_vim + else + let params['tagsfile'] = tagsfile + endif + if async + call xolox#misc#async#call({'function': 'xolox#easytags#update#with_vim', 'arguments': [params], 'callback': 'xolox#easytags#async_callback'}) + else + call s:report_results(xolox#easytags#update#with_vim(params), 0) + " When :UpdateTags was executed manually we'll refresh the dynamic + " syntax highlighting so that new tags are immediately visible. + if !a:silent && xolox#misc#option#get('easytags_auto_highlight', 1) + HighlightTags + endif endif return 1 catch @@ -229,57 +197,47 @@ function! s:check_cfile(silent, filter_tags, have_args) " {{{3 endif let silent = a:silent || a:filter_tags if xolox#misc#option#get('easytags_autorecurse', 0) - let cdir = s:resolve(expand('%:p:h')) + let cdir = xolox#easytags#utils#resolve(expand('%:p:h')) if !isdirectory(cdir) if silent | return '' | endif throw "The directory of the current file doesn't exist yet!" endif return cdir endif - let cfile = s:resolve(expand('%:p')) + let cfile = xolox#easytags#utils#resolve(expand('%:p')) if cfile == '' || !filereadable(cfile) if silent | return '' | endif throw "You'll need to save your file before using :UpdateTags!" elseif g:easytags_ignored_filetypes != '' && &ft =~ g:easytags_ignored_filetypes if silent | return '' | endif throw "The " . string(&ft) . " file type is explicitly ignored." - elseif empty(xolox#easytags#select_supported_filetypes(&ft)) + elseif empty(xolox#easytags#filetypes#canonicalize(&ft)) if silent | return '' | endif throw "Exuberant Ctags doesn't support the " . string(&ft) . " file type!" endif return cfile endfunction -function! s:prep_cmdline(cfile, tagsfile, firstrun, arguments, context) " {{{3 - let languages = xolox#misc#option#get('easytags_languages', {}) - let applicable_filetypes = xolox#easytags#select_supported_filetypes(&ft) - let ctags_language_name = xolox#easytags#to_ctags_ft(get(applicable_filetypes, 0, '')) - let language = get(languages, ctags_language_name, {}) +function! s:prep_cmdline(cfile, tagsfile, arguments) " {{{3 + let vim_file_type = xolox#easytags#filetypes#canonicalize(&filetype) + let custom_languages = xolox#misc#option#get('easytags_languages', {}) + let language = get(custom_languages, vim_file_type, {}) if empty(language) let program = xolox#misc#option#get('easytags_cmd') let cmdline = [program, '--fields=+l', '--c-kinds=+p', '--c++-kinds=+p'] - if a:firstrun - call add(cmdline, xolox#misc#escape#shell('-f' . a:tagsfile)) - call add(cmdline, '--sort=' . (&ic ? 'foldcase' : 'yes')) - else - call add(cmdline, '--sort=no') - call add(cmdline, '-f-') - endif + call add(cmdline, '--sort=no') + call add(cmdline, '-f-') if xolox#misc#option#get('easytags_include_members', 0) call add(cmdline, '--extra=+q') endif else let program = get(language, 'cmd', xolox#misc#option#get('easytags_cmd')) if empty(program) - call xolox#misc#msg#warn("easytags.vim %s: No 'cmd' defined for language '%s', and also no global default!", g:xolox#easytags#version, ctags_language_name) + call xolox#misc#msg#warn("easytags.vim %s: No 'cmd' defined for language '%s', and also no global default!", g:xolox#easytags#version, vim_file_type) return endif let cmdline = [program] + get(language, 'args', []) - if a:firstrun - call add(cmdline, xolox#misc#escape#shell(get(language, 'fileoutput_opt', '-f') . a:tagsfile)) - else - call add(cmdline, xolox#misc#escape#shell(get(language, 'stdout_opt', '-f-'))) - endif + call add(cmdline, xolox#misc#escape#shell(get(language, 'stdout_opt', '-f-'))) endif let have_args = 0 if a:cfile != '' @@ -290,7 +248,7 @@ function! s:prep_cmdline(cfile, tagsfile, firstrun, arguments, context) " {{{3 if empty(language) " TODO Should --language-force distinguish between C and C++? " TODO --language-force doesn't make sense for JavaScript tags in HTML files? - let filetype = xolox#easytags#to_ctags_ft(applicable_filetypes[0]) + let filetype = xolox#easytags#filetypes#to_ctags(vim_file_type) call add(cmdline, xolox#misc#escape#shell('--language-force=' . filetype)) endif call add(cmdline, xolox#misc#escape#shell(a:cfile)) @@ -304,7 +262,7 @@ function! s:prep_cmdline(cfile, tagsfile, firstrun, arguments, context) " {{{3 else let matches = split(expand(arg), "\n") if !empty(matches) - call map(matches, 'xolox#misc#escape#shell(s:canonicalize(v:val, a:context))') + call map(matches, 'xolox#misc#escape#shell(xolox#easytags#utils#canonicalize(v:val))') call extend(cmdline, matches) let have_args = 1 endif @@ -315,114 +273,11 @@ function! s:prep_cmdline(cfile, tagsfile, firstrun, arguments, context) " {{{3 return have_args ? join(cmdline) : '' endfunction -function! s:run_ctags(starttime, cfile, tagsfile, firstrun, cmdline) " {{{3 - let lines = [] - let has_updates = 1 - if a:cmdline != '' - call xolox#misc#msg#debug("easytags.vim %s: Executing %s.", g:xolox#easytags#version, a:cmdline) - let lines = xolox#misc#os#exec({'command': a:cmdline})['stdout'] - let has_updates = a:firstrun || s:has_updates(a:cfile, join(lines, "\n")) - if a:firstrun - if a:cfile != '' - call xolox#misc#timer#stop("easytags.vim %s: Created tags for %s in %s.", g:xolox#easytags#version, expand('%:p:~'), a:starttime) - else - call xolox#misc#timer#stop("easytags.vim %s: Created tags in %s.", g:xolox#easytags#version, a:starttime) - endif - return [[], 0] - endif - endif - return [xolox#easytags#parse_entries(lines), has_updates] -endfunction - -" Vim 7.3 now has the sha256() function. We use it below to recognize when the -" tags haven't changed from the last time we ran Exuberant Ctags on a file; in -" this case the tags file doesn't have to be written to disk which makes the -" plug-in much faster for a very common case. - -let s:fingerprints = {} - -function! s:has_updates(cfile, output) - if empty(a:cfile) - " The cache doesn't work when tags aren't created for the current file. - return 1 - endif - let fingerprint = s:get_fingerprint(a:cfile, a:output) - call xolox#misc#msg#debug("easytags.vim %s: Fingerprint of tags in %s is %s.", g:xolox#easytags#version, a:cfile, string(fingerprint)) - if !empty(fingerprint) && get(s:fingerprints, a:cfile, '') ==# fingerprint - call xolox#misc#msg#debug("easytags.vim %s: The fingerprint didn't change! We can take a shortcut :-)", g:xolox#easytags#version) - return 0 - endif - let s:fingerprints[a:cfile] = fingerprint - return 1 -endfunction - -if exists('*sha256') - function! s:get_fingerprint(cfile, output) - return sha256(a:output) - endfunction -else - function! s:get_fingerprint(cfile, output) - " Don't want to re-implement a costly hashing function in Vimscript. Just - " handle files that never had any tags. - if empty(a:output) - return get(s:fingerprints, a:cfile, 1) - else - return '' - endif - endfunction -endif - -function! s:filter_merge_tags(filter_tags, tagsfile, output, context) " {{{3 - let [headers, entries] = xolox#easytags#read_tagsfile(a:tagsfile) - let filters = [] - " Filter old tags that are to be replaced with the tags in {output}. - let tagged_files = s:find_tagged_files(a:output, a:context) - if !empty(tagged_files) - call add(filters, '!has_key(tagged_files, s:canonicalize(v:val[1], a:context))') - endif - " Filter tags for non-existing files? - if a:filter_tags - call add(filters, 'filereadable(v:val[1])') - endif - let num_old_entries = len(entries) - if !empty(filters) - " Apply the filters. - call filter(entries, join(filters, ' && ')) - endif - let num_filtered = num_old_entries - len(entries) - " Merge the old and new tags. - call extend(entries, a:output) - " Since we've already read the tags file we might as well cache the tagged - " files. We do so before saving the tags file so that the items in {entries} - " are not yet flattened by xolox#easytags#write_tagsfile(). - let fname = s:canonicalize(a:tagsfile, a:context) - call s:cache_tagged_files_in(fname, getftime(fname), entries, a:context) - " Now we're ready to save the tags file. - if !xolox#easytags#write_tagsfile(a:tagsfile, headers, entries) - let msg = "Failed to write filtered tags file %s!" - throw printf(msg, fnamemodify(a:tagsfile, ':~')) - endif - return num_filtered -endfunction - -function! s:find_tagged_files(entries, context) " {{{3 - let tagged_files = {} - for entry in a:entries - let filename = s:canonicalize(entry[1], a:context) - if filename != '' - if !has_key(tagged_files, filename) - let tagged_files[filename] = 1 - endif - endif - endfor - return tagged_files -endfunction - function! xolox#easytags#highlight() " {{{2 " TODO This is a mess; Re-implement Python version in Vim script, benchmark, remove Python version. try " Treat C++ and Objective-C as plain C. - let filetype = get(s:canonical_aliases, &ft, &ft) + let filetype = xolox#easytags#filetypes#canonicalize(&filetype) let tagkinds = get(s:tagkinds, filetype, []) if exists('g:syntax_on') && !empty(tagkinds) && !exists('b:easytags_nohl') let starttime = xolox#misc#timer#start() @@ -444,13 +299,9 @@ function! xolox#easytags#highlight() " {{{2 " Fall back to the slow and naive Vim script implementation. if !exists('taglist') " Get the list of tags when we need it and remember the results. - if !has_key(s:aliases, filetype) - let ctags_filetype = xolox#easytags#to_ctags_ft(filetype) - let taglist = filter(taglist('.'), "get(v:val, 'language', '') ==? ctags_filetype") - else - let aliases = s:aliases[&ft] - let taglist = filter(taglist('.'), "has_key(aliases, tolower(get(v:val, 'language', '')))") - endif + let ctags_filetypes = xolox#easytags#filetypes#find_ctags_aliases(filetype) + let filetypes_pattern = printf('^\(%s\)$', join(map(ctags_filetypes, 'xolox#misc#pattern#escape(v:val)'), '\|')) + let taglist = filter(taglist('.'), "get(v:val, 'language', '') =~? filetypes_pattern") endif " Filter a copy of the list of tags to the relevant kinds. if has_key(tagkind, 'tagkinds') @@ -489,236 +340,8 @@ function! xolox#easytags#highlight() " {{{2 endtry endfunction -function! xolox#easytags#by_filetype(undo) " {{{2 - try - if empty(g:easytags_by_filetype) - throw "Please set g:easytags_by_filetype before running :TagsByFileType!" - endif - let context = s:create_context() - let global_tagsfile = expand(g:easytags_file) - let disabled_tagsfile = global_tagsfile . '.disabled' - if !a:undo - let [headers, entries] = xolox#easytags#read_tagsfile(global_tagsfile) - call s:save_by_filetype(0, headers, entries, context) - call rename(global_tagsfile, disabled_tagsfile) - let msg = "easytags.vim %s: Finished copying tags from %s to %s! Note that your old tags file has been renamed to %s instead of deleting it, should you want to restore it." - call xolox#misc#msg#info(msg, g:xolox#easytags#version, g:easytags_file, g:easytags_by_filetype, disabled_tagsfile) - else - let headers = [] - let all_entries = [] - for tagsfile in split(glob(g:easytags_by_filetype . '/*'), '\n') - let [headers, entries] = xolox#easytags#read_tagsfile(tagsfile) - call extend(all_entries, entries) - endfor - call xolox#easytags#write_tagsfile(global_tagsfile, headers, all_entries) - call xolox#misc#msg#info("easytags.vim %s: Finished copying tags from %s to %s!", g:xolox#easytags#version, g:easytags_by_filetype, g:easytags_file) - endif - catch - call xolox#misc#msg#warn("easytags.vim %s: %s (at %s)", g:xolox#easytags#version, v:exception, v:throwpoint) - endtry -endfunction - -function! s:save_by_filetype(filter_tags, headers, entries, context) - let filetypes = {} - let num_invalid = 0 - for entry in a:entries - let ctags_ft = matchstr(entry[4], '^language:\zs\S\+$') - if empty(ctags_ft) - " TODO This triggers on entries where the pattern contains tabs. The interesting thing is that Vim reads these entries fine... Fix it in xolox#easytags#read_tagsfile()? - let num_invalid += 1 - if &vbs >= 1 - call xolox#misc#msg#debug("easytags.vim %s: Skipping tag without 'language:' field: %s", - \ g:xolox#easytags#version, string(entry)) - endif - else - let vim_ft = xolox#easytags#to_vim_ft(ctags_ft) - if !has_key(filetypes, vim_ft) - let filetypes[vim_ft] = [] - endif - call add(filetypes[vim_ft], entry) - endif - endfor - if num_invalid > 0 - call xolox#misc#msg#warn("easytags.vim %s: Skipped %i lines without 'language:' tag!", g:xolox#easytags#version, num_invalid) - endif - let directory = xolox#misc#path#absolute(g:easytags_by_filetype) - for vim_ft in keys(filetypes) - let tagsfile = xolox#misc#path#merge(directory, vim_ft) - let existing = filereadable(tagsfile) - call xolox#misc#msg#debug("easytags.vim %s: Writing %s tags to %s tags file %s.", - \ g:xolox#easytags#version, len(filetypes[vim_ft]), - \ existing ? "existing" : "new", tagsfile) - if !existing - call xolox#easytags#write_tagsfile(tagsfile, a:headers, filetypes[vim_ft]) - else - call s:filter_merge_tags(a:filter_tags, tagsfile, filetypes[vim_ft], a:context) - endif - endfor -endfunction - " Public supporting functions (might be useful to others). {{{1 -function! xolox#easytags#supported_filetypes() " {{{2 - if !exists('s:supported_filetypes') - let starttime = xolox#misc#timer#start() - let listing = [] - if !empty(g:easytags_cmd) - let command = g:easytags_cmd . ' --list-languages' - let listing = xolox#misc#os#exec({'command': command})['stdout'] - endif - let s:supported_filetypes = keys(xolox#misc#option#get('easytags_languages', {})) - for line in listing - if line =~ '\[disabled\]$' - " Ignore languages that have been explicitly disabled using `--languages=-Vim'. - continue - elseif line =~ '^\w\S*$' - call add(s:supported_filetypes, xolox#easytags#to_vim_ft(line)) - elseif line =~ '\S' - call xolox#misc#msg#warn("easytags.vim %s: Failed to parse line of output from ctags --list-languages: %s", g:xolox#easytags#version, string(line)) - endif - endfor - let msg = "easytags.vim %s: Retrieved %i supported languages in %s." - call xolox#misc#timer#stop(msg, g:xolox#easytags#version, len(s:supported_filetypes), starttime) - endif - return s:supported_filetypes -endfunction - -function! xolox#easytags#select_supported_filetypes(vim_ft) " {{{2 - let supported_filetypes = xolox#easytags#supported_filetypes() - let applicable_filetypes = [] - for ft in split(&filetype, '\.') - if index(supported_filetypes, ft) >= 0 - call add(applicable_filetypes, ft) - endif - endfor - return applicable_filetypes -endfunction - -function! xolox#easytags#read_tagsfile(tagsfile) " {{{2 - " I'm not sure whether this is by design or an implementation detail but - " it's possible for the "!_TAG_FILE_SORTED" header to appear after one or - " more tags and Vim will apparently still use the header! For this reason - " the xolox#easytags#write_tagsfile() function should also recognize it, - " otherwise Vim might complain with "E432: Tags file not sorted". - let headers = [] - let entries = [] - let num_invalid = 0 - for line in readfile(a:tagsfile) - if line =~# '^!_TAG_' - call add(headers, line) - else - let entry = xolox#easytags#parse_entry(line) - if !empty(entry) - call add(entries, entry) - else - let num_invalid += 1 - endif - endif - endfor - if num_invalid > 0 - call xolox#misc#msg#warn("easytags.vim %s: Ignored %i invalid line(s) in %s!", g:xolox#easytags#version, num_invalid, a:tagsfile) - endif - return [headers, entries] -endfunction - -function! xolox#easytags#parse_entry(line) " {{{2 - let fields = split(a:line, '\t') - return len(fields) >= 3 ? fields : [] -endfunction - -function! xolox#easytags#parse_entries(lines) " {{{2 - call map(a:lines, 'xolox#easytags#parse_entry(v:val)') - return filter(a:lines, '!empty(v:val)') -endfunction - -function! xolox#easytags#write_tagsfile(tagsfile, headers, entries) " {{{2 - " This function always sorts the tags file but understands "foldcase". - let sort_order = 1 - for line in a:headers - if match(line, '^!_TAG_FILE_SORTED\t2') == 0 - let sort_order = 2 - endif - endfor - call map(a:entries, 's:join_entry(v:val)') - if sort_order == 1 - call sort(a:entries) - else - call sort(a:entries, function('s:foldcase_compare')) - endif - let lines = [] - if xolox#misc#os#is_win() - " Exuberant Ctags on Windows requires \r\n but Vim's writefile() doesn't add them! - for line in a:headers - call add(lines, line . "\r") - endfor - for line in a:entries - call add(lines, line . "\r") - endfor - else - call extend(lines, a:headers) - call extend(lines, a:entries) - endif - let tempname = a:tagsfile . '.easytags.tmp' - return writefile(lines, tempname) == 0 && rename(tempname, a:tagsfile) == 0 -endfunction - -function! s:join_entry(value) - return type(a:value) == type([]) ? join(a:value, "\t") : a:value -endfunction - -function! s:foldcase_compare(a, b) - let a = toupper(a:a) - let b = toupper(a:b) - return a == b ? 0 : a ># b ? 1 : -1 -endfunction - -function! xolox#easytags#file_has_tags(filename) " {{{2 - " Check whether the given source file occurs in one of the tags files known - " to Vim. This function might not always give the right answer because of - " caching, but for the intended purpose that's no problem: When editing an - " existing file which has no tags defined the plug-in will run Exuberant - " Ctags to update the tags, *unless the file has already been tagged*. - call s:cache_tagged_files(s:create_context()) - return has_key(s:tagged_files, s:resolve(a:filename)) -endfunction - -if !exists('s:tagged_files') - let s:tagged_files = {} - let s:known_tagfiles = {} -endif - -function! s:cache_tagged_files(context) " {{{3 - if empty(s:tagged_files) - " Initialize the cache of tagged files on first use. After initialization - " we'll only update the cache when we're reading a tags file from disk for - " other purposes anyway (so the cache doesn't introduce too much overhead). - let starttime = xolox#misc#timer#start() - for tagsfile in tagfiles() - if !filereadable(tagsfile) - call xolox#misc#msg#warn("easytags.vim %s: Skipping unreadable tags file %s!", g:xolox#easytags#version, tagsfile) - else - let fname = s:canonicalize(tagsfile, a:context) - let ftime = getftime(fname) - if get(s:known_tagfiles, fname, 0) != ftime - let [headers, entries] = xolox#easytags#read_tagsfile(fname) - call s:cache_tagged_files_in(fname, ftime, entries, a:context) - endif - endif - endfor - call xolox#misc#timer#stop("easytags.vim %s: Initialized cache of tagged files in %s.", g:xolox#easytags#version, starttime) - endif -endfunction - -function! s:cache_tagged_files_in(fname, ftime, entries, context) " {{{3 - for entry in a:entries - let filename = s:canonicalize(entry[1], a:context) - if filename != '' - let s:tagged_files[filename] = 1 - endif - endfor - let s:known_tagfiles[a:fname] = a:ftime -endfunction - function! xolox#easytags#get_tagsfile() " {{{2 let tagsfile = '' " Look for a suitable project specific tags file? @@ -736,10 +359,10 @@ function! xolox#easytags#get_tagsfile() " {{{2 endif endif " Check if a file type specific tags file is useful? - let applicable_filetypes = xolox#easytags#select_supported_filetypes(&ft) - if empty(tagsfile) && !empty(g:easytags_by_filetype) && !empty(applicable_filetypes) + let vim_file_type = xolox#easytags#filetypes#canonicalize(&filetype) + if empty(tagsfile) && !empty(g:easytags_by_filetype) && !empty(vim_file_type) let directory = xolox#misc#path#absolute(g:easytags_by_filetype) - let tagsfile = xolox#misc#path#merge(directory, applicable_filetypes[0]) + let tagsfile = xolox#misc#path#merge(directory, vim_file_type) endif " Default to the global tags file? if empty(tagsfile) @@ -775,8 +398,14 @@ function! xolox#easytags#syntax_groups_to_ignore() " {{{2 call add(groups, 'doxygen.*') endif return join(groups, ',') -filetype +endfunction +function! xolox#easytags#async_callback(response) " {{{2 + if has_key(a:response, 'result') + call s:report_results(a:response['result'], 1) + else + call xolox#misc#msg#warn("easytags.vim %s: Asynchronous tags file update failed! (%s at %s)", g:xolox#easytags#version, a:response['exception'], a:response['throwpoint']) + endif endfunction " Public API for definition of file type specific dynamic syntax highlighting. {{{1 @@ -794,72 +423,20 @@ function! xolox#easytags#define_tagkind(object) " {{{2 call add(s:tagkinds[a:object.filetype], a:object) endfunction -function! xolox#easytags#map_filetypes(vim_ft, ctags_ft) " {{{2 - call add(s:vim_filetypes, a:vim_ft) - call add(s:ctags_filetypes, a:ctags_ft) -endfunction - -function! xolox#easytags#alias_filetypes(...) " {{{2 - " TODO Simplify alias handling, this much complexity really isn't needed! - for type in a:000 - let s:canonical_aliases[type] = a:1 - if !has_key(s:aliases, type) - let s:aliases[type] = {} - endif - endfor - for i in range(a:0) - for j in range(a:0) - let vimft1 = a:000[i] - let ctagsft1 = xolox#easytags#to_ctags_ft(vimft1) - let vimft2 = a:000[j] - let ctagsft2 = xolox#easytags#to_ctags_ft(vimft2) - if !has_key(s:aliases[vimft1], ctagsft2) - let s:aliases[vimft1][ctagsft2] = 1 - endif - if !has_key(s:aliases[vimft2], ctagsft1) - let s:aliases[vimft2][ctagsft1] = 1 - endif - endfor - endfor -endfunction - -function! xolox#easytags#to_vim_ft(ctags_ft) " {{{2 - let type = tolower(a:ctags_ft) - let index = index(s:ctags_filetypes, type) - return index >= 0 ? s:vim_filetypes[index] : type -endfunction - -function! xolox#easytags#to_ctags_ft(vim_ft) " {{{2 - let type = tolower(a:vim_ft) - let index = index(s:vim_filetypes, type) - return index >= 0 ? s:ctags_filetypes[index] : type -endfunction - " Miscellaneous script-local functions. {{{1 -function! s:create_context() " {{{2 - return {'cache': {}} -endfunction - -function! s:resolve(filename) " {{{2 - if xolox#misc#option#get('easytags_resolve_links', 0) - return resolve(a:filename) - else - return a:filename +function! s:report_results(response, async) " {{{1 + let actions = [] + if a:response['num_updated'] > 0 + call add(actions, printf('updated %i tags', a:response['num_updated'])) endif -endfunction - -function! s:canonicalize(filename, context) " {{{2 - if a:filename != '' - if has_key(a:context.cache, a:filename) - return a:context.cache[a:filename] - else - let canonical = s:resolve(fnamemodify(a:filename, ':p')) - let a:context.cache[a:filename] = canonical - return canonical - endif + if a:response['num_filtered'] > 0 + call add(actions, printf('filtered %i invalid tags', a:response['num_filtered'])) + endif + if !empty(actions) + let actions_string = xolox#misc#str#ucfirst(join(actions, ' and ')) + call xolox#misc#msg#info("easytags.vim %s: %s in %s (%s).", g:xolox#easytags#version, actions_string, a:response['elapsed_time'], a:async ? 'asynchronously' : 'synchronously') endif - return '' endfunction function! s:python_available() " {{{2 @@ -883,8 +460,8 @@ function! s:highlight_with_python(syntax_group, tagkind) " {{{2 let context = {} let context['tagsfiles'] = tagfiles() let context['syntaxgroup'] = a:syntax_group - let applicable_filetypes = xolox#easytags#select_supported_filetypes(&ft) - let context['filetype'] = xolox#easytags#to_ctags_ft(applicable_filetypes[0]) + " TODO This doesn't support file type groups! + let context['filetype'] = xolox#easytags#filetypes#to_ctags(xolox#easytags#filetypes#canonicalize(&filetype)) let context['tagkinds'] = get(a:tagkind, 'tagkinds', '') let context['prefix'] = get(a:tagkind, 'pattern_prefix', '') let context['suffix'] = get(a:tagkind, 'pattern_suffix', '') @@ -916,19 +493,6 @@ endif let s:tagkinds = {} -" Define the built-in Vim <=> Ctags file-type mappings. -let s:vim_filetypes = [] -let s:ctags_filetypes = [] -call xolox#easytags#map_filetypes('cpp', 'c++') -call xolox#easytags#map_filetypes('cs', 'c#') -call xolox#easytags#map_filetypes(exists('g:filetype_asp') ? g:filetype_asp : 'aspvbs', 'asp') - -" Define the Vim file-types that are aliased by default. -let s:aliases = {} -let s:canonical_aliases = {} -call xolox#easytags#alias_filetypes('c', 'cpp', 'objc', 'objcpp') -call xolox#easytags#alias_filetypes('html', 'htmldjango') - " Enable line continuation. let s:cpo_save = &cpo set cpo&vim diff --git a/autoload/xolox/easytags/filetypes.vim b/autoload/xolox/easytags/filetypes.vim new file mode 100644 index 0000000..d655ef7 --- /dev/null +++ b/autoload/xolox/easytags/filetypes.vim @@ -0,0 +1,122 @@ +" Vim script +" Author: Peter Odding +" Last Change: June 20, 2014 +" URL: http://peterodding.com/code/vim/easytags/ + +" This submodule of the vim-easytags plug-in translates between back and forth +" between Vim file types and Exuberant Ctags languages. This is complicated by +" a couple of things: +" +" - Vim allows file types to be combined like `filetype=c.doxygen'. +" +" - Some file types need to be canonicalized, for example the `htmldjango' +" Vim file type should be treated as the `html' Exuberant Ctags language. + +" Whether we've run Exuberant Ctags to discover the supported file types. +let s:discovered_filetypes = 0 + +" List of supported Vim file types. +let s:supported_filetypes = [] + +" Mapping of Exuberant Ctags languages to Vim file types and vice versa. +let g:xolox#easytags#filetypes#ctags_to_vim = {} +let g:xolox#easytags#filetypes#vim_to_ctags = {} + +" Mapping of Vim file types to canonical file types. +let s:canonical_filetypes = {} + +" Mapping of canonical Vim file types to their groups. +let s:filetype_groups = {} + +function! xolox#easytags#filetypes#add_group(...) " {{{1 + " Define a group of Vim file types whose tags should be stored together. + let canonical_filetype = tolower(a:1) + let s:filetype_groups[canonical_filetype] = a:000[1:] + for ft in s:filetype_groups[canonical_filetype] + let s:canonical_filetypes[tolower(ft)] = canonical_filetype + endfor +endfunction + +function! xolox#easytags#filetypes#add_mapping(vim_filetype, ctags_language) " {{{1 + " Map an Exuberant Ctags language to a Vim file type and vice versa. + let vim_filetype = tolower(a:vim_filetype) + let ctags_language = tolower(a:ctags_language) + let g:xolox#easytags#filetypes#ctags_to_vim[ctags_language] = vim_filetype + let g:xolox#easytags#filetypes#vim_to_ctags[vim_filetype] = ctags_language +endfunction + +function! xolox#easytags#filetypes#to_vim(ctags_language) " {{{1 + " Translate an Exuberant Ctags language to a Vim file type. + let ctags_language = tolower(a:ctags_language) + return get(g:xolox#easytags#filetypes#ctags_to_vim, ctags_language, ctags_language) +endfunction + +function! xolox#easytags#filetypes#to_ctags(vim_filetype) " {{{1 + " Translate a Vim file type to an Exuberant Ctags language. + let vim_filetype = tolower(a:vim_filetype) + return get(g:xolox#easytags#filetypes#vim_to_ctags, vim_filetype, vim_filetype) +endfunction + +function! xolox#easytags#filetypes#canonicalize(vim_filetype_value) " {{{1 + " Select a canonical, supported Vim file type given a value of &filetype. + call s:discover_supported_filetypes() + " Split the possibly combined Vim file type into individual file types. + for filetype in split(tolower(a:vim_filetype_value), '\.') + " Canonicalize the Vim file type. + let filetype = get(s:canonical_filetypes, filetype, filetype) + if index(s:supported_filetypes, filetype) >= 0 + return filetype + endif + endfor + return '' +endfunction + +function! xolox#easytags#filetypes#find_ctags_aliases(canonical_vim_filetype) " {{{1 + " Find Exuberant Ctags languages that correspond to a canonical, supported Vim file type. + if has_key(s:filetype_groups, a:canonical_vim_filetype) + let filetypes = copy(s:filetype_groups[a:canonical_vim_filetype]) + return map(filetypes, 'xolox#easytags#filetypes#to_ctags(v:val)') + else + return [xolox#easytags#filetypes#to_ctags(a:canonical_vim_filetype)] + endif +endfunction + +function! s:discover_supported_filetypes() " {{{1 + " Initialize predefined groups & mappings and discover supported file types. + if !s:discovered_filetypes + " Discover the file types supported by Exuberant Ctags? + if !empty(g:easytags_cmd) + let starttime = xolox#misc#timer#start() + let command = g:easytags_cmd . ' --list-languages' + for line in xolox#misc#os#exec({'command': command})['stdout'] + if line =~ '\[disabled\]$' + " Ignore languages that have been explicitly disabled using `--languages=-Vim'. + continue + elseif line =~ '^\w\S*$' + call add(s:supported_filetypes, xolox#easytags#filetypes#to_vim(xolox#misc#str#trim(line))) + elseif line =~ '\S' + call xolox#misc#msg#warn("easytags.vim %s: Failed to parse line of output from ctags --list-languages: %s", g:xolox#easytags#version, string(line)) + endif + endfor + let msg = "easytags.vim %s: Retrieved %i supported languages in %s." + call xolox#misc#timer#stop(msg, g:xolox#easytags#version, len(s:supported_filetypes), starttime) + endif + " Add file types supported by language specific programs. + call extend(s:supported_filetypes, keys(xolox#misc#option#get('easytags_languages', {}))) + " Don't run s:discover_supported_filetypes() more than once. + let s:discovered_filetypes = 1 + endif +endfunction + +" }}}1 + +" Define the default file type groups. +call xolox#easytags#filetypes#add_group('c', 'cpp', 'objc', 'objcpp') +call xolox#easytags#filetypes#add_group('html', 'htmldjango') + +" Define the default file type mappings. +call xolox#easytags#filetypes#add_mapping('cpp', 'c++') +call xolox#easytags#filetypes#add_mapping('cs', 'c#') +call xolox#easytags#filetypes#add_mapping(exists('g:filetype_asp') ? g:filetype_asp : 'aspvbs', 'asp') + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/easytags/update.vim b/autoload/xolox/easytags/update.vim new file mode 100644 index 0000000..eca86c3 --- /dev/null +++ b/autoload/xolox/easytags/update.vim @@ -0,0 +1,268 @@ +" Vim script +" Author: Peter Odding +" Last Change: June 22, 2014 +" URL: http://peterodding.com/code/vim/easytags/ + +" This Vim auto-load script contains the parts of vim-easytags that are used +" to update tags files. The vim-easytags plug-in can run this code in one of +" two ways: +" +" - Synchronously inside your main Vim process, blocking your editing session +" during the tags file update (not very nice as your tags files get larger +" and updating them becomes slower). +" +" - Asynchronously in a separate Vim process to update a tags file in the +" background without blocking your editing session (this provides a much +" nicer user experience). +" +" This code is kept separate from the rest of the plug-in to force me to use +" simple form of communication (a Vim dictionary with all of the state +" required to update tags files) which in the future can be used to implement +" an alternative update mechanism in a faster scripting language (for example +" I could translate the Vim dictionary to JSON and feed it to Python). + +function! xolox#easytags#update#with_vim(params) " {{{1 + let counters = {} + let starttime = xolox#misc#timer#start() + call xolox#misc#msg#debug("easytags.vim %s: Executing %s.", g:xolox#easytags#version, a:params['command']) + let lines = xolox#misc#os#exec({'command': a:params['command']})['stdout'] + let entries = xolox#easytags#update#parse_entries(lines) + let counters['num_updated'] = len(entries) + let directory = get(a:params, 'directory', '') + let cache = s:create_cache() + if !empty(directory) + let counters['num_filtered'] = s:save_by_filetype(a:params['filter_tags'], [], entries, cache, directory) + else + let counters['num_filtered'] = s:filter_merge_tags(a:params['filter_tags'], a:params['tagsfile'], entries, cache) + endif + let counters['elapsed_time'] = xolox#misc#timer#convert(starttime) + return counters +endfunction + +function! xolox#easytags#update#convert_by_filetype(undo) " {{{1 + try + if empty(g:easytags_by_filetype) + throw "Please set g:easytags_by_filetype before running :TagsByFileType!" + endif + let global_tagsfile = expand(g:easytags_file) + let disabled_tagsfile = global_tagsfile . '.disabled' + if !a:undo + let [headers, entries] = xolox#easytags#update#read_tagsfile(global_tagsfile) + call s:save_by_filetype(0, headers, entries) + call rename(global_tagsfile, disabled_tagsfile) + let msg = "easytags.vim %s: Finished copying tags from %s to %s! Note that your old tags file has been renamed to %s instead of deleting it, should you want to restore it." + call xolox#misc#msg#info(msg, g:xolox#easytags#version, g:easytags_file, g:easytags_by_filetype, disabled_tagsfile) + else + let headers = [] + let all_entries = [] + for tagsfile in split(glob(g:easytags_by_filetype . '/*'), '\n') + let [headers, entries] = xolox#easytags#update#read_tagsfile(tagsfile) + call extend(all_entries, entries) + endfor + call xolox#easytags#update#write_tagsfile(global_tagsfile, headers, all_entries) + call xolox#misc#msg#info("easytags.vim %s: Finished copying tags from %s to %s!", g:xolox#easytags#version, g:easytags_by_filetype, g:easytags_file) + endif + catch + call xolox#misc#msg#warn("easytags.vim %s: %s (at %s)", g:xolox#easytags#version, v:exception, v:throwpoint) + endtry +endfunction + +function! s:filter_merge_tags(filter_tags, tagsfile, output, cache) " {{{1 + let [headers, entries] = xolox#easytags#update#read_tagsfile(a:tagsfile) + let tagged_files = s:find_tagged_files(a:output, a:cache) + if !empty(tagged_files) + call filter(entries, '!has_key(tagged_files, a:cache.canonicalize(v:val[1]))') + endif + " Filter tags for non-existing files? + let count_before_filter = len(entries) + if a:filter_tags + call filter(entries, 'a:cache.exists(v:val[1])') + endif + let num_filtered = count_before_filter - len(entries) + " Merge the old and new tags. + call extend(entries, a:output) + " Now we're ready to save the tags file. + if !xolox#easytags#update#write_tagsfile(a:tagsfile, headers, entries) + let msg = "Failed to write filtered tags file %s!" + throw printf(msg, fnamemodify(a:tagsfile, ':~')) + endif + return num_filtered +endfunction + +function! s:find_tagged_files(entries, cache) " {{{1 + let tagged_files = {} + for entry in a:entries + let filename = a:cache.canonicalize(entry[1]) + if filename != '' + if !has_key(tagged_files, filename) + let tagged_files[filename] = 1 + endif + endif + endfor + return tagged_files +endfunction + +function! s:save_by_filetype(filter_tags, headers, entries, cache, directory) " {{{1 + let filetypes = {} + let num_invalid = 0 + let num_filtered = 0 + for entry in a:entries + let ctags_ft = matchstr(entry[4], '^language:\zs\S\+$') + if empty(ctags_ft) + " TODO This triggers on entries where the pattern contains tabs. The interesting thing is that Vim reads these entries fine... Fix it in xolox#easytags#update#read_tagsfile()? + let num_invalid += 1 + if &vbs >= 1 + call xolox#misc#msg#debug("easytags.vim %s: Skipping tag without 'language:' field: %s", + \ g:xolox#easytags#version, string(entry)) + endif + else + let vim_ft = xolox#easytags#filetypes#to_vim(ctags_ft) + if !has_key(filetypes, vim_ft) + let filetypes[vim_ft] = [] + endif + call add(filetypes[vim_ft], entry) + endif + endfor + if num_invalid > 0 + call xolox#misc#msg#warn("easytags.vim %s: Skipped %i lines without 'language:' tag!", g:xolox#easytags#version, num_invalid) + endif + let directory = xolox#misc#path#absolute(a:directory) + for vim_ft in keys(filetypes) + let tagsfile = xolox#misc#path#merge(directory, vim_ft) + let existing = filereadable(tagsfile) + call xolox#misc#msg#debug("easytags.vim %s: Writing %s tags to %s tags file %s.", + \ g:xolox#easytags#version, len(filetypes[vim_ft]), + \ existing ? "existing" : "new", tagsfile) + if !existing + call xolox#easytags#update#write_tagsfile(tagsfile, a:headers, filetypes[vim_ft]) + else + let num_filtered += s:filter_merge_tags(a:filter_tags, tagsfile, filetypes[vim_ft], a:cache) + endif + endfor + return num_filtered +endfunction + +function! xolox#easytags#update#read_tagsfile(tagsfile) " {{{1 + " I'm not sure whether this is by design or an implementation detail but + " it's possible for the "!_TAG_FILE_SORTED" header to appear after one or + " more tags and Vim will apparently still use the header! For this reason + " the xolox#easytags#update#write_tagsfile() function should also recognize it, + " otherwise Vim might complain with "E432: Tags file not sorted". + let headers = [] + let entries = [] + let num_invalid = 0 + for line in readfile(a:tagsfile) + if line =~# '^!_TAG_' + call add(headers, line) + else + let entry = xolox#easytags#update#parse_entry(line) + if !empty(entry) + call add(entries, entry) + else + let num_invalid += 1 + endif + endif + endfor + if num_invalid > 0 + call xolox#misc#msg#warn("easytags.vim %s: Ignored %i invalid line(s) in %s!", g:xolox#easytags#version, num_invalid, a:tagsfile) + endif + return [headers, entries] +endfunction + +function! xolox#easytags#update#parse_entry(line) " {{{1 + let fields = split(a:line, '\t') + return len(fields) >= 3 ? fields : [] +endfunction + +function! xolox#easytags#update#parse_entries(lines) " {{{1 + call map(a:lines, 'xolox#easytags#update#parse_entry(v:val)') + return filter(a:lines, '!empty(v:val)') +endfunction + +function! xolox#easytags#update#write_tagsfile(tagsfile, headers, entries) " {{{1 + " This function always sorts the tags file but understands "foldcase". + let sort_order = 0 + let sort_header_present = 0 + let sort_header_pattern = '^!_TAG_FILE_SORTED\t\zs\d' + " Discover the sort order defined in the tags file headers. + let i = 0 + for line in a:headers + let match = matchstr(line, sort_header_pattern) + if !empty(match) + let sort_header_present = 1 + let sort_order = match + 0 + if sort_order == 0 + let sort_order = 2 + let a:headers[i] = substitute(line, sort_header_pattern, '2', '') + endif + endif + endfor + if !sort_header_present + " If no sorting is defined in the tags file headers we default to + " "foldcase" sorting and add the header. + let sort_order = 2 + call add(a:headers, "!_TAG_FILE_SORTED\t2\t/0=unsorted, 1=sorted, 2=foldcase/") + endif + call xolox#easytags#update#join_entries(a:entries) + if sort_order == 1 + call sort(a:entries) + else + call sort(a:entries, function('xolox#easytags#update#foldcase_compare')) + endif + let lines = [] + if xolox#misc#os#is_win() + " Exuberant Ctags on Windows requires \r\n but Vim's writefile() doesn't add them! + for line in a:headers + call add(lines, line . "\r") + endfor + for line in a:entries + call add(lines, line . "\r") + endfor + else + call extend(lines, a:headers) + call extend(lines, a:entries) + endif + let tempname = a:tagsfile . '.easytags.tmp' + return writefile(lines, tempname) == 0 && rename(tempname, a:tagsfile) == 0 +endfunction + +function! s:enumerate(list) + let items = [] + let index = 0 + for item in a:list + call add(items, [index, item]) + let index += 1 + endfor + return items +endfunction + +function! xolox#easytags#update#join_entry(value) " {{{1 + return type(a:value) == type([]) ? join(a:value, "\t") : a:value +endfunction + +function! xolox#easytags#update#join_entries(values) " {{{1 + call map(a:values, 'xolox#easytags#update#join_entry(v:val)') + return filter(a:values, '!empty(v:val)') +endfunction + +function! xolox#easytags#update#foldcase_compare(a, b) " {{{1 + let a = toupper(a:a) + let b = toupper(a:b) + return a == b ? 0 : a ># b ? 1 : -1 +endfunction + +function! s:create_cache() " {{{1 + let cache = {'canonicalize_cache': {}, 'exists_cache': {}} + function cache.canonicalize(pathname) dict + if !has_key(self, a:pathname) + let self[a:pathname] = xolox#easytags#utils#canonicalize(a:pathname) + endif + return self[a:pathname] + endfunction + function cache.exists(pathname) dict + if !has_key(self, a:pathname) + let self[a:pathname] = filereadable(a:pathname) + endif + endfunction + return cache +endfunction diff --git a/autoload/xolox/easytags/utils.vim b/autoload/xolox/easytags/utils.vim new file mode 100644 index 0000000..a88c7a8 --- /dev/null +++ b/autoload/xolox/easytags/utils.vim @@ -0,0 +1,20 @@ +" Vim script +" Author: Peter Odding +" Last Change: June 20, 2014 +" URL: http://peterodding.com/code/vim/easytags/ + +" Utility functions for vim-easytags. + +function! xolox#easytags#utils#canonicalize(pathname) + if !empty(a:pathname) + return xolox#misc#path#absolute(xolox#easytags#utils#resolve(a:pathname)) + endif + return a:pathname +endfunction + +function! xolox#easytags#utils#resolve(pathname) + if !empty(a:pathname) && xolox#misc#option#get('easytags_resolve_links', 0) + return resolve(a:pathname) + endif + return a:pathname +endfunction -- cgit v1.2.3