From 0c57fc8380c809d2efd4bdb727874536569d489e Mon Sep 17 00:00:00 2001 From: Peter Odding Date: Sun, 6 Jun 2010 10:13:56 +0200 Subject: Initial commit --- Makefile | 41 ++++++++ README.md | 146 +++++++++++++++++++++++++++++ TODO.md | 7 ++ autoload.vim | 272 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ doc/README.footer | 2 + doc/README.header | 20 ++++ easytags.vim | 93 +++++++++++++++++++ 7 files changed, 581 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 TODO.md create mode 100644 autoload.vim create mode 100644 doc/README.footer create mode 100644 doc/README.header create mode 100644 easytags.vim diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3dada56 --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +DEPENDS=$(HOME)/.vim/autoload/xolox/escape.vim \ + $(HOME)/.vim/autoload/xolox/timer.vim +VIMDOC=doc/easytags.txt +HTMLDOC=doc/readme.html +ZIPDIR := $(shell mktemp -d) +ZIPFILE := $(shell mktemp -u) + +# NOTE: Make does NOT expand the following back ticks! +VERSION=`grep '^" Version:' easytags.vim | awk '{print $$3}'` + +# The main rule builds a ZIP that can be published to http://www.vim.org. +archive: Makefile easytags.vim autoload.vim $(VIMDOC) $(HTMLDOC) + @echo "Creating \`easytags-$(VERSION).zip' .." + @mkdir -p $(ZIPDIR)/plugin $(ZIPDIR)/autoload/xolox $(ZIPDIR)/doc + @cp easytags.vim $(ZIPDIR)/plugin + @cp autoload.vim $(ZIPDIR)/autoload/easytags.vim + @cp $(DEPENDS) $(ZIPDIR)/autoload/xolox + @cp $(VIMDOC) $(ZIPDIR)/doc/easytags.txt + @cp $(HTMLDOC) $(ZIPDIR)/doc/easytags.html + @cd $(ZIPDIR) && zip -r $(ZIPFILE) . >/dev/null + @rm -R $(ZIPDIR) + @mv $(ZIPFILE) easytags-$(VERSION).zip + +# This rule converts the Markdown README to Vim documentation. +$(VIMDOC): Makefile README.md + @echo "Creating \`$(VIMDOC)' .." + @mkd2vimdoc.py `basename $(VIMDOC)` < README.md > $(VIMDOC) + +# This rule converts the Markdown README to HTML, which reads easier. +$(HTMLDOC): Makefile README.md doc/README.header doc/README.footer + @echo "Creating \`$(HTMLDOC)' .." + @cat doc/README.header > $(HTMLDOC) + @cat README.md | markdown | SmartyPants >> $(HTMLDOC) + @cat doc/README.footer >> $(HTMLDOC) + +# This is only useful for myself, it uploads the latest README to my website. +web: $(HTMLDOC) + @echo "Uploading homepage .." + @scp -q $(HTMLDOC) vps:/home/peterodding.com/public/files/code/vim/easytags/index.html + +all: archive web diff --git a/README.md b/README.md new file mode 100644 index 0000000..bebb52f --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# Automated tag generation and syntax highlighting in Vim + +[Vim] [vim] has long been my favorite text editor and combined with [Exuberant +Ctags] [exuberant_ctags] it has the potential to provide most of what I expect +from an [integrated development environment] [ide]. Exuberant Ctags is the +latest incarnation of a [family of computer programs] [ctags] that scan +source code files to create an index of identifiers (tags) and where they are +defined. Vim uses this index (a so-called tags file) to enable you to jump to +the definition of any identifier using the `Ctrl-]` mapping. + +When you're familiar with integrated development environments you may recognize +this feature as "Go-to definition". One advantage of the combination of Vim and +Exuberant Ctags over integrated development environments is that Vim supports +syntax highlighting for [over 500 file types] [vim_support] (!) and Exuberant +Ctags can generate tags for [over 40 file types] [ctags_support] as well... + +There's just one problem: You have to manually keep your tags files up-to-date +and this turns about to be a royal pain in the ass! So I set out to write a Vim +plug-in that would do this boring work for me. When I finished the plug-in's +basic functionality (one automatic command and a call to `system()`) I became +interested in dynamic syntax highlighting, so I added that as well to see if it +would work -- surprisingly well I'm happy to report! + +## Install & first use + +Unzip the most recent [ZIP archive] [latest_zip] file inside your Vim profile +directory (usually this is `~/.vim` on UNIX and `%USERPROFILE%\vimfiles` on +Windows), restart Vim and try it out: Edit any file type supported by Exuberant +Ctags and within ten seconds the plug-in should create/update your tags file +(`~/.vimtags` on UNIX, `~/_vimtags` on Windows) with the tags defined in the +file you just edited! This means that whatever file you're editing in Vim (as +long as its on the local file system), tags will always be available by the +time you need them! + +Additionally if the file you just opened is a C, Lua, PHP, Python or Vim source +file you should also notice that the function and type names defined in the +file have been syntax highlighted. + +## Configuration + +The plug-in is intended to work without configuration but can be customized by +changing the following options: + +### The `easytags_file` option + +As mentioned above the plug-in will store your tags in `~/.vimtags` on UNIX and +`~/_vimtags` on Windows. To change the location of this file, set the global +variable `easytags_file`, e.g.: + + :let g:easytags_file = '~/.vim/tags' + +A leading `~` in the `easytags_file` variable is expanded to your current home +directory (`$HOME` on UNIX, `%USERPROFILE%` on Windows). + +### The `easytags_always_enabled` option + +By default the plug-in automatically generates and highlights tags when you +stop typing for a few seconds. This means that when you edit a file, the +dynamic highlighting won't appear until you pause for a moment. If you don't +want this you can configure the plug-in to always enable dynamic highlighting: + + :let g:easytags_always_enabled = 1 + +Be warned that after setting this option you'll probably notice why it's +disabled by default: Every time you edit a file in Vim, the plug-in will first +run Exuberant Ctags and then highlight the tags, which slows Vim down quite a +lot. I have some ideas on how to improve this latency by executing Exuberant +Ctags in the background, so stay tuned! + +Note: If you change this option it won't apply until you restart Vim, so you'll +have to set this option in your `~/.vimrc` script (`~/_vimrc` on Windows). + +### The `easytags_on_cursorhold` option + +As I explained above the plug-in by default doesn't update or highlight your +tags until you stop typing for a moment. The plug-in tries hard to do the least +amount of work possible in this break but it might still interrupt your +workflow. If it does you can disable the periodic update: + + :let g:easytags_on_cursorhold = 0 + +Note: Like the `easytags_always_enabled` option, if you change this option it +won't apply until you restart Vim, so you'll have to set this option in your +`~/.vimrc` script (`~/_vimrc` on Windows). + +### The `easytags_resolve_links` option + +UNIX has [symbolic links] [symlinks] and [hard links] [hardlinks], both of +which conflict with the concept of having one unique location for every +identifier. With regards to hard links there's not much anyone can do, but +because I use symbolic links quite a lot I've added this option. It's disabled +by default since it has a small performance impact and might not do what +unknowing users expect it to: When you enable this option the plug-in will +resolve symbolic links in pathnames, which means your tags file will only +contain entries with [canonical pathnames] [canon]. To enable this option +(which I strongly suggest doing when you run UNIX and use symbolic links) +execute the following Vim command: + + :let easytags_resolve_links = 1 + +## Troubleshooting + +Once or twice now in several years I've experienced Exuberant Ctags getting +into an infinite loop when given garbage input. In my case this happened by +accident a few days ago :-|. Because my plug-in executes `ctags` in the +foreground this will block Vim indefinitely! If this happens you might be +able to kill `ctags` by pressing `Ctrl-C` but if that doesn't work you can also +kill it without stopping Vim using a task manager or the `pkill` command: + + pkill -KILL ctags + +If Vim seems very slow and you suspect this plug-in might be the one to blame, +increase Vim's verbosity level: + + :set vbs=1 + +Every time the plug-in executes it will time how long the execution takes and +add the results to Vim's message history, which you can view by executing the +`:messages` command. + +## Contact + +If you have questions, bug reports, suggestions, etc. the author can be +contacted at . The latest version is available at + and +. If you like this plug-in please vote +for it on [www.vim.org] [vim_scripts_entry]. + +## License + +This software is licensed under the [MIT license] [mit_license]. +© 2010 Peter Odding <>. + + +[canon]: http://en.wikipedia.org/wiki/Canonicalization +[ctags]: http://en.wikipedia.org/wiki/Ctags +[ctags_support]: http://ctags.sourceforge.net/languages.html +[exuberant_ctags]: http://ctags.sourceforge.net/ +[hardlinks]: http://en.wikipedia.org/wiki/Hard_link +[ide]: http://en.wikipedia.org/wiki/Integrated_development_environment +[latest_zip]: http://github.com/downloads/xolox/vim-easytags/easytags-latest.zip +[mit_license]: http://en.wikipedia.org/wiki/MIT_License +[symlinks]: http://en.wikipedia.org/wiki/Symbolic_link +[vim]: http://www.vim.org/ +[vim_scripts_entry]: http://www.vim.org/scripts/script.php?script_id=3114 +[vim_support]: http://ftp.vim.org/vim/runtime/syntax/ diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6f64910 --- /dev/null +++ b/TODO.md @@ -0,0 +1,7 @@ +# Long term plans + + * Integration with my unreleased project plug-in so that when you edit any + file in a project, all related files are automatically scanned for tags? + + * Use separate tags files for each language stored in ~/.vim/tags/ to increase + performance because a single, global tags file quickly grows to a megabyte? diff --git a/autoload.vim b/autoload.vim new file mode 100644 index 0000000..5900b85 --- /dev/null +++ b/autoload.vim @@ -0,0 +1,272 @@ +" Vim script +" Maintainer: Peter Odding +" Last Change: June 6, 2010 +" URL: http://peterodding.com/code/vim/easytags + +function! easytags#autoload() " {{{1 + + " Update the entries for the current file in the global tags file? + let start = xolox#timer#start() + if getftime(expand('%')) > getftime(easytags#get_tagsfile()) + UpdateTags + call xolox#timer#stop(start, "easytags.vim: Automatically updated tags in %s second(s)") + endif + + " Apply highlighting of tags in global tags file to current buffer? + if &eventignore !~? '\' + let start = xolox#timer#start() + if !exists('b:easytags_last_highlighted') + HighlightTags + else + for tagfile in tagfiles() + if getftime(tagfile) > b:easytags_last_highlighted + HighlightTags + break + endif + endfor + endif + let b:easytags_last_highlighted = localtime() + call xolox#timer#stop(start, "easytags.vim: Automatically highlighted tags in %s second(s)") + endif + +endfunction + +function! easytags#update_cmd(filter_invalid_tags) " {{{1 + if !exists('s:supported_filetypes') + let start = xolox#timer#start() + let listing = system(g:easytags_cmd . ' --list-languages') + if v:shell_error + throw "Failed to get Exuberant Ctags language mappings!" + endif + let s:supported_filetypes = split(listing, '\n') + call map(s:supported_filetypes, 'easytags#to_vim_ft(v:val)') + call xolox#timer#stop(start, "easytags.vim: Parsed language mappings in %s second(s)") + endif + let supported_filetype = index(s:supported_filetypes, &ft) >= 0 + if supported_filetype || a:filter_invalid_tags + let start = xolox#timer#start() + let tagsfile = easytags#get_tagsfile() + let filename = expand('%:p') + if g:easytags_resolve_links + let filename = resolve(filename) + endif + let command = [g:easytags_cmd, '-f', shellescape(tagsfile)] + if filereadable(tagsfile) + call add(command, '-a') + let start_filter = xolox#timer#start() + let lines = readfile(tagsfile) + let filters = [] + if supported_filetype + call add(filters, 'v:val !~ ' . string('\s' . xolox#escape#pattern(filename) . '\s')) + endif + if a:filter_invalid_tags + call add(filters, 'filereadable(get(split(v:val, "\t"), 1))') + endif + let filter = 'v:val =~ "^!_TAG_" || (' . join(filters, ' && ') . ')' + let filtered = filter(copy(lines), filter) + if lines != filtered + call writefile(filtered, tagsfile) + endif + call xolox#timer#stop(start_filter, "easytags.vim: Filtered tags file in %s second(s)") + endif + if supported_filetype + call add(command, '--language-force=' . easytags#to_ctags_ft(&ft)) + call add(command, shellescape(filename)) + let listing = system(join(command)) + if v:shell_error + let message = "Failed to update tags file! (%s)" + throw printf(message, listing) + endif + endif + call xolox#timer#stop(start, "easytags.vim: Updated tags in %s second(s)") + endif +endfunction + +function! easytags#highlight_cmd() " {{{1 + if exists('g:syntax_on') && has_key(s:tagkinds, &ft) + let start = xolox#timer#start() + let taglist = filter(taglist('.'), "get(v:val, 'language', '') ==? &ft") + for tagkind in s:tagkinds[&ft] + let hlgroup_tagged = tagkind.hlgroup . 'Tag' + if hlexists(hlgroup_tagged) + execute 'syntax clear' hlgroup_tagged + else + execute 'highlight def link' hlgroup_tagged tagkind.hlgroup + endif + let matches = filter(copy(taglist), tagkind.filter) + call map(matches, 'xolox#escape#pattern(get(v:val, "name"))') + let pattern = tagkind.pattern_prefix . '\%(' . join(s:unique(matches), '\|') . '\)' . tagkind.pattern_suffix + let command = 'syntax match %s /%s/ containedin=ALLBUT,.*String.*,.*Comment.*' + execute printf(command, hlgroup_tagged, escape(pattern, '/')) + endfor + redraw + call xolox#timer#stop(start, "easytags.vim: Highlighted tags in %s second(s)") + endif +endfunction + +function! s:unique(list) + let index = 0 + while index < len(a:list) + let value = a:list[index] + let match = index(a:list, value, index+1) + if match >= 0 + call remove(a:list, match) + else + let index += 1 + endif + unlet value + endwhile + return a:list +endfunction + +function! easytags#get_tagsfile() " {{{1 + let tagsfile = expand(g:easytags_file) + if filereadable(tagsfile) && filewritable(tagsfile) != 1 + let message = "easytags.vim: The tags file isn't writable! (%s)" + echoerr printf(message, fnamemodify(directory, ':~')) + endif + return tagsfile +endfunction + +function! easytags#define_tagkind(object) " {{{1 + if !has_key(a:object, 'pattern_prefix') + let a:object.pattern_prefix = '\C\<' + endif + if !has_key(a:object, 'pattern_suffix') + let a:object.pattern_suffix = '\>' + endif + if !has_key(s:tagkinds, a:object.filetype) + let s:tagkinds[a:object.filetype] = [] + endif + call add(s:tagkinds[a:object.filetype], a:object) +endfunction + +function! easytags#map_filetypes(vim_ft, ctags_ft) " {{{1 + call add(s:vim_filetypes, a:vim_ft) + call add(s:ctags_filetypes, a:ctags_ft) +endfunction + +function! easytags#to_vim_ft(ctags_ft) " {{{1 + let type = tolower(a:ctags_ft) + let index = index(s:ctags_filetypes, type) + return index >= 0 ? s:vim_filetypes[index] : type +endfunction + +function! easytags#to_ctags_ft(vim_ft) " {{{1 + let type = tolower(a:vim_ft) + let index = index(s:vim_filetypes, type) + return index >= 0 ? s:ctags_filetypes[index] : type +endfunction + +" Built-in file type & tag kind definitions. {{{1 + +if !exists('s:tagkinds') + + let s:vim_filetypes = [] + let s:ctags_filetypes = [] + call easytags#map_filetypes('cpp', 'c++') + call easytags#map_filetypes('cs', 'c#') + call easytags#map_filetypes(exists('filetype_asp') ? filetype_asp : 'aspvbs', 'asp') + + let s:tagkinds = {} + + " Enable line continuation. + let s:cpo_save = &cpo + set cpo&vim + + " Lua. {{{2 + + call easytags#define_tagkind({ + \ 'filetype': 'lua', + \ 'hlgroup': 'luaFunc', + \ 'filter': 'get(v:val, "kind") ==# "f"'}) + + " C. {{{2 + + call easytags#define_tagkind({ + \ 'filetype': 'c', + \ 'hlgroup': 'cType', + \ 'filter': 'get(v:val, "kind") =~# "[cgstu]"'}) + + call easytags#define_tagkind({ + \ 'filetype': 'c', + \ 'hlgroup': 'cPreProc', + \ 'filter': 'get(v:val, "kind") ==# "d"'}) + + call easytags#define_tagkind({ + \ 'filetype': 'c', + \ 'hlgroup': 'cFunction', + \ 'filter': 'get(v:val, "kind") =~# "[fp]"'}) + + highlight def link cFunction Function + + " PHP. {{{2 + + call easytags#define_tagkind({ + \ 'filetype': 'php', + \ 'hlgroup': 'phpFunctions', + \ 'filter': 'get(v:val, "kind") ==# "f"'}) + + call easytags#define_tagkind({ + \ 'filetype': 'php', + \ 'hlgroup': 'phpClasses', + \ 'filter': 'get(v:val, "kind") ==# "c"'}) + + " Vim script. {{{2 + + call easytags#define_tagkind({ + \ 'filetype': 'vim', + \ 'hlgroup': 'vimAutoGroup', + \ 'filter': 'get(v:val, "kind") ==# "a"'}) + + highlight def link vimAutoGroup vimAutoEvent + + call easytags#define_tagkind({ + \ 'filetype': 'vim', + \ 'hlgroup': 'vimCommand', + \ 'filter': 'get(v:val, "kind") ==# "c"', + \ 'pattern_prefix': '\(\(^\|\s\):\?\)\@<=', + \ 'pattern_suffix': '\(!\?\(\s\|$\)\)\@='}) + + " Exuberant Ctags doesn't mark script local functions in Vim scripts as + " "static". When your tags file contains search patterns this plug-in can use + " those search patterns to check which Vim script functions are defined + " globally and which script local. + + call easytags#define_tagkind({ + \ 'filetype': 'vim', + \ 'hlgroup': 'vimFuncName', + \ 'filter': 'get(v:val, "kind") ==# "f" && get(v:val, "cmd") !~? ''\w\|\\)\@\w\|\\)'}) + + highlight def link vimScriptFuncName vimFuncName + + " Python. {{{2 + + call easytags#define_tagkind({ + \ 'filetype': 'python', + \ 'hlgroup': 'pythonFunction', + \ 'filter': 'get(v:val, "kind") ==# "f"', + \ 'pattern_prefix': '\%(\ == '!') +command! -bar HighlightTags call easytags#highlight_cmd() + +" Automatic commands. {{{1 + +augroup PluginEasyTags + autocmd! + if g:easytags_always_enabled + autocmd BufReadPost,BufWritePost * call easytags#autoload() + endif + if g:easytags_on_cursorhold + autocmd CursorHold,CursorHoldI * call easytags#autoload() + endif + autocmd User PublishPre HighlightTags +augroup END + +" }}}1 + +" Make sure the plug-in is only loaded once. +let g:loaded_easytags = 1 + +" vim: ts=2 sw=2 et -- cgit v1.2.3