diff options
Diffstat (limited to 'autoload')
-rw-r--r-- | autoload/xolox/easytags.vim | 1020 | ||||
-rw-r--r-- | autoload/xolox/misc/README.md | 49 | ||||
-rw-r--r-- | autoload/xolox/misc/buffer.vim | 51 | ||||
-rw-r--r-- | autoload/xolox/misc/compat.vim | 23 | ||||
-rw-r--r-- | autoload/xolox/misc/complete.vim | 18 | ||||
-rw-r--r-- | autoload/xolox/misc/escape.vim | 46 | ||||
-rw-r--r-- | autoload/xolox/misc/list.vim | 49 | ||||
-rw-r--r-- | autoload/xolox/misc/msg.vim | 83 | ||||
-rw-r--r-- | autoload/xolox/misc/open.vim | 70 | ||||
-rw-r--r-- | autoload/xolox/misc/option.vim | 84 | ||||
-rw-r--r-- | autoload/xolox/misc/os.vim | 30 | ||||
-rw-r--r-- | autoload/xolox/misc/path.vim | 201 | ||||
-rw-r--r-- | autoload/xolox/misc/str.vim | 12 | ||||
-rw-r--r-- | autoload/xolox/misc/timer.vim | 85 |
14 files changed, 1821 insertions, 0 deletions
diff --git a/autoload/xolox/easytags.vim b/autoload/xolox/easytags.vim new file mode 100644 index 0000000..02ae1e5 --- /dev/null +++ b/autoload/xolox/easytags.vim @@ -0,0 +1,1020 @@ +" Vim script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: April 20, 2013 +" URL: http://peterodding.com/code/vim/easytags/ + +let g:xolox#easytags#version = '3.1.2' + +call xolox#misc#compat#check('easytags', 1) + +" Public interface through (automatic) commands. {{{1 + +function! xolox#easytags#register(global) " {{{2 + " Parse the &tags option and get a list of all tags files *including + " non-existing files* (this is why we can't just call tagfiles()). + let tagfiles = xolox#misc#option#split_tags(&tags) + let expanded = map(copy(tagfiles), 'resolve(expand(v:val))') + " Add the filename to the &tags option when the user hasn't done so already. + let tagsfile = a:global ? g:easytags_file : xolox#easytags#get_tagsfile() + if index(expanded, xolox#misc#path#absolute(tagsfile)) == -1 + " This is a real mess because of bugs in Vim?! :let &tags = '...' doesn't + " work on UNIX and Windows, :set tags=... doesn't work on Windows. What I + " mean with "doesn't work" is that tagfiles() == [] after the :let/:set + " command even though the tags file exists! One easy way to confirm that + " this is a bug in Vim is to type :set tags= then press <Tab> followed by + " <CR>. Now you entered the exact same value that the code below also did + " but suddenly Vim sees the tags file and tagfiles() != [] :-S + call add(tagfiles, tagsfile) + let value = xolox#misc#option#join_tags(tagfiles) + let cmd = (a:global ? 'set' : 'setl') . ' tags=' . escape(value, '\ ') + if xolox#misc#os#is_win() && v:version < 703 + " TODO How to clear the expression from Vim's status line? + call feedkeys(":" . cmd . "|let &ro=&ro\<CR>", 'n') + else + execute cmd + endif + endif +endfunction + +" The localtime() when the CursorHold event last fired. +let s:last_automatic_run = 0 + +function! xolox#easytags#autoload(event) " {{{2 + try + 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. + 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) + 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) + 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 + 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) && index(xolox#easytags#supported_filetypes(), &ft) >= 0 + " 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 + endif + endif + " Apply highlighting of tags to current buffer? + if do_highlight + if !exists('b:easytags_last_highlighted') + call xolox#easytags#highlight() + else + for tagfile in tagfiles() + if getftime(tagfile) > b:easytags_last_highlighted + call xolox#easytags#highlight() + break + endif + endfor + endif + let b:easytags_last_highlighted = localtime() + endif + endif + catch + call xolox#misc#msg#warn("easytags.vim %s: %s (at %s)", g:xolox#easytags#version, v:exception, v:throwpoint) + endtry +endfunction + +function! xolox#easytags#update(silent, filter_tags, filenames) " {{{2 + 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 + endif + " When :UpdateTags was executed manually we'll refresh the dynamic + " syntax highlighting so that new tags are immediately visible. + if !a:silent + HighlightTags + endif + return 1 + catch + call xolox#misc#msg#warn("easytags.vim %s: %s (at %s)", g:xolox#easytags#version, v:exception, v:throwpoint) + endtry +endfunction + +function! s:check_cfile(silent, filter_tags, have_args) " {{{3 + if a:have_args + return '' + endif + let silent = a:silent || a:filter_tags + if xolox#misc#option#get('easytags_autorecurse', 0) + let cdir = s: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')) + 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 index(xolox#easytags#supported_filetypes(), &ft) == -1 + 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 ctags_language_name = xolox#easytags#to_ctags_ft(&filetype) + let language = get(languages, ctags_language_name, {}) + 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 + 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) + 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 + endif + let have_args = 0 + if a:cfile != '' + if xolox#misc#option#get('easytags_autorecurse', 0) + call add(cmdline, empty(language) ? '-R' : xolox#misc#escape#shell(get(language, 'recurse_flag', '-R'))) + call add(cmdline, xolox#misc#escape#shell(a:cfile)) + else + 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(&filetype) + call add(cmdline, xolox#misc#escape#shell('--language-force=' . filetype)) + endif + call add(cmdline, xolox#misc#escape#shell(a:cfile)) + endif + let have_args = 1 + else + for arg in a:arguments + if arg =~ '^-' + call add(cmdline, arg) + let have_args = 1 + else + let matches = split(expand(arg), "\n") + if !empty(matches) + call map(matches, 'xolox#misc#escape#shell(s:canonicalize(v:val, a:context))') + call extend(cmdline, matches) + let have_args = 1 + endif + endif + endfor + endif + " No need to run Exuberant Ctags without any filename arguments! + 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) + try + let lines = xolox#shell#execute(a:cmdline, 1) + let has_updates = a:firstrun || s:has_updates(a:cfile, join(lines, "\n")) + catch /^Vim\%((\a\+)\)\=:E117/ + " Ignore missing shell.vim plug-in. + let output = system(a:cmdline) + if v:shell_error + let msg = "Failed to update tags file %s: %s!" + throw printf(msg, fnamemodify(a:tagsfile, ':~'), strtrans(output)) + endif + let lines = split(output, "\n") + let has_updates = a:firstrun || s:has_updates(a:cfile, output) + endtry + 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 [] + 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 old/new tags and write tags file. + call extend(entries, a:output) + 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 + " We've already read the tags file, might as well cache the tagged files :-) + let fname = s:canonicalize(a:tagsfile, a:context) + call s:cache_tagged_files_in(fname, getftime(fname), entries, a:context) + 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 tagkinds = get(s:tagkinds, filetype, []) + if exists('g:syntax_on') && !empty(tagkinds) && !exists('b:easytags_nohl') + let starttime = xolox#misc#timer#start() + let used_python = 0 + for tagkind in tagkinds + let hlgroup_tagged = tagkind.hlgroup . 'Tag' + " Define style on first run, clear highlighting on later runs. + if !hlexists(hlgroup_tagged) + execute 'highlight def link' hlgroup_tagged tagkind.hlgroup + else + execute 'syntax clear' hlgroup_tagged + endif + " Try to perform the highlighting using the fast Python script. + " TODO The tags files are read multiple times by the Python script + " within one run of xolox#easytags#highlight() + if s:highlight_with_python(hlgroup_tagged, tagkind) + let used_python = 1 + else + " 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 + endif + " Filter a copy of the list of tags to the relevant kinds. + if has_key(tagkind, 'tagkinds') + let filter = 'v:val.kind =~ tagkind.tagkinds' + else + let filter = tagkind.vim_filter + endif + let matches = filter(copy(taglist), filter) + if matches != [] + " Convert matched tags to :syntax command and execute it. + let matches = xolox#misc#list#unique(map(matches, 'xolox#misc#escape#pattern(get(v:val, "name"))')) + let pattern = tagkind.pattern_prefix . '\%(' . join(matches, '\|') . '\)' . tagkind.pattern_suffix + let template = 'syntax match %s /%s/ containedin=ALLBUT,%s' + let command = printf(template, hlgroup_tagged, escape(pattern, '/'), xolox#misc#option#get('easytags_ignored_syntax_groups')) + call xolox#misc#msg#debug("easytags.vim %s: Executing command '%s'.", g:xolox#easytags#version, command) + try + execute command + catch /^Vim\%((\a\+)\)\=:E339/ + let msg = "easytags.vim %s: Failed to highlight %i %s tags because pattern is too big! (%i KB)" + call xolox#misc#msg#warn(msg, g:xolox#easytags#version, len(matches), tagkind.hlgroup, len(pattern) / 1024) + endtry + endif + endif + endfor + redraw + let bufname = expand('%:p:~') + if bufname == '' + let bufname = 'unnamed buffer #' . bufnr('%') + endif + let msg = "easytags.vim %s: Highlighted tags in %s in %s%s." + call xolox#misc#timer#stop(msg, g:xolox#easytags#version, bufname, starttime, used_python ? " (using Python)" : "") + return 1 + endif + catch + call xolox#misc#msg#warn("easytags.vim %s: %s (at %s)", g:xolox#easytags#version, v:exception, v:throwpoint) + 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' + try + let listing = xolox#shell#execute(command, 1) + catch /^Vim\%((\a\+)\)\=:E117/ + " Ignore missing shell.vim plug-in. + let listing = split(system(command), "\n") + if v:shell_error + let msg = "Failed to get supported languages! (output: %s)" + throw printf(msg, strtrans(join(listing, "\n"))) + endif + endtry + endif + let s:supported_filetypes = map(copy(listing) + keys(xolox#misc#option#get('easytags_languages', {})), 's:check_filetype(listing, v:val)') + 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! s:check_filetype(listing, cline) + if a:cline !~ '^\w\S*$' + let msg = "Failed to get supported languages! (output: %s)" + throw printf(msg, strtrans(join(a:listing, "\n"))) + endif + return xolox#easytags#to_vim_ft(a:cline) +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? + let dynamic_files = xolox#misc#option#get('easytags_dynamic_files', 0) + if dynamic_files == 1 + let tagsfile = get(tagfiles(), 0, '') + elseif dynamic_files == 2 + let tagsfile = xolox#misc#option#eval_tags(&tags, 1) + endif + " Check if a file type specific tags file is useful? + if empty(tagsfile) && !empty(g:easytags_by_filetype) && index(xolox#easytags#supported_filetypes(), &ft) >= 0 + let directory = xolox#misc#path#absolute(g:easytags_by_filetype) + let tagsfile = xolox#misc#path#merge(directory, &filetype) + endif + " Default to the global tags file? + if empty(tagsfile) + let tagsfile = expand(xolox#misc#option#get('easytags_file')) + endif + " If the tags file exists, make sure it is writable! + if filereadable(tagsfile) && filewritable(tagsfile) != 1 + let message = "The tags file %s isn't writable!" + throw printf(message, fnamemodify(tagsfile, ':~')) + endif + return tagsfile +endfunction + +" Public API for definition of file type specific dynamic syntax highlighting. {{{1 + +function! xolox#easytags#define_tagkind(object) " {{{2 + 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! 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 + 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 + endif + return '' +endfunction + +function! s:python_available() " {{{2 + if !exists('s:is_python_available') + try + execute 'pyfile' fnameescape(g:easytags_python_script) + redir => output + silent python easytags_ping() + redir END + let s:is_python_available = (output =~ 'it works!') + catch + let s:is_python_available = 0 + endtry + endif + return s:is_python_available +endfunction + +function! s:highlight_with_python(syntax_group, tagkind) " {{{2 + if xolox#misc#option#get('easytags_python_enabled', 1) && s:python_available() + " Gather arguments for Python function. + let context = {} + let context['tagsfiles'] = tagfiles() + let context['syntaxgroup'] = a:syntax_group + let context['filetype'] = xolox#easytags#to_ctags_ft(&ft) + let context['tagkinds'] = get(a:tagkind, 'tagkinds', '') + let context['prefix'] = get(a:tagkind, 'pattern_prefix', '') + let context['suffix'] = get(a:tagkind, 'pattern_suffix', '') + let context['filters'] = get(a:tagkind, 'python_filter', {}) + let context['ignoresyntax'] = xolox#misc#option#get('easytags_ignored_syntax_groups') + " Call the Python function and intercept the output. + try + redir => commands + python import vim + silent python print easytags_gensyncmd(**vim.eval('context')) + redir END + execute commands + return 1 + catch + redir END + " If the Python script raised an error, don't run it again. + let g:easytags_python_enabled = 0 + endtry + endif + return 0 +endfunction + +" Built-in file type & tag kind definitions. {{{1 + +" Don't bother redefining everything below when this script is sourced again. +if exists('s:tagkinds') + finish +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') + +" Enable line continuation. +let s:cpo_save = &cpo +set cpo&vim + +" Lua. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'lua', + \ 'hlgroup': 'luaFunc', + \ 'tagkinds': 'f'}) + +" C. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'c', + \ 'hlgroup': 'cType', + \ 'tagkinds': '[cgstu]'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'c', + \ 'hlgroup': 'cEnum', + \ 'tagkinds': 'e'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'c', + \ 'hlgroup': 'cPreProc', + \ 'tagkinds': 'd'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'c', + \ 'hlgroup': 'cFunction', + \ 'tagkinds': '[fp]'}) + +highlight def link cEnum Identifier +highlight def link cFunction Function + +if xolox#misc#option#get('easytags_include_members', 0) + call xolox#easytags#define_tagkind({ + \ 'filetype': 'c', + \ 'hlgroup': 'cMember', + \ 'tagkinds': 'm'}) + highlight def link cMember Identifier +endif + +" PHP. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'php', + \ 'hlgroup': 'phpFunctions', + \ 'tagkinds': 'f', + \ 'pattern_suffix': '(\@='}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'php', + \ 'hlgroup': 'phpClasses', + \ 'tagkinds': 'c'}) + +" Vim script. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'vim', + \ 'hlgroup': 'vimAutoGroup', + \ 'tagkinds': 'a'}) + +highlight def link vimAutoGroup vimAutoEvent + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'vim', + \ 'hlgroup': 'vimCommand', + \ 'tagkinds': '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 xolox#easytags#define_tagkind({ + \ 'filetype': 'vim', + \ 'hlgroup': 'vimFuncName', + \ 'vim_filter': 'v:val.kind ==# "f" && get(v:val, "cmd", "") !~? ''<sid>\w\|\<s:\w''', + \ 'python_filter': { 'kind': 'f', 'nomatch': '(?i)(<sid>\w|\bs:\w)' }, + \ 'pattern_prefix': '\C\%(\<s:\|<[sS][iI][dD]>\)\@<!\<'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'vim', + \ 'hlgroup': 'vimScriptFuncName', + \ 'vim_filter': 'v:val.kind ==# "f" && get(v:val, "cmd", "") =~? ''<sid>\w\|\<s:\w''', + \ 'python_filter': { 'kind': 'f', 'match': '(?i)(<sid>\w|\bs:\w)' }, + \ 'pattern_prefix': '\C\%(\<s:\|<[sS][iI][dD]>\)'}) + +highlight def link vimScriptFuncName vimFuncName + +" Python. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'python', + \ 'hlgroup': 'pythonFunction', + \ 'tagkinds': 'f', + \ 'pattern_prefix': '\%(\<def\s\+\)\@<!\<'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'python', + \ 'hlgroup': 'pythonMethod', + \ 'tagkinds': 'm', + \ 'pattern_prefix': '\.\@<='}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'python', + \ 'hlgroup': 'pythonClass', + \ 'tagkinds': 'c'}) + +highlight def link pythonMethodTag pythonFunction +highlight def link pythonClassTag pythonFunction + +" Java. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'java', + \ 'hlgroup': 'javaClass', + \ 'tagkinds': 'c'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'java', + \ 'hlgroup': 'javaMethod', + \ 'tagkinds': 'm'}) + +highlight def link javaClass Identifier +highlight def link javaMethod Function + +" C#. {{{2 + +" TODO C# name spaces, interface names, enumeration member names, structure names? + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'cs', + \ 'hlgroup': 'csClassOrStruct', + \ 'tagkinds': 'c'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'cs', + \ 'hlgroup': 'csMethod', + \ 'tagkinds': '[ms]'}) + +highlight def link csClassOrStruct Identifier +highlight def link csMethod Function + +" Ruby. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'ruby', + \ 'hlgroup': 'rubyModuleName', + \ 'tagkinds': 'm'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'ruby', + \ 'hlgroup': 'rubyClassName', + \ 'tagkinds': 'c'}) + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'ruby', + \ 'hlgroup': 'rubyMethodName', + \ 'tagkinds': '[fF]'}) + +highlight def link rubyModuleName Type +highlight def link rubyClassName Type +highlight def link rubyMethodName Function + +" Awk. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'awk', + \ 'hlgroup': 'awkFunctionTag', + \ 'tagkinds': 'f'}) + +highlight def link awkFunctionTag Function + +" Shell. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'sh', + \ 'hlgroup': 'shFunctionTag', + \ 'tagkinds': 'f', + \ 'pattern_suffix': '\(\w\|\s*()\)\@!'}) + +highlight def link shFunctionTag Operator + +" Tcl. {{{2 + +call xolox#easytags#define_tagkind({ + \ 'filetype': 'tcl', + \ 'hlgroup': 'tclCommandTag', + \ 'tagkinds': 'p'}) + +highlight def link tclCommandTag Operator + +" }}} + +" Restore "cpoptions". +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/README.md b/autoload/xolox/misc/README.md new file mode 100644 index 0000000..f375fe6 --- /dev/null +++ b/autoload/xolox/misc/README.md @@ -0,0 +1,49 @@ +# Miscellaneous auto-load Vim scripts + +The git repository at [github.com/xolox/vim-misc] [repository] contains Vim scripts that are used by most of the [Vim plug-ins I've written] [plugins] yet don't really belong with any single one. I include this repository as a subdirectory of my plug-in repositories using the following commands: + + $ git remote add -f vim-misc https://github.com/xolox/vim-misc.git + $ git merge -s ours --no-commit vim-misc/master + $ git read-tree --prefix=autoload/xolox/misc/ -u vim-misc/master + $ git commit -m "Merge vim-misc repository as subdirectory" + +The above trick is called the [subtree merge strategy] [merge-strategy]. To update a plug-in repository to the latest version of the miscellaneous auto-load scripts I execute the following command: + + $ git pull -s subtree vim-misc master + +## Why make things so complex? + +I came up with this solution after multiple years of back and forth between Vim Online users, the GitHub crowd and my own sanity: + +1. When I started publishing my first Vim plug-ins I would prepare ZIP archives for Vim Online using makefiles. The makefiles would make sure the miscellaneous scripts were included in the uploaded distributions. This had two disadvantages: It lost git history and the repositories on GitHub were not usable out of the box, so [I got complaints from GitHub (Pathogen) users] [github-complaints]. + +2. My second attempt to solve the problem used git submodules which seemed like the ideal solution until I actually started doing it. Submodules are not initialized during a normal `git clone`, you need to use `git clone --recursive` instead but Vim plug-in managers like [Pathogen] [pathogen] and [Vundle] [vundle] don't do this (at least [they didn't when I tried] [vundle-discussion]) so people would end up with broken checkouts. + +3. After finding out that git submodules were not going to solve my problems I searched for other inclusion strategies supported by git. After a while I came upon the [subtree merge strategy] [merge-strategy] which I have been using for more than two years now. + +## Compatibility issues + +Regardless of the inclusion strategies discussed above, my current scheme has a flaw: If more than one of my plug-ins are installed in a Vim profile using [Pathogen] [pathogen] or [Vundle] [vundle], the miscellaneous autoload scripts will all be loaded from the subdirectory of one single plug-in. + +This means that when I break compatibility in the miscellaneous scripts, I have to make sure to merge the changes into all of my plug-ins. Even then, if a user has more than one of my plug-ins installed but updates only one of them, the other plug-ins (that are not yet up to date) can break (because of the backwards incompatible change). + +The `xolox#misc#compat#check()` function makes sure that incompatibilities are detected early so that the user knows which plug-in to update if incompatibilities arise. + +## Contact + +If you have questions, bug reports, suggestions, etc. the author can be contacted at <peter@peterodding.com>. The latest version is available at <http://peterodding.com/code/vim/misc> and <http://github.com/xolox/vim-misc>. + +## License + +This software is licensed under the [MIT license] [mit]. +© 2013 Peter Odding <<peter@peterodding.com>>. + + +[github-complaints]: https://github.com/xolox/vim-easytags/issues/1 +[merge-strategy]: http://www.kernel.org/pub/software/scm/git/docs/howto/using-merge-subtree.html +[mit]: http://en.wikipedia.org/wiki/MIT_License +[pathogen]: http://www.vim.org/scripts/script.php?script_id=2332 +[plugins]: http://peterodding.com/code/vim/ +[repository]: https://github.com/xolox/vim-misc +[vundle-discussion]: https://github.com/gmarik/vundle/pull/41 +[vundle]: https://github.com/gmarik/vundle diff --git a/autoload/xolox/misc/buffer.vim b/autoload/xolox/misc/buffer.vim new file mode 100644 index 0000000..3597cc2 --- /dev/null +++ b/autoload/xolox/misc/buffer.vim @@ -0,0 +1,51 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: April 18, 2013 +" URL: http://peterodding.com/code/vim/misc/ + +function! xolox#misc#buffer#is_empty() " {{{1 + " Check if the current buffer is an empty, unchanged buffer which can be reused. + return !&modified && expand('%') == '' && line('$') <= 1 && getline(1) == '' +endfunction + +function! xolox#misc#buffer#prepare(...) " {{{1 + " Open a special buffer (with generated contents, not directly edited by the user). + if a:0 == 1 && type(a:1) == type('') + " Backwards compatibility with old interface. + let options = {'name': a:1, 'path': a:1} + elseif type(a:1) == type({}) + let options = a:1 + else + throw "Invalid arguments" + endif + let winnr = 1 + let found = 0 + for bufnr in tabpagebuflist() + if xolox#misc#path#equals(options['path'], bufname(bufnr)) + execute winnr . 'wincmd w' + let found = 1 + break + else + let winnr += 1 + endif + endfor + if !(found || xolox#misc#buffer#is_empty()) + vsplit + endif + silent execute 'edit' fnameescape(options['path']) + lcd " clear working directory + setlocal buftype=nofile bufhidden=hide noswapfile + let &l:statusline = '[' . options['name'] . ']' + call xolox#misc#buffer#unlock() + silent %delete +endfunction + +function! xolox#misc#buffer#lock() " {{{1 + " Lock a special buffer so it can no longer be edited. + setlocal readonly nomodifiable nomodified +endfunction + +function! xolox#misc#buffer#unlock() " {{{1 + " Unlock a special buffer so that its content can be updated. + setlocal noreadonly modifiable +endfunction diff --git a/autoload/xolox/misc/compat.vim b/autoload/xolox/misc/compat.vim new file mode 100644 index 0000000..83d00a0 --- /dev/null +++ b/autoload/xolox/misc/compat.vim @@ -0,0 +1,23 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: April 20, 2013 +" URL: http://peterodding.com/code/vim/misc/ + +" The following integer will be bumped whenever a change in the miscellaneous +" scripts breaks backwards compatibility. This enables my Vim plug-ins to fail +" early when they detect an incompatible version, instead of breaking at the +" worst possible moments :-). +let g:xolox#misc#compat#version = 1 + +" Remember the directory where the miscellaneous scripts are loaded from +" so the user knows which plug-in to update if incompatibilities arise. +let s:misc_directory = fnamemodify(expand('<sfile>'), ':p:h') + +function! xolox#misc#compat#check(plugin_name, required_version) + if a:required_version != g:xolox#misc#compat#version + let msg = "The %s plug-in requires version %i of the miscellaneous scripts, however version %i was loaded from %s!" + throw printf(msg, a:plugin_name, a:required_version, g:xolox#misc#compat#version, s:misc_directory) + endif +endfunction + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/complete.vim b/autoload/xolox/misc/complete.vim new file mode 100644 index 0000000..2ada676 --- /dev/null +++ b/autoload/xolox/misc/complete.vim @@ -0,0 +1,18 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: March 15, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +" Keyword completion from the current buffer for user defined commands. + +function! xolox#misc#complete#keywords(arglead, cmdline, cursorpos) + let words = {} + for line in getline(1, '$') + for word in split(line, '\W\+') + let words[word] = 1 + endfor + endfor + return sort(keys(filter(words, 'v:key =~# a:arglead'))) +endfunction + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/escape.vim b/autoload/xolox/misc/escape.vim new file mode 100644 index 0000000..1dd1838 --- /dev/null +++ b/autoload/xolox/misc/escape.vim @@ -0,0 +1,46 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: November 21, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +" Convert a string into a :substitute pattern that matches the string literally. + +function! xolox#misc#escape#pattern(string) + if type(a:string) == type('') + let string = escape(a:string, '^$.*\~[]') + return substitute(string, '\n', '\\n', 'g') + endif + return '' +endfunction + +" Convert a string into a :substitute replacement that inserts the string literally. + +function! xolox#misc#escape#substitute(string) + if type(a:string) == type('') + let string = escape(a:string, '\&~%') + return substitute(string, '\n', '\\r', 'g') + endif + return '' +endfunction + +" Convert a string into a quoted command line argument. I was going to add a +" long rant here about &shellslash, but really, it won't make any difference. +" Let's just suffice to say that I have yet to encounter a single person out +" there who uses this option for its intended purpose (running a UNIX-style +" shell on Windows). + +function! xolox#misc#escape#shell(string) + if xolox#misc#os#is_win() + try + let ssl_save = &shellslash + set noshellslash + return shellescape(a:string) + finally + let &shellslash = ssl_save + endtry + else + return shellescape(a:string) + endif +endfunction + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/list.vim b/autoload/xolox/misc/list.vim new file mode 100644 index 0000000..ee243d4 --- /dev/null +++ b/autoload/xolox/misc/list.vim @@ -0,0 +1,49 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: August 31, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +" Remove duplicate values from {list} in-place (preserves order). + +function! xolox#misc#list#unique(list) + call reverse(a:list) + call filter(a:list, 'count(a:list, v:val) == 1') + return reverse(a:list) +endfunction + +" Binary insertion (more efficient than calling sort() after each insertion). + +function! xolox#misc#list#binsert(list, value, ...) + let idx = s:binsert_r(a:list, 0, len(a:list), a:value, exists('a:1') && a:1) + return insert(a:list, a:value, idx) +endfunction + +function! s:binsert_r(list, low, high, value, ignorecase) + let mid = a:low + (a:high - a:low) / 2 + if a:low == a:high + return a:low + elseif a:ignorecase ? a:value >? a:list[mid] : a:value > a:list[mid] + return s:binsert_r(a:list, mid + 1, a:high, a:value, a:ignorecase) + elseif a:ignorecase ? a:value <? a:list[mid] : a:value < a:list[mid] + return s:binsert_r(a:list, a:low, mid, a:value, a:ignorecase) + else + return mid + endif +endfunction + +if 0 + " Tests for xolox#misc#list#binsert(). + let s:list = ['a', 'B', 'e'] + function! s:test(value, expected) + call xolox#misc#list#binsert(s:list, a:value, 1) + if s:list != a:expected + call xolox#misc#msg#warn("list.vim: Test failed! Expected %s, got %s", + \ string(a:expected), string(s:list)) + endif + endfunction + call s:test('c', ['a', 'B', 'c', 'e']) + call s:test('D', ['a', 'B', 'c', 'D', 'e']) + call s:test('f', ['a', 'B', 'c', 'D', 'e', 'f']) +endif + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/msg.vim b/autoload/xolox/misc/msg.vim new file mode 100644 index 0000000..9ba2b7c --- /dev/null +++ b/autoload/xolox/misc/msg.vim @@ -0,0 +1,83 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: March 15, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +if !exists('g:xolox_message_buffer') + " For when I lose my :messages history :-\ + let g:xolox_message_buffer = 100 +endif + +if !exists('g:xolox_messages') + let g:xolox_messages = [] +endif + +" Show a formatted informational message to the user. + +function! xolox#misc#msg#info(...) + call s:show_message('title', a:000) +endfunction + +" Show a formatted warning message to the user. + +function! xolox#misc#msg#warn(...) + call s:show_message('warningmsg', a:000) +endfunction + +" Show a formatted debugging message to the user? + +function! xolox#misc#msg#debug(...) + if &vbs >= 1 + call s:show_message('question', a:000) + endif +endfunction + +" The implementation of info() and warn(). + +function! s:show_message(hlgroup, args) + let nargs = len(a:args) + if nargs == 1 + let message = a:args[0] + elseif nargs >= 2 + let message = call('printf', a:args) + endif + if exists('message') + try + " Temporarily disable Vim's |hit-enter| prompt and mode display. + if !exists('s:more_save') + let s:more_save = &more + let s:ruler_save = &ruler + let s:smd_save = &showmode + endif + set nomore noshowmode + if winnr('$') == 1 | set noruler | endif + augroup PluginXoloxHideMode + autocmd! CursorHold,CursorHoldI * call s:clear_message() + augroup END + execute 'echohl' a:hlgroup + " Redraw to avoid |hit-enter| prompt. + redraw | echomsg message + if g:xolox_message_buffer > 0 + call add(g:xolox_messages, message) + if len(g:xolox_messages) > g:xolox_message_buffer + call remove(g:xolox_messages, 0) + endif + endif + finally + " Always clear message highlighting, even when interrupted by Ctrl-C. + echohl none + endtry + endif +endfunction + +function! s:clear_message() + echo '' + let &more = s:more_save + let &showmode = s:smd_save + let &ruler = s:ruler_save + unlet s:more_save s:ruler_save s:smd_save + autocmd! PluginXoloxHideMode + augroup! PluginXoloxHideMode +endfunction + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/open.vim b/autoload/xolox/misc/open.vim new file mode 100644 index 0000000..1fb24e0 --- /dev/null +++ b/autoload/xolox/misc/open.vim @@ -0,0 +1,70 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: November 21, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +if !exists('s:version') + let s:version = '1.1' + let s:enoimpl = "open.vim %s: %s() hasn't been implemented for your platform! If you have suggestions, please contact peter@peterodding.com." + let s:handlers = ['gnome-open', 'kde-open', 'exo-open', 'xdg-open'] +endif + +function! xolox#misc#open#file(path, ...) + if xolox#misc#os#is_win() + try + call xolox#shell#open_with_windows_shell(a:path) + catch /^Vim\%((\a\+)\)\=:E117/ + let command = '!start CMD /C START "" %s' + silent execute printf(command, xolox#misc#escape#shell(a:path)) + endtry + return + elseif has('macunix') + let cmd = 'open ' . shellescape(a:path) . ' 2>&1' + call s:handle_error(cmd, system(cmd)) + return + else + for handler in s:handlers + a:000 + if executable(handler) + call xolox#misc#msg#debug("open.vim %s: Using '%s' to open '%s'.", s:version, handler, a:path) + let cmd = shellescape(handler) . ' ' . shellescape(a:path) . ' 2>&1' + call s:handle_error(cmd, system(cmd)) + return + endif + endfor + endif + throw printf(s:enoimpl, s:script, 'xolox#misc#open#file') +endfunction + +function! xolox#misc#open#url(url) + let url = a:url + if url !~ '^\w\+://' + if url !~ '@' + let url = 'http://' . url + elseif url !~ '^mailto:' + let url = 'mailto:' . url + endif + endif + if has('unix') && !has('gui_running') && $DISPLAY == '' + for browser in ['lynx', 'links', 'w3m'] + if executable(browser) + execute '!' . browser fnameescape(url) + call s:handle_error(browser . ' ' . url, '') + return + endif + endfor + endif + call xolox#misc#open#file(url, 'firefox', 'google-chrome') +endfunction + +function! s:handle_error(cmd, output) + if v:shell_error + let message = "open.vim %s: Failed to execute program! (command line: %s%s)" + let output = strtrans(xolox#misc#str#trim(a:output)) + if output != '' + let output = ", output: " . string(output) + endif + throw printf(message, s:version, a:cmd, output) + endif +endfunction + +" vim: et ts=2 sw=2 fdm=marker diff --git a/autoload/xolox/misc/option.vim b/autoload/xolox/misc/option.vim new file mode 100644 index 0000000..efaf1bc --- /dev/null +++ b/autoload/xolox/misc/option.vim @@ -0,0 +1,84 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: August 31, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +function! xolox#misc#option#get(name, ...) + if exists('b:' . a:name) + " Buffer local variable. + return eval('b:' . a:name) + elseif exists('g:' . a:name) + " Global variable. + return eval('g:' . a:name) + elseif exists('a:1') + " Default value. + return a:1 + endif +endfunction + +" Functions to parse multi-valued Vim options like &tags and &runtimepath. + +function! xolox#misc#option#split(value) + let values = split(a:value, '[^\\]\zs,') + return map(values, 's:unescape(v:val)') +endfunction + +function! s:unescape(s) + return substitute(a:s, '\\\([\\,]\)', '\1', 'g') +endfunction + +function! xolox#misc#option#join(values) + let values = copy(a:values) + call map(values, 's:escape(v:val)') + return join(values, ',') +endfunction + +function! s:escape(s) + return escape(a:s, ',\') +endfunction + +function! xolox#misc#option#split_tags(value) + let values = split(a:value, '[^\\]\zs,') + return map(values, 's:unescape_tags(v:val)') +endfunction + +function! s:unescape_tags(s) + return substitute(a:s, '\\\([\\, ]\)', '\1', 'g') +endfunction + +function! xolox#misc#option#join_tags(values) + let values = copy(a:values) + call map(values, 's:escape_tags(v:val)') + return join(values, ',') +endfunction + +function! s:escape_tags(s) + return escape(a:s, ', ') +endfunction + +function! xolox#misc#option#eval_tags(value, ...) + let pathnames = [] + let first_only = exists('a:1') && a:1 + for pattern in xolox#misc#option#split_tags(a:value) + " Make buffer relative pathnames absolute. + if pattern =~ '^\./' + let directory = xolox#misc#escape#substitute(expand('%:p:h')) + let pattern = substitute(pattern, '^.\ze/', directory, '') + endif + " Make working directory relative pathnames absolute. + if xolox#misc#path#is_relative(pattern) + let pattern = xolox#misc#path#merge(getcwd(), pattern) + endif + " Ignore the trailing `;' for recursive upwards searching because we + " always want the most specific pathname available. + let pattern = substitute(pattern, ';$', '', '') + " Expand the pattern. + call extend(pathnames, split(expand(pattern), "\n")) + if first_only && !empty(pathnames) + return pathnames[0] + endif + endfor + return firstonly ? '' : pathnames +endfunction + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/os.vim b/autoload/xolox/misc/os.vim new file mode 100644 index 0000000..451ca57 --- /dev/null +++ b/autoload/xolox/misc/os.vim @@ -0,0 +1,30 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: November 24, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +let g:xolox#misc#os#version = '0.1' + +" Check whether Vim is running on Microsoft Windows. + +function! xolox#misc#os#is_win() + return has('win16') || has('win32') || has('win64') +endfunction + +" Execute an external command (hiding the console on Windows when possible). + +function! xolox#misc#os#exec(cmdline, ...) + try + " Try using my shell.vim plug-in. + return call('xolox#shell#execute', [a:cmdline, 1] + a:000) + catch /^Vim\%((\a\+)\)\=:E117/ + " Fall back to system() when we get an "unknown function" error. + let output = call('system', [a:cmdline] + a:000) + if v:shell_error + throw printf("os.vim %s: Command %s failed: %s", g:xolox#misc#os#version, a:cmdline, xolox#misc#str#trim(output)) + endif + return split(output, "\n") + endtry +endfunction + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/path.vim b/autoload/xolox/misc/path.vim new file mode 100644 index 0000000..6f8fe44 --- /dev/null +++ b/autoload/xolox/misc/path.vim @@ -0,0 +1,201 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: April 18, 2013 +" URL: http://peterodding.com/code/vim/misc/ + +let s:windows_compatible = has('win32') || has('win64') +let s:mac_os_x_compatible = has('macunix') + +function! xolox#misc#path#which(...) + let extensions = s:windows_compatible ? split($PATHEXT, ';') : [''] + let matches = [] + let checked = {} + for directory in split($PATH, s:windows_compatible ? ';' : ':') + let directory = xolox#misc#path#absolute(directory) + if !has_key(checked, directory) + if isdirectory(directory) + for program in a:000 + for extension in extensions + let path = xolox#misc#path#merge(directory, program . extension) + if executable(path) + call add(matches, path) + endif + endfor + endfor + endif + let checked[directory] = 1 + endif + endfor + return matches +endfunction + +" Split a pathname into a list of path components. + +function! xolox#misc#path#split(path) + if type(a:path) == type('') + if s:windows_compatible + return split(a:path, '[\/]\+') + else + let absolute = (a:path =~ '^/') + let segments = split(a:path, '/\+') + return absolute ? insert(segments, '/') : segments + endif + endif + return [] +endfunction + +" Join a list of path components into a pathname. + +function! xolox#misc#path#join(parts) + if type(a:parts) == type([]) + if !s:windows_compatible && a:parts[0] == '/' + return join(a:parts, '/')[1 : -1] + else + return join(a:parts, '/') + endif + endif + return '' +endfunction + +" Canonicalize and resolve a pathname. + +function! xolox#misc#path#absolute(path) + if type(a:path) == type('') + let path = fnamemodify(a:path, ':p') + " resolve() doesn't work when there's a trailing path separator. + if path =~ '/$' + let stripped_slash = 1 + let path = substitute(path, '/$', '', '') + endif + let path = resolve(path) + " Restore the path separator after calling resolve(). + if exists('stripped_slash') && path !~ '/$' + let path .= '/' + endif + return path + endif + return '' +endfunction + +" Make an absolute pathname relative. + +function! xolox#misc#path#relative(path, base) + let path = xolox#misc#path#split(a:path) + let base = xolox#misc#path#split(a:base) + while path != [] && base != [] && path[0] == base[0] + call remove(path, 0) + call remove(base, 0) + endwhile + let distance = repeat(['..'], len(base)) + return xolox#misc#path#join(distance + path) +endfunction + +" Join a directory and filename into a single pathname. + +function! xolox#misc#path#merge(parent, child, ...) + " TODO Use isabs()! + if type(a:parent) == type('') && type(a:child) == type('') + if s:windows_compatible + let parent = substitute(a:parent, '[\\/]\+$', '', '') + let child = substitute(a:child, '^[\\/]\+', '', '') + return parent . '\' . child + else + let parent = substitute(a:parent, '/\+$', '', '') + let child = substitute(a:child, '^/\+', '', '') + return parent . '/' . child + endif + endif + return '' +endfunction + +" Find the common prefix of path components in a list of pathnames. + +function! xolox#misc#path#commonprefix(paths) + let common = xolox#misc#path#split(a:paths[0]) + for path in a:paths + let index = 0 + for segment in xolox#misc#path#split(path) + if len(common) <= index + break + elseif common[index] != segment + call remove(common, index, -1) + break + endif + let index += 1 + endfor + endfor + return xolox#misc#path#join(common) +endfunction + +" Encode a pathname so it can be used as a filename. + +function! xolox#misc#path#encode(path) + if s:windows_compatible + let mask = '[*|\\/:"<>?%]' + elseif s:mac_os_x_compatible + let mask = '[\\/%:]' + else + let mask = '[\\/%]' + endif + return substitute(a:path, mask, '\=printf("%%%x", char2nr(submatch(0)))', 'g') +endfunction + +" Decode a pathname previously encoded with xolox#misc#path#encode(). + +function! xolox#misc#path#decode(encoded_path) + return substitute(a:encoded_path, '%\(\x\x\?\)', '\=nr2char("0x" . submatch(1))', 'g') +endfunction + +" Check whether two pathnames point to the same file. + +if s:windows_compatible + function! xolox#misc#path#equals(a, b) + return a:a ==? a:b || xolox#misc#path#absolute(a:a) ==? xolox#misc#path#absolute(a:b) + endfunction +else + function! xolox#misc#path#equals(a, b) + return a:a ==# a:b || xolox#misc#path#absolute(a:a) ==# xolox#misc#path#absolute(a:b) + endfunction +endif + +" Check whether a path is relative. + +function! xolox#misc#path#is_relative(path) + if a:path =~ '^\w\+://' + return 0 + elseif s:windows_compatible + return a:path !~ '^\(\w:\|[\\/]\)' + else + return a:path !~ '^/' + endif +endfunction + +" Create a temporary directory and return the path. + +function! xolox#misc#path#tempdir() + if !exists('s:tempdir_counter') + let s:tempdir_counter = 1 + endif + if exists('*mkdir') + if s:windows_compatible + let template = $TMP . '\vim_tempdir_' + elseif filewritable('/tmp') == 2 + let template = '/tmp/vim_tempdir_' + endif + endif + if !exists('template') + throw "xolox#misc#path#tempdir() hasn't been implemented on your platform!" + endif + while 1 + let directory = template . s:tempdir_counter + try + call mkdir(directory, '', 0700) + return directory + catch /\<E739\>/ + " Keep looking for a non-existing directory. + endtry + let s:tempdir_counter += 1 + endwhile +endfunction + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/str.vim b/autoload/xolox/misc/str.vim new file mode 100644 index 0000000..74a05aa --- /dev/null +++ b/autoload/xolox/misc/str.vim @@ -0,0 +1,12 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: June 14, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +" Trim whitespace from start and end of string. + +function! xolox#misc#str#trim(s) + return substitute(a:s, '^\_s*\(.\{-}\)\_s*$', '\1', '') +endfunction + +" vim: ts=2 sw=2 et diff --git a/autoload/xolox/misc/timer.vim b/autoload/xolox/misc/timer.vim new file mode 100644 index 0000000..151972d --- /dev/null +++ b/autoload/xolox/misc/timer.vim @@ -0,0 +1,85 @@ +" Vim auto-load script +" Author: Peter Odding <peter@peterodding.com> +" Last Change: March 15, 2011 +" URL: http://peterodding.com/code/vim/misc/ + +if !exists('g:timer_enabled') + let g:timer_enabled = 0 +endif + +if !exists('g:timer_verbosity') + let g:timer_verbosity = 1 +endif + +let s:has_reltime = has('reltime') + +" Start a timer. + +function! xolox#misc#timer#start() + if g:timer_enabled || &verbose >= g:timer_verbosity + return s:has_reltime ? reltime() : [localtime()] + endif + return [] +endfunction + +" Stop a timer and print the elapsed time (only if the user is interested). + +function! xolox#misc#timer#stop(...) + if (g:timer_enabled || &verbose >= g:timer_verbosity) + call call('xolox#misc#msg#info', map(copy(a:000), 's:convert_value(v:val)')) + endif +endfunction + +function! s:convert_value(value) + if type(a:value) != type([]) + return a:value + elseif !empty(a:value) + if s:has_reltime + let ts = xolox#misc#str#trim(reltimestr(reltime(a:value))) + else + let ts = localtime() - a:value[0] + endif + return xolox#misc#timer#format_timespan(ts) + else + return '?' + endif +endfunction + +" Format number of seconds as human friendly description. + +let s:units = [['day', 60 * 60 * 24], ['hour', 60 * 60], ['minute', 60], ['second', 1]] + +function! xolox#misc#timer#format_timespan(ts) + + " Convert timespan to integer. + let seconds = a:ts + 0 + + " Fast common case with extra precision from reltime(). + if seconds < 5 + let extract = matchstr(a:ts, '^\d\+\(\.0*[1-9][1-9]\?\)\?') + if extract =~ '[123456789]' + return extract . ' second' . (extract != '1' ? 's' : '') + endif + endif + + " Generic but slow code. + let result = [] + for [name, size] in s:units + if seconds >= size + let counter = seconds / size + let seconds = seconds % size + let suffix = counter != 1 ? 's' : '' + call add(result, printf('%i %s%s', counter, name, suffix)) + endif + endfor + + " Format the resulting text? + if len(result) == 1 + return result[0] + else + return join(result[0:-2], ', ') . ' and ' . result[-1] + endif + +endfunction + +" vim: ts=2 sw=2 et |