/
bundler.vim
657 lines (570 loc) · 19.8 KB
/
bundler.vim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
" bundler.vim - Support for Ruby's Bundler
" Maintainer: Tim Pope <http://tpo.pe/>
" Version: 2.0
if exists('g:loaded_bundler') || &cp || v:version < 700
finish
endif
let g:loaded_bundler = 1
if !exists('g:dispatch_compilers')
let g:dispatch_compilers = {}
endif
let g:dispatch_compilers['bundle exec'] = ''
" Utility {{{1
function! s:function(name) abort
return function(substitute(a:name,'^s:',matchstr(expand('<sfile>'), '<SNR>\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 <silent><buffer> gf :Bopen <C-R><C-F><CR>
nnoremap <silent><buffer> <C-W>f :Bsplit <C-R><C-F><CR>
nnoremap <silent><buffer> <C-W><C-F> :Bsplit <C-R><C-F><CR>
nnoremap <silent><buffer> <C-W>gf :Btabedit <C-R><C-F><CR>
endfunction
augroup bundler_syntax
autocmd!
autocmd BufNewFile,BufRead */.bundle/config set filetype=yaml
autocmd BufNewFile,BufRead Gemfile if &filetype !=# 'ruby' | setf ruby | endif
autocmd Syntax ruby if expand('<afile>: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) abort
if !exists('b:bundler_root')
let dir = s:FindBundlerRoot(a:path)
if dir != ''
let b:bundler_root = dir
endif
endif
return exists('b:bundler_root')
endfunction
function! s:Setup(path) abort
if s:Detect(a:path)
silent doautocmd User Bundler
endif
endfunction
function! s:ProjectionistDetect() abort
if s:Detect(get(g:, 'projectionist_file', ''))
call projectionist#append(b:bundler_root, {
\ 'Gemfile': {'dispatch': ['bundle', '--gemfile={file}'], 'alternate': 'Gemfile.lock'},
\ 'Gemfile.lock': {'alternate': 'Gemfile'}})
for projections in bundler#project().projections_list()
call projectionist#append(b:bundler_root, projections)
endfor
endif
endfunction
augroup bundler
autocmd!
autocmd FileType * call s:Setup(expand('<afile>:p'))
autocmd BufNewFile,BufReadPost *
\ if empty(&filetype) |
\ call s:Setup(expand('<afile>:p')) |
\ endif
autocmd User ProjectionistDetect call s:ProjectionistDetect()
autocmd User ProjectionistActivate
\ if exists('b:bundler_root') && !exists(':Bopen') |
\ silent doautocmd User Bundler |
\ 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') ? ';' : ':')
endif
try
exe chdir s:fnameescape(self.path())
if len(gem_paths) == 0
let gem_paths = split(system(prefix.'ruby -rubygems -e "print Gem.path.join(%(;))"'), ';')
endif
let abi_version = system('ruby -rrbconfig -e "print RbConfig::CONFIG[\"ruby_version\"]"')
exe chdir s:fnameescape(cwd)
finally
exe chdir s:fnameescape(cwd)
endtry
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\\<BUNDLE_PATH: '\\=\\zs[^\n']*")
if !empty(bundle_path)
if body =~# '\C\<BUNDLE_DISABLE_SHARED_GEMS:'
let gem_paths = [self.path(bundle_path, 'ruby', abi_version)]
else
let gem_paths = [self.path(bundle_path)]
endif
endif
endif
endfor
let git_sudo_install_path = expand('~/.bundler/ruby/').abi_version
for source in self._locked.git
for [name, ver] in items(source.versions)
for path in gem_paths
let dir = path . '/bundler/gems/' . matchstr(source.remote, '.*/\zs.\{-\}\ze\%(\.git\)\=$') . '-' . source.revision[0:11]
if isdirectory(dir)
let files = split(glob(dir . '/*/' . name . '.gemspec'), "\n")
if empty(files)
let paths[name] = dir
else
let paths[name] = files[0][0 : -10-strlen(name)]
endif
break
endif
endfor
let dir = git_sudo_install_path . '/' . matchstr(source.remote, '.*/\zs.\{-\}\ze\%(\.git\)\=$') . '-' . source.revision[0:11]
if isdirectory(dir)
let files = split(glob(dir . '/*/' . name . '.gemspec'), "\n")
if empty(files)
let paths[name] = dir
else
let paths[name] = files[0][0 : -10-strlen(name)]
endif
endif
endfor
endfor
for source in self._locked.path
for [name, ver] in items(source.versions)
if source.remote =~# '^\~/'
let local = expand(source.remote)
elseif source.remote !~# '^/'
let local = simplify(self.path(source.remote))
else
let local = source.remote
endif
let files = split(glob(local . '/*/' . name . '.gemspec'), "\n")
if empty(files)
let paths[name] = local
else
let paths[name] = files[0][0 : -10-strlen(name)]
endif
endfor
endfor
for source in self._locked.gem
for [name, ver] in items(source.versions)
for path in gem_paths
let dir = path . '/gems/' . name . '-' . ver
if isdirectory(dir)
let paths[name] = dir
break
endif
endfor
if !has_key(paths, name)
for path in gem_paths
let dir = glob(path . '/gems/' . name . '-' . ver . '-*')
if isdirectory(dir)
let paths[name] = dir
break
endif
endfor
endif
endfor
endfor
if has_key(self, '_projections_list')
call remove(self, '_projections_list')
endif
let self._path_time = time
let self._paths = paths
let self._sorted = sort(values(paths))
let index = index(self._sorted, self.path())
if index > 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
function! s:project_projections_list() dict abort
call self.paths()
if !has_key(self, '_projections_list')
let self._projections_list = []
let list = self._projections_list
if !empty(get(g:, 'gem_projections', {}))
for name in keys(self.versions())
if has_key(g:gem_projections, name)
call add(list, g:gem_projections[name])
endif
endfor
endif
for path in self.sorted()
if filereadable(path . '/lib/projections.json')
call add(list, projectionist#json_parse(readfile(path . '/lib/projections.json')))
endif
endfor
endif
return self._projections_list
endfunction
call s:add_methods('project', ['locked', 'gems', 'paths', 'sorted', 'versions', 'has', 'projections_list'])
" }}}1
" Buffer {{{1
let s:buffer_prototype = {}
function! s:buffer(...) abort
let buffer = {'#': bufnr(a:0 ? a:1 : '%')}
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('<bang>',<q-args>)")
function! s:IsBundlerProject()
return &makeprg =~# '^bundle' && exists('b:bundler_root')
endfunction
function! s:QuickFixCmdPreMake()
if !s:IsBundlerProject()
return
endif
call s:push_chdir()
endfunction
function! s:QuickFixCmdPostMake()
if !s:IsBundlerProject()
return
endif
call s:pop_command()
call s:project().paths('refresh')
endfunction
augroup bundler_make
autocmd FileType gemfilelock call s:SetupMake()
autocmd FileType ruby
\ if expand('<afile>:t') ==? 'gemfile' |
\ call s:SetupMake() |
\ endif
autocmd QuickFixCmdPre make,lmake call s:QuickFixCmdPreMake()
autocmd QuickFixCmdPost make,lmake call s:QuickFixCmdPostMake()
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<bang>',<q-args>,1)")
call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bedit :execute s:Open('edit<bang>',<q-args>,0)")
call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bsplit :execute s:Open('split',<q-args>,<bang>1)")
call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bvsplit :execute s:Open('vsplit',<q-args>,<bang>1)")
call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Btabedit :execute s:Open('tabedit',<q-args>,<bang>1)")
call s:command("-bar -bang -nargs=? -complete=customlist,s:OpenComplete Bpedit :execute s:Open('pedit',<q-args>,<bang>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: