From 27c29aa6a6b558b2f917a0c661fb4804bcdeb05e Mon Sep 17 00:00:00 2001
From: Peter Odding <peter@peterodding.com>
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/xolox')

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 <peter@peterodding.com>
-" 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 !~? '\<syntax\>'
     " 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 <peter@peterodding.com>
+" 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 <peter@peterodding.com>
+" 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 <peter@peterodding.com>
+" 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