" bundler.vim - Support for Ruby's Bundler " Maintainer: Tim Pope " Version: 2.0 if exists('g:loaded_bundler') || &cp || v:version < 700 finish endif let g:loaded_bundler = 1 " Utility {{{1 function! s:function(name) abort return function(substitute(a:name,'^s:',matchstr(expand(''), '\d\+_'),'')) endfunction function! s:sub(str,pat,rep) abort return substitute(a:str,'\v\C'.a:pat,a:rep,'') endfunction function! s:gsub(str,pat,rep) abort return substitute(a:str,'\v\C'.a:pat,a:rep,'g') endfunction function! s:shellesc(arg) abort if a:arg =~ '^[A-Za-z0-9_/.-]\+$' return a:arg else return shellescape(a:arg) endif endfunction function! s:fnameescape(file) abort if exists('*fnameescape') return fnameescape(a:file) else return escape(a:file," \t\n*?[{`$\\%#'\"|!<") endif endfunction function! s:shellslash(path) if exists('+shellslash') && !&shellslash return s:gsub(a:path,'\\','/') else return a:path endif endfunction function! s:completion_filter(results,A) let results = sort(copy(a:results)) call filter(results,'v:val !~# "\\~$"') let filtered = filter(copy(results),'v:val[0:strlen(a:A)-1] ==# a:A') if !empty(filtered) | return filtered | endif let regex = s:gsub(a:A,'[^/:]','[&].*') let filtered = filter(copy(results),'v:val =~# "^".regex') if !empty(filtered) | return filtered | endif let filtered = filter(copy(results),'"/".v:val =~# "[/:]".regex') if !empty(filtered) | return filtered | endif let regex = s:gsub(a:A,'.','[&].*') let filtered = filter(copy(results),'"/".v:val =~# regex') return filtered endfunction function! s:throw(string) abort let v:errmsg = 'bundler: '.a:string throw v:errmsg endfunction function! s:warn(str) echohl WarningMsg echomsg a:str echohl None let v:warningmsg = a:str endfunction function! s:add_methods(namespace, method_names) abort for name in a:method_names let s:{a:namespace}_prototype[name] = s:function('s:'.a:namespace.'_'.name) endfor endfunction let s:commands = [] function! s:command(definition) abort let s:commands += [a:definition] endfunction function! s:define_commands() for command in s:commands exe 'command! -buffer '.command endfor endfunction augroup bundler_utility autocmd! autocmd User Bundler call s:define_commands() augroup END let s:abstract_prototype = {} " }}}1 " Syntax highlighting {{{1 function! s:syntaxfile() syntax keyword rubyGemfileMethod gemspec gem source path git group platforms env ruby hi def link rubyGemfileMethod Function endfunction function! s:syntaxlock() setlocal iskeyword+=-,. syn match gemfilelockHeading '^[[:upper:]]\+$' syn match gemfilelockKey '^\s\+\zs\S\+:'he=e-1 skipwhite nextgroup=gemfilelockRevision syn match gemfilelockKey 'remote:'he=e-1 skipwhite nextgroup=gemfilelockRemote syn match gemfilelockRemote '\S\+' contained syn match gemfilelockRevision '[[:alnum:]._-]\+$' contained syn match gemfilelockGem '^\s\+\zs[[:alnum:]._-]\+\%([ !]\|$\)\@=' contains=gemfilelockFound,gemfilelockMissing skipwhite nextgroup=gemfilelockVersions,gemfilelockBang syn match gemfilelockVersions '([^()]*)' contained contains=gemfilelockVersion syn match gemfilelockVersion '[^,()]*' contained syn match gemfilelockBang '!' contained if !empty(bundler#project()) exe 'syn match gemfilelockFound "\<\%(bundler\|' . join(keys(s:project().paths()), '\|') . '\)\>" contained' exe 'syn match gemfilelockMissing "\<\%(' . join(keys(filter(s:project().versions(), '!has_key(s:project().paths(), v:key)')), '\|') . '\)\>" contained' else exe 'syn match gemfilelockFound "\<\%(\S*\)\>" contained' endif syn match gemfilelockHeading '^PLATFORMS$' nextgroup=gemfilelockPlatform skipnl skipwhite syn match gemfilelockPlatform '^ \zs[[:alnum:]._-]\+$' contained nextgroup=gemfilelockPlatform skipnl skipwhite hi def link gemfilelockHeading PreProc hi def link gemfilelockPlatform Typedef hi def link gemfilelockKey Identifier hi def link gemfilelockRemote String hi def link gemfilelockRevision Number hi def link gemfilelockFound Statement hi def link gemfilelockMissing Error hi def link gemfilelockVersion Type hi def link gemfilelockBang Special endfunction function! s:setuplock() nnoremap gf :Bopen nnoremap f :Bsplit nnoremap :Bsplit nnoremap gf :Btabedit endfunction augroup bundler_syntax autocmd! autocmd BufNewFile,BufRead */.bundle/config set filetype=yaml autocmd BufNewFile,BufRead Gemfile set filetype=ruby autocmd Syntax ruby if expand(':t') ==? 'gemfile' | call s:syntaxfile() | endif autocmd BufNewFile,BufRead [Gg]emfile.lock setf gemfilelock autocmd FileType gemfilelock set suffixesadd=.rb autocmd Syntax gemfilelock call s:syntaxlock() autocmd FileType gemfilelock call s:setuplock() autocmd User Rails/Gemfile.lock call s:setuplock() augroup END " }}}1 " Initialization {{{1 function! s:FindBundlerRoot(path) abort let path = s:shellslash(a:path) let fn = fnamemodify(path,':s?[\/]$??') let ofn = "" let nfn = fn while fn != ofn if filereadable(fn.'/Gemfile') return s:sub(simplify(fnamemodify(fn,':p')),'[\\/]$','') endif let ofn = fn let fn = fnamemodify(ofn,':h') endwhile return '' endfunction function! s:Detect(path) if !exists('b:bundler_root') let dir = s:FindBundlerRoot(a:path) if dir != '' let b:bundler_root = dir endif endif if exists('b:bundler_root') silent doautocmd User Bundler endif endfunction augroup bundler autocmd! autocmd FileType * call s:Detect(expand(':p')) autocmd BufNewFile,BufReadPost * \ if empty(&filetype) | \ call s:Detect(expand(':p')) | \ endif autocmd VimEnter * if expand('')==''|call s:Detect(getcwd())|endif augroup END " }}}1 " Project {{{1 let s:project_prototype = {} let s:projects = {} function! bundler#project(...) abort let dir = a:0 ? a:1 : (exists('b:bundler_root') && b:bundler_root !=# '' ? b:bundler_root : s:FindBundlerRoot(expand('%:p'))) if dir !=# '' if has_key(s:projects,dir) let project = get(s:projects,dir) else let project = {'root': dir} let s:projects[dir] = project endif return extend(extend(project,s:project_prototype,'keep'),s:abstract_prototype,'keep') endif return {} endfunction function! s:project(...) abort let project = a:0 ? bundler#project(a:1) : bundler#project() if empty(project) call s:throw('not a Bundler project: '.(a:0 ? a:1 : expand('%'))) else return project endif endfunction function! s:project_path(...) dict abort return join([self.root]+a:000,'/') endfunction call s:add_methods('project',['path']) function! s:project_locked() dict abort let lock_file = self.path('Gemfile.lock') let time = getftime(lock_file) if time != -1 && time != get(self,'_lock_time',-1) let self._locked = {'git': [], 'gem': [], 'path': []} let self._versions = {} for line in readfile(lock_file) if line =~# '^\S' let properties = {'versions': {}} if has_key(self._locked, tolower(line)) call extend(self._locked[tolower(line)], [properties]) endif elseif line =~# '^ \w\+: ' let properties[matchstr(line, '\w\+')] = matchstr(line, ': \zs.*') elseif line =~# '^ [a-zA-Z0-9._-]\+\s\+(\d\+' let name = split(line, ' ')[0] let ver = substitute(line, '.*(\|).*', '', 'g') let properties.versions[name] = ver let self._versions[name] = ver endif endfor let self._lock_time = time endif return get(self, '_locked', {}) endfunction function! s:project_paths(...) dict abort call self.locked() let time = get(self, '_lock_time', -1) if a:0 && a:1 ==# 'refresh' || time != -1 && time != get(self, '_path_time', -1) let paths = {} let chdir = exists("*haslocaldir") && haslocaldir() ? "lchdir" : "chdir" let cwd = getcwd() " Explicitly setting $PATH means /etc/zshenv on OS X can't touch it. if executable('env') let prefix = 'env PATH='.s:shellesc($PATH).' ' else let prefix = '' endif let gem_paths = [] if exists('$GEM_PATH') let gem_paths = split($GEM_PATH, has('win32') ? ';' : ':') else try exe chdir s:fnameescape(self.path()) let gem_paths = split(system(prefix.'ruby -rubygems -e "print Gem.path.join(%(;))"'), ';') finally exe chdir s:fnameescape(cwd) endtry endif let abi_version = matchstr(get(gem_paths, 0, '1.9.1'), '[0-9.]\+$') for config in [expand('~/.bundle/config'), self.path('.bundle/config')] if filereadable(config) let body = join(readfile(config), "\n") let bundle_path = matchstr(body, "\\C\\ 0 call insert(self._sorted, remove(self._sorted,index)) endif call self.alter_buffer_paths() return paths endif return get(self,'_paths',{}) endfunction function! s:project_sorted() dict abort call self.paths() return get(self, '_sorted', []) endfunction function! s:project_gems() dict abort return self.paths() endfunction function! s:project_versions() dict abort call self.locked() return get(self, '_versions', {}) endfunction function! s:project_has(gem) dict abort call self.locked() return has_key(self.versions(), a:gem) endfunction call s:add_methods('project', ['locked', 'gems', 'paths', 'sorted', 'versions', 'has']) " }}}1 " Buffer {{{1 let s:buffer_prototype = {} function! s:buffer(...) abort let buffer = {'#': bufnr(a:0 ? a:1 : '%')} let g:buffer = buffer call extend(extend(buffer,s:buffer_prototype,'keep'),s:abstract_prototype,'keep') if buffer.getvar('bundler_root') !=# '' return buffer endif call s:throw('not a Bundler project: '.(a:0 ? a:1 : expand('%'))) endfunction function! bundler#buffer(...) abort return s:buffer(a:0 ? a:1 : '%') endfunction function! s:buffer_getvar(var) dict abort return getbufvar(self['#'],a:var) endfunction function! s:buffer_setvar(var,value) dict abort return setbufvar(self['#'],a:var,a:value) endfunction function! s:buffer_project() dict abort return s:project(self.getvar('bundler_root')) endfunction call s:add_methods('buffer',['getvar','setvar','project']) " }}}1 " Bundle {{{1 function! s:push_chdir() if !exists("s:command_stack") | let s:command_stack = [] | endif let chdir = exists("*haslocaldir") && haslocaldir() ? "lchdir " : "chdir " call add(s:command_stack,chdir.s:fnameescape(getcwd())) exe chdir.'`=s:project().path()`' endfunction function! s:pop_command() if exists("s:command_stack") && len(s:command_stack) > 0 exe remove(s:command_stack,-1) endif endfunction function! s:Bundle(bang,arg) let old_makeprg = &l:makeprg let old_errorformat = &l:errorformat let old_compiler = get(b:, 'current_compiler', '') try compiler bundler execute 'make! '.a:arg if a:bang ==# '' return 'if !empty(getqflist()) | cfirst | endif' else return '' endif finally let &l:errorformat = old_errorformat let &l:makeprg = old_makeprg let b:current_compiler = old_compiler if empty(b:current_compiler) unlet b:current_compiler endif endtry endfunction function! s:BundleComplete(A,L,P) if a:L =~# '^\S\+\s\+\%(show\|update\) ' return s:completion_filter(keys(s:project().paths()),a:A) endif return s:completion_filter(['install','update','exec','package','config','check','list','show','outdated','console','viz','benchmark'],a:A) endfunction function! s:SetupMake() abort compiler bundler endfunction call s:command("-bar -bang -nargs=? -complete=customlist,s:BundleComplete Bundle :execute s:Bundle('',)") augroup bundler_make autocmd FileType gemfilelock call s:SetupMake() autocmd FileType ruby \ if expand(':t') ==? 'gemfile' | \ call s:SetupMake() | \ endif autocmd QuickFixCmdPre make,lmake \ if &makeprg =~# '^bundle' && exists('b:bundler_root') | \ call s:push_chdir() | \ endif autocmd QuickFixCmdPost make,lmake \ if &makeprg =~# '^bundle' && exists('b:bundler_root') | \ call s:pop_command() | \ call s:project().paths("refresh") | \ endif augroup END " }}}1 " Bopen {{{1 function! s:Open(cmd,gem,lcd) if a:gem ==# '' && a:lcd return a:cmd.' `=bundler#buffer().project().path("Gemfile")`' elseif a:gem ==# '' return a:cmd.' `=bundler#buffer().project().path("Gemfile.lock")`' else if !has_key(s:project().paths(), a:gem) call s:project().paths('refresh') endif if !has_key(s:project().paths(), a:gem) if has_key(s:project().versions(), a:gem) let v:errmsg = "Gem \"".a:gem."\" is in bundle but not installed" else let v:errmsg = "Gem \"".a:gem."\" is not in bundle" endif return 'echoerr v:errmsg' endif let path = fnameescape(bundler#buffer().project().paths()[a:gem]) let exec = a:cmd.' '.path if a:cmd =~# '^pedit' && a:lcd let exec .= '|wincmd P|lcd '.path.'|wincmd p' elseif a:lcd let exec .= '|lcd '.path endif return exec endif endfunction function! s:OpenComplete(A,L,P) return s:completion_filter(keys(s:project().paths()),a:A) endfunction call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bopen :execute s:Open('edit',,1)") call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bedit :execute s:Open('edit',,0)") call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bsplit :execute s:Open('split',,1)") call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bvsplit :execute s:Open('vsplit',,1)") call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Btabedit :execute s:Open('tabedit',,1)") call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bpedit :execute s:Open('pedit',,1)") " }}}1 " Paths {{{1 function! s:build_path_option(paths,suffix) abort return join(map(copy(a:paths),'",".escape(s:shellslash(v:val."/".a:suffix),", ")'),'') endfunction function! s:buffer_alter_paths() dict abort if self.getvar('&suffixesadd') =~# '\.rb\>' let new = self.project().sorted() let old = type(self.getvar('bundler_paths')) == type([]) ? self.getvar('bundler_paths') : [] for [option, suffix] in [['path', 'lib'], ['tags', 'tags']] let value = self.getvar('&'.option) if !empty(old) let drop = s:build_path_option(old,suffix) let index = stridx(value,drop) if index > 0 let value = value[0:index-1] . value[index+strlen(drop):-1] endif endif call self.setvar('&'.option,value.s:build_path_option(new,suffix)) endfor call self.setvar('bundler_paths',new) endif endfunction call s:add_methods('buffer',['alter_paths']) function! s:project_alter_buffer_paths() dict abort for bufnr in range(1,bufnr('$')) if getbufvar(bufnr,'bundler_root') ==# self.path() let vim_parsing_quirk = s:buffer(bufnr).alter_paths() endif if getbufvar(bufnr, '&syntax') ==# 'gemfilelock' call setbufvar(bufnr, '&syntax', 'gemfilelock') endif endfor endfunction call s:add_methods('project',['alter_buffer_paths']) augroup bundler_path autocmd! autocmd User Bundler call s:buffer().alter_paths() augroup END " }}}1 " vim:set sw=2 sts=2: