]> Tony Duckles's Git Repositories (git.nynim.org) - dotfiles.git/blob - .vim/autoload/plug.vim
Merge remote-tracking branch 'origin/master' into epic
[dotfiles.git] / .vim / autoload / plug.vim
1 " vim-plug: Vim plugin manager
2 " ============================
3 "
4 " Download plug.vim and put it in ~/.vim/autoload
5 "
6 " curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
7 " https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
8 "
9 " Edit your .vimrc
10 "
11 " call plug#begin('~/.vim/plugged')
12 "
13 " " Make sure you use single quotes
14 "
15 " " Shorthand notation; fetches https://github.com/junegunn/vim-easy-align
16 " Plug 'junegunn/vim-easy-align'
17 "
18 " " Any valid git URL is allowed
19 " Plug 'https://github.com/junegunn/vim-github-dashboard.git'
20 "
21 " " Multiple Plug commands can be written in a single line using | separators
22 " Plug 'SirVer/ultisnips' | Plug 'honza/vim-snippets'
23 "
24 " " On-demand loading
25 " Plug 'scrooloose/nerdtree', { 'on': 'NERDTreeToggle' }
26 " Plug 'tpope/vim-fireplace', { 'for': 'clojure' }
27 "
28 " " Using a non-master branch
29 " Plug 'rdnetto/YCM-Generator', { 'branch': 'stable' }
30 "
31 " " Using a tagged release; wildcard allowed (requires git 1.9.2 or above)
32 " Plug 'fatih/vim-go', { 'tag': '*' }
33 "
34 " " Plugin options
35 " Plug 'nsf/gocode', { 'tag': 'v.20150303', 'rtp': 'vim' }
36 "
37 " " Plugin outside ~/.vim/plugged with post-update hook
38 " Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
39 "
40 " " Unmanaged plugin (manually installed and updated)
41 " Plug '~/my-prototype-plugin'
42 "
43 " " Initialize plugin system
44 " call plug#end()
45 "
46 " Then reload .vimrc and :PlugInstall to install plugins.
47 "
48 " Plug options:
49 "
50 "| Option | Description |
51 "| ----------------------- | ------------------------------------------------ |
52 "| `branch`/`tag`/`commit` | Branch/tag/commit of the repository to use |
53 "| `rtp` | Subdirectory that contains Vim plugin |
54 "| `dir` | Custom directory for the plugin |
55 "| `as` | Use different name for the plugin |
56 "| `do` | Post-update hook (string or funcref) |
57 "| `on` | On-demand loading: Commands or `<Plug>`-mappings |
58 "| `for` | On-demand loading: File types |
59 "| `frozen` | Do not update unless explicitly specified |
60 "
61 " More information: https://github.com/junegunn/vim-plug
62 "
63 "
64 " Copyright (c) 2017 Junegunn Choi
65 "
66 " MIT License
67 "
68 " Permission is hereby granted, free of charge, to any person obtaining
69 " a copy of this software and associated documentation files (the
70 " "Software"), to deal in the Software without restriction, including
71 " without limitation the rights to use, copy, modify, merge, publish,
72 " distribute, sublicense, and/or sell copies of the Software, and to
73 " permit persons to whom the Software is furnished to do so, subject to
74 " the following conditions:
75 "
76 " The above copyright notice and this permission notice shall be
77 " included in all copies or substantial portions of the Software.
78 "
79 " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
80 " EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
81 " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
82 " NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
83 " LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
84 " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
85 " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
86
87 if exists('g:loaded_plug')
88 finish
89 endif
90 let g:loaded_plug = 1
91
92 let s:cpo_save = &cpo
93 set cpo&vim
94
95 let s:plug_src = 'https://github.com/junegunn/vim-plug.git'
96 let s:plug_tab = get(s:, 'plug_tab', -1)
97 let s:plug_buf = get(s:, 'plug_buf', -1)
98 let s:mac_gui = has('gui_macvim') && has('gui_running')
99 let s:is_win = has('win32') || has('win64')
100 let s:nvim = has('nvim-0.2') || (has('nvim') && exists('*jobwait') && !s:is_win)
101 let s:vim8 = has('patch-8.0.0039') && exists('*job_start')
102 let s:me = resolve(expand('<sfile>:p'))
103 let s:base_spec = { 'branch': 'master', 'frozen': 0 }
104 let s:TYPE = {
105 \ 'string': type(''),
106 \ 'list': type([]),
107 \ 'dict': type({}),
108 \ 'funcref': type(function('call'))
109 \ }
110 let s:loaded = get(s:, 'loaded', {})
111 let s:triggers = get(s:, 'triggers', {})
112
113 function! plug#begin(...)
114 if a:0 > 0
115 let s:plug_home_org = a:1
116 let home = s:path(fnamemodify(expand(a:1), ':p'))
117 elseif exists('g:plug_home')
118 let home = s:path(g:plug_home)
119 elseif !empty(&rtp)
120 let home = s:path(split(&rtp, ',')[0]) . '/plugged'
121 else
122 return s:err('Unable to determine plug home. Try calling plug#begin() with a path argument.')
123 endif
124 if fnamemodify(home, ':t') ==# 'plugin' && fnamemodify(home, ':h') ==# s:first_rtp
125 return s:err('Invalid plug home. '.home.' is a standard Vim runtime path and is not allowed.')
126 endif
127
128 let g:plug_home = home
129 let g:plugs = {}
130 let g:plugs_order = []
131 let s:triggers = {}
132
133 call s:define_commands()
134 return 1
135 endfunction
136
137 function! s:define_commands()
138 command! -nargs=+ -bar Plug call plug#(<args>)
139 if !executable('git')
140 return s:err('`git` executable not found. Most commands will not be available. To suppress this message, prepend `silent!` to `call plug#begin(...)`.')
141 endif
142 command! -nargs=* -bar -bang -complete=customlist,s:names PlugInstall call s:install(<bang>0, [<f-args>])
143 command! -nargs=* -bar -bang -complete=customlist,s:names PlugUpdate call s:update(<bang>0, [<f-args>])
144 command! -nargs=0 -bar -bang PlugClean call s:clean(<bang>0)
145 command! -nargs=0 -bar PlugUpgrade if s:upgrade() | execute 'source' s:esc(s:me) | endif
146 command! -nargs=0 -bar PlugStatus call s:status()
147 command! -nargs=0 -bar PlugDiff call s:diff()
148 command! -nargs=? -bar -bang -complete=file PlugSnapshot call s:snapshot(<bang>0, <f-args>)
149 endfunction
150
151 function! s:to_a(v)
152 return type(a:v) == s:TYPE.list ? a:v : [a:v]
153 endfunction
154
155 function! s:to_s(v)
156 return type(a:v) == s:TYPE.string ? a:v : join(a:v, "\n") . "\n"
157 endfunction
158
159 function! s:glob(from, pattern)
160 return s:lines(globpath(a:from, a:pattern))
161 endfunction
162
163 function! s:source(from, ...)
164 let found = 0
165 for pattern in a:000
166 for vim in s:glob(a:from, pattern)
167 execute 'source' s:esc(vim)
168 let found = 1
169 endfor
170 endfor
171 return found
172 endfunction
173
174 function! s:assoc(dict, key, val)
175 let a:dict[a:key] = add(get(a:dict, a:key, []), a:val)
176 endfunction
177
178 function! s:ask(message, ...)
179 call inputsave()
180 echohl WarningMsg
181 let answer = input(a:message.(a:0 ? ' (y/N/a) ' : ' (y/N) '))
182 echohl None
183 call inputrestore()
184 echo "\r"
185 return (a:0 && answer =~? '^a') ? 2 : (answer =~? '^y') ? 1 : 0
186 endfunction
187
188 function! s:ask_no_interrupt(...)
189 try
190 return call('s:ask', a:000)
191 catch
192 return 0
193 endtry
194 endfunction
195
196 function! plug#end()
197 if !exists('g:plugs')
198 return s:err('Call plug#begin() first')
199 endif
200
201 if exists('#PlugLOD')
202 augroup PlugLOD
203 autocmd!
204 augroup END
205 augroup! PlugLOD
206 endif
207 let lod = { 'ft': {}, 'map': {}, 'cmd': {} }
208
209 if exists('g:did_load_filetypes')
210 filetype off
211 endif
212 for name in g:plugs_order
213 if !has_key(g:plugs, name)
214 continue
215 endif
216 let plug = g:plugs[name]
217 if get(s:loaded, name, 0) || !has_key(plug, 'on') && !has_key(plug, 'for')
218 let s:loaded[name] = 1
219 continue
220 endif
221
222 if has_key(plug, 'on')
223 let s:triggers[name] = { 'map': [], 'cmd': [] }
224 for cmd in s:to_a(plug.on)
225 if cmd =~? '^<Plug>.\+'
226 if empty(mapcheck(cmd)) && empty(mapcheck(cmd, 'i'))
227 call s:assoc(lod.map, cmd, name)
228 endif
229 call add(s:triggers[name].map, cmd)
230 elseif cmd =~# '^[A-Z]'
231 let cmd = substitute(cmd, '!*$', '', '')
232 if exists(':'.cmd) != 2
233 call s:assoc(lod.cmd, cmd, name)
234 endif
235 call add(s:triggers[name].cmd, cmd)
236 else
237 call s:err('Invalid `on` option: '.cmd.
238 \ '. Should start with an uppercase letter or `<Plug>`.')
239 endif
240 endfor
241 endif
242
243 if has_key(plug, 'for')
244 let types = s:to_a(plug.for)
245 if !empty(types)
246 augroup filetypedetect
247 call s:source(s:rtp(plug), 'ftdetect/**/*.vim', 'after/ftdetect/**/*.vim')
248 augroup END
249 endif
250 for type in types
251 call s:assoc(lod.ft, type, name)
252 endfor
253 endif
254 endfor
255
256 for [cmd, names] in items(lod.cmd)
257 execute printf(
258 \ 'command! -nargs=* -range -bang -complete=file %s call s:lod_cmd(%s, "<bang>", <line1>, <line2>, <q-args>, %s)',
259 \ cmd, string(cmd), string(names))
260 endfor
261
262 for [map, names] in items(lod.map)
263 for [mode, map_prefix, key_prefix] in
264 \ [['i', '<C-O>', ''], ['n', '', ''], ['v', '', 'gv'], ['o', '', '']]
265 execute printf(
266 \ '%snoremap <silent> %s %s:<C-U>call <SID>lod_map(%s, %s, %s, "%s")<CR>',
267 \ mode, map, map_prefix, string(map), string(names), mode != 'i', key_prefix)
268 endfor
269 endfor
270
271 for [ft, names] in items(lod.ft)
272 augroup PlugLOD
273 execute printf('autocmd FileType %s call <SID>lod_ft(%s, %s)',
274 \ ft, string(ft), string(names))
275 augroup END
276 endfor
277
278 call s:reorg_rtp()
279 filetype plugin indent on
280 if has('vim_starting')
281 if has('syntax') && !exists('g:syntax_on')
282 syntax enable
283 end
284 else
285 call s:reload_plugins()
286 endif
287 endfunction
288
289 function! s:loaded_names()
290 return filter(copy(g:plugs_order), 'get(s:loaded, v:val, 0)')
291 endfunction
292
293 function! s:load_plugin(spec)
294 call s:source(s:rtp(a:spec), 'plugin/**/*.vim', 'after/plugin/**/*.vim')
295 endfunction
296
297 function! s:reload_plugins()
298 for name in s:loaded_names()
299 call s:load_plugin(g:plugs[name])
300 endfor
301 endfunction
302
303 function! s:trim(str)
304 return substitute(a:str, '[\/]\+$', '', '')
305 endfunction
306
307 function! s:version_requirement(val, min)
308 for idx in range(0, len(a:min) - 1)
309 let v = get(a:val, idx, 0)
310 if v < a:min[idx] | return 0
311 elseif v > a:min[idx] | return 1
312 endif
313 endfor
314 return 1
315 endfunction
316
317 function! s:git_version_requirement(...)
318 if !exists('s:git_version')
319 let s:git_version = map(split(split(s:system('git --version'))[2], '\.'), 'str2nr(v:val)')
320 endif
321 return s:version_requirement(s:git_version, a:000)
322 endfunction
323
324 function! s:progress_opt(base)
325 return a:base && !s:is_win &&
326 \ s:git_version_requirement(1, 7, 1) ? '--progress' : ''
327 endfunction
328
329 if s:is_win
330 function! s:rtp(spec)
331 return s:path(a:spec.dir . get(a:spec, 'rtp', ''))
332 endfunction
333
334 function! s:path(path)
335 return s:trim(substitute(a:path, '/', '\', 'g'))
336 endfunction
337
338 function! s:dirpath(path)
339 return s:path(a:path) . '\'
340 endfunction
341
342 function! s:is_local_plug(repo)
343 return a:repo =~? '^[a-z]:\|^[%~]'
344 endfunction
345 else
346 function! s:rtp(spec)
347 return s:dirpath(a:spec.dir . get(a:spec, 'rtp', ''))
348 endfunction
349
350 function! s:path(path)
351 return s:trim(a:path)
352 endfunction
353
354 function! s:dirpath(path)
355 return substitute(a:path, '[/\\]*$', '/', '')
356 endfunction
357
358 function! s:is_local_plug(repo)
359 return a:repo[0] =~ '[/$~]'
360 endfunction
361 endif
362
363 function! s:err(msg)
364 echohl ErrorMsg
365 echom '[vim-plug] '.a:msg
366 echohl None
367 endfunction
368
369 function! s:warn(cmd, msg)
370 echohl WarningMsg
371 execute a:cmd 'a:msg'
372 echohl None
373 endfunction
374
375 function! s:esc(path)
376 return escape(a:path, ' ')
377 endfunction
378
379 function! s:escrtp(path)
380 return escape(a:path, ' ,')
381 endfunction
382
383 function! s:remove_rtp()
384 for name in s:loaded_names()
385 let rtp = s:rtp(g:plugs[name])
386 execute 'set rtp-='.s:escrtp(rtp)
387 let after = globpath(rtp, 'after')
388 if isdirectory(after)
389 execute 'set rtp-='.s:escrtp(after)
390 endif
391 endfor
392 endfunction
393
394 function! s:reorg_rtp()
395 if !empty(s:first_rtp)
396 execute 'set rtp-='.s:first_rtp
397 execute 'set rtp-='.s:last_rtp
398 endif
399
400 " &rtp is modified from outside
401 if exists('s:prtp') && s:prtp !=# &rtp
402 call s:remove_rtp()
403 unlet! s:middle
404 endif
405
406 let s:middle = get(s:, 'middle', &rtp)
407 let rtps = map(s:loaded_names(), 's:rtp(g:plugs[v:val])')
408 let afters = filter(map(copy(rtps), 'globpath(v:val, "after")'), '!empty(v:val)')
409 let rtp = join(map(rtps, 'escape(v:val, ",")'), ',')
410 \ . ','.s:middle.','
411 \ . join(map(afters, 'escape(v:val, ",")'), ',')
412 let &rtp = substitute(substitute(rtp, ',,*', ',', 'g'), '^,\|,$', '', 'g')
413 let s:prtp = &rtp
414
415 if !empty(s:first_rtp)
416 execute 'set rtp^='.s:first_rtp
417 execute 'set rtp+='.s:last_rtp
418 endif
419 endfunction
420
421 function! s:doautocmd(...)
422 if exists('#'.join(a:000, '#'))
423 execute 'doautocmd' ((v:version > 703 || has('patch442')) ? '<nomodeline>' : '') join(a:000)
424 endif
425 endfunction
426
427 function! s:dobufread(names)
428 for name in a:names
429 let path = s:rtp(g:plugs[name]).'/**'
430 for dir in ['ftdetect', 'ftplugin']
431 if len(finddir(dir, path))
432 if exists('#BufRead')
433 doautocmd BufRead
434 endif
435 return
436 endif
437 endfor
438 endfor
439 endfunction
440
441 function! plug#load(...)
442 if a:0 == 0
443 return s:err('Argument missing: plugin name(s) required')
444 endif
445 if !exists('g:plugs')
446 return s:err('plug#begin was not called')
447 endif
448 let names = a:0 == 1 && type(a:1) == s:TYPE.list ? a:1 : a:000
449 let unknowns = filter(copy(names), '!has_key(g:plugs, v:val)')
450 if !empty(unknowns)
451 let s = len(unknowns) > 1 ? 's' : ''
452 return s:err(printf('Unknown plugin%s: %s', s, join(unknowns, ', ')))
453 end
454 let unloaded = filter(copy(names), '!get(s:loaded, v:val, 0)')
455 if !empty(unloaded)
456 for name in unloaded
457 call s:lod([name], ['ftdetect', 'after/ftdetect', 'plugin', 'after/plugin'])
458 endfor
459 call s:dobufread(unloaded)
460 return 1
461 end
462 return 0
463 endfunction
464
465 function! s:remove_triggers(name)
466 if !has_key(s:triggers, a:name)
467 return
468 endif
469 for cmd in s:triggers[a:name].cmd
470 execute 'silent! delc' cmd
471 endfor
472 for map in s:triggers[a:name].map
473 execute 'silent! unmap' map
474 execute 'silent! iunmap' map
475 endfor
476 call remove(s:triggers, a:name)
477 endfunction
478
479 function! s:lod(names, types, ...)
480 for name in a:names
481 call s:remove_triggers(name)
482 let s:loaded[name] = 1
483 endfor
484 call s:reorg_rtp()
485
486 for name in a:names
487 let rtp = s:rtp(g:plugs[name])
488 for dir in a:types
489 call s:source(rtp, dir.'/**/*.vim')
490 endfor
491 if a:0
492 if !s:source(rtp, a:1) && !empty(s:glob(rtp, a:2))
493 execute 'runtime' a:1
494 endif
495 call s:source(rtp, a:2)
496 endif
497 call s:doautocmd('User', name)
498 endfor
499 endfunction
500
501 function! s:lod_ft(pat, names)
502 let syn = 'syntax/'.a:pat.'.vim'
503 call s:lod(a:names, ['plugin', 'after/plugin'], syn, 'after/'.syn)
504 execute 'autocmd! PlugLOD FileType' a:pat
505 call s:doautocmd('filetypeplugin', 'FileType')
506 call s:doautocmd('filetypeindent', 'FileType')
507 endfunction
508
509 function! s:lod_cmd(cmd, bang, l1, l2, args, names)
510 call s:lod(a:names, ['ftdetect', 'after/ftdetect', 'plugin', 'after/plugin'])
511 call s:dobufread(a:names)
512 execute printf('%s%s%s %s', (a:l1 == a:l2 ? '' : (a:l1.','.a:l2)), a:cmd, a:bang, a:args)
513 endfunction
514
515 function! s:lod_map(map, names, with_prefix, prefix)
516 call s:lod(a:names, ['ftdetect', 'after/ftdetect', 'plugin', 'after/plugin'])
517 call s:dobufread(a:names)
518 let extra = ''
519 while 1
520 let c = getchar(0)
521 if c == 0
522 break
523 endif
524 let extra .= nr2char(c)
525 endwhile
526
527 if a:with_prefix
528 let prefix = v:count ? v:count : ''
529 let prefix .= '"'.v:register.a:prefix
530 if mode(1) == 'no'
531 if v:operator == 'c'
532 let prefix = "\<esc>" . prefix
533 endif
534 let prefix .= v:operator
535 endif
536 call feedkeys(prefix, 'n')
537 endif
538 call feedkeys(substitute(a:map, '^<Plug>', "\<Plug>", '') . extra)
539 endfunction
540
541 function! plug#(repo, ...)
542 if a:0 > 1
543 return s:err('Invalid number of arguments (1..2)')
544 endif
545
546 try
547 let repo = s:trim(a:repo)
548 let opts = a:0 == 1 ? s:parse_options(a:1) : s:base_spec
549 let name = get(opts, 'as', fnamemodify(repo, ':t:s?\.git$??'))
550 let spec = extend(s:infer_properties(name, repo), opts)
551 if !has_key(g:plugs, name)
552 call add(g:plugs_order, name)
553 endif
554 let g:plugs[name] = spec
555 let s:loaded[name] = get(s:loaded, name, 0)
556 catch
557 return s:err(v:exception)
558 endtry
559 endfunction
560
561 function! s:parse_options(arg)
562 let opts = copy(s:base_spec)
563 let type = type(a:arg)
564 if type == s:TYPE.string
565 let opts.tag = a:arg
566 elseif type == s:TYPE.dict
567 call extend(opts, a:arg)
568 if has_key(opts, 'dir')
569 let opts.dir = s:dirpath(expand(opts.dir))
570 endif
571 else
572 throw 'Invalid argument type (expected: string or dictionary)'
573 endif
574 return opts
575 endfunction
576
577 function! s:infer_properties(name, repo)
578 let repo = a:repo
579 if s:is_local_plug(repo)
580 return { 'dir': s:dirpath(expand(repo)) }
581 else
582 if repo =~ ':'
583 let uri = repo
584 else
585 if repo !~ '/'
586 throw printf('Invalid argument: %s (implicit `vim-scripts'' expansion is deprecated)', repo)
587 endif
588 let fmt = get(g:, 'plug_url_format', 'https://git::@github.com/%s.git')
589 let uri = printf(fmt, repo)
590 endif
591 return { 'dir': s:dirpath(g:plug_home.'/'.a:name), 'uri': uri }
592 endif
593 endfunction
594
595 function! s:install(force, names)
596 call s:update_impl(0, a:force, a:names)
597 endfunction
598
599 function! s:update(force, names)
600 call s:update_impl(1, a:force, a:names)
601 endfunction
602
603 function! plug#helptags()
604 if !exists('g:plugs')
605 return s:err('plug#begin was not called')
606 endif
607 for spec in values(g:plugs)
608 let docd = join([s:rtp(spec), 'doc'], '/')
609 if isdirectory(docd)
610 silent! execute 'helptags' s:esc(docd)
611 endif
612 endfor
613 return 1
614 endfunction
615
616 function! s:syntax()
617 syntax clear
618 syntax region plug1 start=/\%1l/ end=/\%2l/ contains=plugNumber
619 syntax region plug2 start=/\%2l/ end=/\%3l/ contains=plugBracket,plugX
620 syn match plugNumber /[0-9]\+[0-9.]*/ contained
621 syn match plugBracket /[[\]]/ contained
622 syn match plugX /x/ contained
623 syn match plugDash /^-/
624 syn match plugPlus /^+/
625 syn match plugStar /^*/
626 syn match plugMessage /\(^- \)\@<=.*/
627 syn match plugName /\(^- \)\@<=[^ ]*:/
628 syn match plugSha /\%(: \)\@<=[0-9a-f]\{4,}$/
629 syn match plugTag /(tag: [^)]\+)/
630 syn match plugInstall /\(^+ \)\@<=[^:]*/
631 syn match plugUpdate /\(^* \)\@<=[^:]*/
632 syn match plugCommit /^ \X*[0-9a-f]\{7,9} .*/ contains=plugRelDate,plugEdge,plugTag
633 syn match plugEdge /^ \X\+$/
634 syn match plugEdge /^ \X*/ contained nextgroup=plugSha
635 syn match plugSha /[0-9a-f]\{7,9}/ contained
636 syn match plugRelDate /([^)]*)$/ contained
637 syn match plugNotLoaded /(not loaded)$/
638 syn match plugError /^x.*/
639 syn region plugDeleted start=/^\~ .*/ end=/^\ze\S/
640 syn match plugH2 /^.*:\n-\+$/
641 syn keyword Function PlugInstall PlugStatus PlugUpdate PlugClean
642 hi def link plug1 Title
643 hi def link plug2 Repeat
644 hi def link plugH2 Type
645 hi def link plugX Exception
646 hi def link plugBracket Structure
647 hi def link plugNumber Number
648
649 hi def link plugDash Special
650 hi def link plugPlus Constant
651 hi def link plugStar Boolean
652
653 hi def link plugMessage Function
654 hi def link plugName Label
655 hi def link plugInstall Function
656 hi def link plugUpdate Type
657
658 hi def link plugError Error
659 hi def link plugDeleted Ignore
660 hi def link plugRelDate Comment
661 hi def link plugEdge PreProc
662 hi def link plugSha Identifier
663 hi def link plugTag Constant
664
665 hi def link plugNotLoaded Comment
666 endfunction
667
668 function! s:lpad(str, len)
669 return a:str . repeat(' ', a:len - len(a:str))
670 endfunction
671
672 function! s:lines(msg)
673 return split(a:msg, "[\r\n]")
674 endfunction
675
676 function! s:lastline(msg)
677 return get(s:lines(a:msg), -1, '')
678 endfunction
679
680 function! s:new_window()
681 execute get(g:, 'plug_window', 'vertical topleft new')
682 endfunction
683
684 function! s:plug_window_exists()
685 let buflist = tabpagebuflist(s:plug_tab)
686 return !empty(buflist) && index(buflist, s:plug_buf) >= 0
687 endfunction
688
689 function! s:switch_in()
690 if !s:plug_window_exists()
691 return 0
692 endif
693
694 if winbufnr(0) != s:plug_buf
695 let s:pos = [tabpagenr(), winnr(), winsaveview()]
696 execute 'normal!' s:plug_tab.'gt'
697 let winnr = bufwinnr(s:plug_buf)
698 execute winnr.'wincmd w'
699 call add(s:pos, winsaveview())
700 else
701 let s:pos = [winsaveview()]
702 endif
703
704 setlocal modifiable
705 return 1
706 endfunction
707
708 function! s:switch_out(...)
709 call winrestview(s:pos[-1])
710 setlocal nomodifiable
711 if a:0 > 0
712 execute a:1
713 endif
714
715 if len(s:pos) > 1
716 execute 'normal!' s:pos[0].'gt'
717 execute s:pos[1] 'wincmd w'
718 call winrestview(s:pos[2])
719 endif
720 endfunction
721
722 function! s:finish_bindings()
723 nnoremap <silent> <buffer> R :call <SID>retry()<cr>
724 nnoremap <silent> <buffer> D :PlugDiff<cr>
725 nnoremap <silent> <buffer> S :PlugStatus<cr>
726 nnoremap <silent> <buffer> U :call <SID>status_update()<cr>
727 xnoremap <silent> <buffer> U :call <SID>status_update()<cr>
728 nnoremap <silent> <buffer> ]] :silent! call <SID>section('')<cr>
729 nnoremap <silent> <buffer> [[ :silent! call <SID>section('b')<cr>
730 endfunction
731
732 function! s:prepare(...)
733 if empty(getcwd())
734 throw 'Invalid current working directory. Cannot proceed.'
735 endif
736
737 for evar in ['$GIT_DIR', '$GIT_WORK_TREE']
738 if exists(evar)
739 throw evar.' detected. Cannot proceed.'
740 endif
741 endfor
742
743 call s:job_abort()
744 if s:switch_in()
745 if b:plug_preview == 1
746 pc
747 endif
748 enew
749 else
750 call s:new_window()
751 endif
752
753 nnoremap <silent> <buffer> q :if b:plug_preview==1<bar>pc<bar>endif<bar>bd<cr>
754 if a:0 == 0
755 call s:finish_bindings()
756 endif
757 let b:plug_preview = -1
758 let s:plug_tab = tabpagenr()
759 let s:plug_buf = winbufnr(0)
760 call s:assign_name()
761
762 for k in ['<cr>', 'L', 'o', 'X', 'd', 'dd']
763 execute 'silent! unmap <buffer>' k
764 endfor
765 setlocal buftype=nofile bufhidden=wipe nobuflisted nolist noswapfile nowrap cursorline modifiable nospell
766 setf vim-plug
767 if exists('g:syntax_on')
768 call s:syntax()
769 endif
770 endfunction
771
772 function! s:assign_name()
773 " Assign buffer name
774 let prefix = '[Plugins]'
775 let name = prefix
776 let idx = 2
777 while bufexists(name)
778 let name = printf('%s (%s)', prefix, idx)
779 let idx = idx + 1
780 endwhile
781 silent! execute 'f' fnameescape(name)
782 endfunction
783
784 function! s:chsh(swap)
785 let prev = [&shell, &shellcmdflag, &shellredir]
786 if s:is_win
787 set shell=cmd.exe shellcmdflag=/c shellredir=>%s\ 2>&1
788 elseif a:swap
789 set shell=sh shellredir=>%s\ 2>&1
790 endif
791 return prev
792 endfunction
793
794 function! s:bang(cmd, ...)
795 try
796 let [sh, shellcmdflag, shrd] = s:chsh(a:0)
797 " FIXME: Escaping is incomplete. We could use shellescape with eval,
798 " but it won't work on Windows.
799 let cmd = a:0 ? s:with_cd(a:cmd, a:1) : a:cmd
800 if s:is_win
801 let batchfile = tempname().'.bat'
802 call writefile(["@echo off\r", cmd . "\r"], batchfile)
803 let cmd = batchfile
804 endif
805 let g:_plug_bang = (s:is_win && has('gui_running') ? 'silent ' : '').'!'.escape(cmd, '#!%')
806 execute "normal! :execute g:_plug_bang\<cr>\<cr>"
807 finally
808 unlet g:_plug_bang
809 let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd]
810 if s:is_win
811 call delete(batchfile)
812 endif
813 endtry
814 return v:shell_error ? 'Exit status: ' . v:shell_error : ''
815 endfunction
816
817 function! s:regress_bar()
818 let bar = substitute(getline(2)[1:-2], '.*\zs=', 'x', '')
819 call s:progress_bar(2, bar, len(bar))
820 endfunction
821
822 function! s:is_updated(dir)
823 return !empty(s:system_chomp('git log --pretty=format:"%h" "HEAD...HEAD@{1}"', a:dir))
824 endfunction
825
826 function! s:do(pull, force, todo)
827 for [name, spec] in items(a:todo)
828 if !isdirectory(spec.dir)
829 continue
830 endif
831 let installed = has_key(s:update.new, name)
832 let updated = installed ? 0 :
833 \ (a:pull && index(s:update.errors, name) < 0 && s:is_updated(spec.dir))
834 if a:force || installed || updated
835 execute 'cd' s:esc(spec.dir)
836 call append(3, '- Post-update hook for '. name .' ... ')
837 let error = ''
838 let type = type(spec.do)
839 if type == s:TYPE.string
840 if spec.do[0] == ':'
841 if !get(s:loaded, name, 0)
842 let s:loaded[name] = 1
843 call s:reorg_rtp()
844 endif
845 call s:load_plugin(spec)
846 try
847 execute spec.do[1:]
848 catch
849 let error = v:exception
850 endtry
851 if !s:plug_window_exists()
852 cd -
853 throw 'Warning: vim-plug was terminated by the post-update hook of '.name
854 endif
855 else
856 let error = s:bang(spec.do)
857 endif
858 elseif type == s:TYPE.funcref
859 try
860 let status = installed ? 'installed' : (updated ? 'updated' : 'unchanged')
861 call spec.do({ 'name': name, 'status': status, 'force': a:force })
862 catch
863 let error = v:exception
864 endtry
865 else
866 let error = 'Invalid hook type'
867 endif
868 call s:switch_in()
869 call setline(4, empty(error) ? (getline(4) . 'OK')
870 \ : ('x' . getline(4)[1:] . error))
871 if !empty(error)
872 call add(s:update.errors, name)
873 call s:regress_bar()
874 endif
875 cd -
876 endif
877 endfor
878 endfunction
879
880 function! s:hash_match(a, b)
881 return stridx(a:a, a:b) == 0 || stridx(a:b, a:a) == 0
882 endfunction
883
884 function! s:checkout(spec)
885 let sha = a:spec.commit
886 let output = s:system('git rev-parse HEAD', a:spec.dir)
887 if !v:shell_error && !s:hash_match(sha, s:lines(output)[0])
888 let output = s:system(
889 \ 'git fetch --depth 999999 && git checkout '.s:esc(sha).' --', a:spec.dir)
890 endif
891 return output
892 endfunction
893
894 function! s:finish(pull)
895 let new_frozen = len(filter(keys(s:update.new), 'g:plugs[v:val].frozen'))
896 if new_frozen
897 let s = new_frozen > 1 ? 's' : ''
898 call append(3, printf('- Installed %d frozen plugin%s', new_frozen, s))
899 endif
900 call append(3, '- Finishing ... ') | 4
901 redraw
902 call plug#helptags()
903 call plug#end()
904 call setline(4, getline(4) . 'Done!')
905 redraw
906 let msgs = []
907 if !empty(s:update.errors)
908 call add(msgs, "Press 'R' to retry.")
909 endif
910 if a:pull && len(s:update.new) < len(filter(getline(5, '$'),
911 \ "v:val =~ '^- ' && v:val !~# 'Already up.to.date'"))
912 call add(msgs, "Press 'D' to see the updated changes.")
913 endif
914 echo join(msgs, ' ')
915 call s:finish_bindings()
916 endfunction
917
918 function! s:retry()
919 if empty(s:update.errors)
920 return
921 endif
922 echo
923 call s:update_impl(s:update.pull, s:update.force,
924 \ extend(copy(s:update.errors), [s:update.threads]))
925 endfunction
926
927 function! s:is_managed(name)
928 return has_key(g:plugs[a:name], 'uri')
929 endfunction
930
931 function! s:names(...)
932 return sort(filter(keys(g:plugs), 'stridx(v:val, a:1) == 0 && s:is_managed(v:val)'))
933 endfunction
934
935 function! s:check_ruby()
936 silent! ruby require 'thread'; VIM::command("let g:plug_ruby = '#{RUBY_VERSION}'")
937 if !exists('g:plug_ruby')
938 redraw!
939 return s:warn('echom', 'Warning: Ruby interface is broken')
940 endif
941 let ruby_version = split(g:plug_ruby, '\.')
942 unlet g:plug_ruby
943 return s:version_requirement(ruby_version, [1, 8, 7])
944 endfunction
945
946 function! s:update_impl(pull, force, args) abort
947 let sync = index(a:args, '--sync') >= 0 || has('vim_starting')
948 let args = filter(copy(a:args), 'v:val != "--sync"')
949 let threads = (len(args) > 0 && args[-1] =~ '^[1-9][0-9]*$') ?
950 \ remove(args, -1) : get(g:, 'plug_threads', 16)
951
952 let managed = filter(copy(g:plugs), 's:is_managed(v:key)')
953 let todo = empty(args) ? filter(managed, '!v:val.frozen || !isdirectory(v:val.dir)') :
954 \ filter(managed, 'index(args, v:key) >= 0')
955
956 if empty(todo)
957 return s:warn('echo', 'No plugin to '. (a:pull ? 'update' : 'install'))
958 endif
959
960 if !s:is_win && s:git_version_requirement(2, 3)
961 let s:git_terminal_prompt = exists('$GIT_TERMINAL_PROMPT') ? $GIT_TERMINAL_PROMPT : ''
962 let $GIT_TERMINAL_PROMPT = 0
963 for plug in values(todo)
964 let plug.uri = substitute(plug.uri,
965 \ '^https://git::@github\.com', 'https://github.com', '')
966 endfor
967 endif
968
969 if !isdirectory(g:plug_home)
970 try
971 call mkdir(g:plug_home, 'p')
972 catch
973 return s:err(printf('Invalid plug directory: %s. '.
974 \ 'Try to call plug#begin with a valid directory', g:plug_home))
975 endtry
976 endif
977
978 if has('nvim') && !exists('*jobwait') && threads > 1
979 call s:warn('echom', '[vim-plug] Update Neovim for parallel installer')
980 endif
981
982 let use_job = s:nvim || s:vim8
983 let python = (has('python') || has('python3')) && !use_job
984 let ruby = has('ruby') && !use_job && (v:version >= 703 || v:version == 702 && has('patch374')) && !(s:is_win && has('gui_running')) && threads > 1 && s:check_ruby()
985
986 let s:update = {
987 \ 'start': reltime(),
988 \ 'all': todo,
989 \ 'todo': copy(todo),
990 \ 'errors': [],
991 \ 'pull': a:pull,
992 \ 'force': a:force,
993 \ 'new': {},
994 \ 'threads': (python || ruby || use_job) ? min([len(todo), threads]) : 1,
995 \ 'bar': '',
996 \ 'fin': 0
997 \ }
998
999 call s:prepare(1)
1000 call append(0, ['', ''])
1001 normal! 2G
1002 silent! redraw
1003
1004 let s:clone_opt = get(g:, 'plug_shallow', 1) ?
1005 \ '--depth 1' . (s:git_version_requirement(1, 7, 10) ? ' --no-single-branch' : '') : ''
1006
1007 if has('win32unix')
1008 let s:clone_opt .= ' -c core.eol=lf -c core.autocrlf=input'
1009 endif
1010
1011 " Python version requirement (>= 2.7)
1012 if python && !has('python3') && !ruby && !use_job && s:update.threads > 1
1013 redir => pyv
1014 silent python import platform; print platform.python_version()
1015 redir END
1016 let python = s:version_requirement(
1017 \ map(split(split(pyv)[0], '\.'), 'str2nr(v:val)'), [2, 6])
1018 endif
1019
1020 if (python || ruby) && s:update.threads > 1
1021 try
1022 let imd = &imd
1023 if s:mac_gui
1024 set noimd
1025 endif
1026 if ruby
1027 call s:update_ruby()
1028 else
1029 call s:update_python()
1030 endif
1031 catch
1032 let lines = getline(4, '$')
1033 let printed = {}
1034 silent! 4,$d _
1035 for line in lines
1036 let name = s:extract_name(line, '.', '')
1037 if empty(name) || !has_key(printed, name)
1038 call append('$', line)
1039 if !empty(name)
1040 let printed[name] = 1
1041 if line[0] == 'x' && index(s:update.errors, name) < 0
1042 call add(s:update.errors, name)
1043 end
1044 endif
1045 endif
1046 endfor
1047 finally
1048 let &imd = imd
1049 call s:update_finish()
1050 endtry
1051 else
1052 call s:update_vim()
1053 while use_job && sync
1054 sleep 100m
1055 if s:update.fin
1056 break
1057 endif
1058 endwhile
1059 endif
1060 endfunction
1061
1062 function! s:log4(name, msg)
1063 call setline(4, printf('- %s (%s)', a:msg, a:name))
1064 redraw
1065 endfunction
1066
1067 function! s:update_finish()
1068 if exists('s:git_terminal_prompt')
1069 let $GIT_TERMINAL_PROMPT = s:git_terminal_prompt
1070 endif
1071 if s:switch_in()
1072 call append(3, '- Updating ...') | 4
1073 for [name, spec] in items(filter(copy(s:update.all), 'index(s:update.errors, v:key) < 0 && (s:update.force || s:update.pull || has_key(s:update.new, v:key))'))
1074 let [pos, _] = s:logpos(name)
1075 if !pos
1076 continue
1077 endif
1078 if has_key(spec, 'commit')
1079 call s:log4(name, 'Checking out '.spec.commit)
1080 let out = s:checkout(spec)
1081 elseif has_key(spec, 'tag')
1082 let tag = spec.tag
1083 if tag =~ '\*'
1084 let tags = s:lines(s:system('git tag --list '.s:shellesc(tag).' --sort -version:refname 2>&1', spec.dir))
1085 if !v:shell_error && !empty(tags)
1086 let tag = tags[0]
1087 call s:log4(name, printf('Latest tag for %s -> %s', spec.tag, tag))
1088 call append(3, '')
1089 endif
1090 endif
1091 call s:log4(name, 'Checking out '.tag)
1092 let out = s:system('git checkout -q '.s:esc(tag).' -- 2>&1', spec.dir)
1093 else
1094 let branch = s:esc(get(spec, 'branch', 'master'))
1095 call s:log4(name, 'Merging origin/'.branch)
1096 let out = s:system('git checkout -q '.branch.' -- 2>&1'
1097 \. (has_key(s:update.new, name) ? '' : ('&& git merge --ff-only origin/'.branch.' 2>&1')), spec.dir)
1098 endif
1099 if !v:shell_error && filereadable(spec.dir.'/.gitmodules') &&
1100 \ (s:update.force || has_key(s:update.new, name) || s:is_updated(spec.dir))
1101 call s:log4(name, 'Updating submodules. This may take a while.')
1102 let out .= s:bang('git submodule update --init --recursive 2>&1', spec.dir)
1103 endif
1104 let msg = s:format_message(v:shell_error ? 'x': '-', name, out)
1105 if v:shell_error
1106 call add(s:update.errors, name)
1107 call s:regress_bar()
1108 silent execute pos 'd _'
1109 call append(4, msg) | 4
1110 elseif !empty(out)
1111 call setline(pos, msg[0])
1112 endif
1113 redraw
1114 endfor
1115 silent 4 d _
1116 try
1117 call s:do(s:update.pull, s:update.force, filter(copy(s:update.all), 'index(s:update.errors, v:key) < 0 && has_key(v:val, "do")'))
1118 catch
1119 call s:warn('echom', v:exception)
1120 call s:warn('echo', '')
1121 return
1122 endtry
1123 call s:finish(s:update.pull)
1124 call setline(1, 'Updated. Elapsed time: ' . split(reltimestr(reltime(s:update.start)))[0] . ' sec.')
1125 call s:switch_out('normal! gg')
1126 endif
1127 endfunction
1128
1129 function! s:job_abort()
1130 if (!s:nvim && !s:vim8) || !exists('s:jobs')
1131 return
1132 endif
1133
1134 for [name, j] in items(s:jobs)
1135 if s:nvim
1136 silent! call jobstop(j.jobid)
1137 elseif s:vim8
1138 silent! call job_stop(j.jobid)
1139 endif
1140 if j.new
1141 call s:system('rm -rf ' . s:shellesc(g:plugs[name].dir))
1142 endif
1143 endfor
1144 let s:jobs = {}
1145 endfunction
1146
1147 function! s:last_non_empty_line(lines)
1148 let len = len(a:lines)
1149 for idx in range(len)
1150 let line = a:lines[len-idx-1]
1151 if !empty(line)
1152 return line
1153 endif
1154 endfor
1155 return ''
1156 endfunction
1157
1158 function! s:job_out_cb(self, data) abort
1159 let self = a:self
1160 let data = remove(self.lines, -1) . a:data
1161 let lines = map(split(data, "\n", 1), 'split(v:val, "\r", 1)[-1]')
1162 call extend(self.lines, lines)
1163 " To reduce the number of buffer updates
1164 let self.tick = get(self, 'tick', -1) + 1
1165 if !self.running || self.tick % len(s:jobs) == 0
1166 let bullet = self.running ? (self.new ? '+' : '*') : (self.error ? 'x' : '-')
1167 let result = self.error ? join(self.lines, "\n") : s:last_non_empty_line(self.lines)
1168 call s:log(bullet, self.name, result)
1169 endif
1170 endfunction
1171
1172 function! s:job_exit_cb(self, data) abort
1173 let a:self.running = 0
1174 let a:self.error = a:data != 0
1175 call s:reap(a:self.name)
1176 call s:tick()
1177 endfunction
1178
1179 function! s:job_cb(fn, job, ch, data)
1180 if !s:plug_window_exists() " plug window closed
1181 return s:job_abort()
1182 endif
1183 call call(a:fn, [a:job, a:data])
1184 endfunction
1185
1186 function! s:nvim_cb(job_id, data, event) dict abort
1187 return a:event == 'stdout' ?
1188 \ s:job_cb('s:job_out_cb', self, 0, join(a:data, "\n")) :
1189 \ s:job_cb('s:job_exit_cb', self, 0, a:data)
1190 endfunction
1191
1192 function! s:spawn(name, cmd, opts)
1193 let job = { 'name': a:name, 'running': 1, 'error': 0, 'lines': [''],
1194 \ 'batchfile': (s:is_win && (s:nvim || s:vim8)) ? tempname().'.bat' : '',
1195 \ 'new': get(a:opts, 'new', 0) }
1196 let s:jobs[a:name] = job
1197 let cmd = has_key(a:opts, 'dir') ? s:with_cd(a:cmd, a:opts.dir) : a:cmd
1198 if !empty(job.batchfile)
1199 call writefile(["@echo off\r", cmd . "\r"], job.batchfile)
1200 let cmd = job.batchfile
1201 endif
1202 let argv = add(s:is_win ? ['cmd', '/c'] : ['sh', '-c'], cmd)
1203
1204 if s:nvim
1205 call extend(job, {
1206 \ 'on_stdout': function('s:nvim_cb'),
1207 \ 'on_exit': function('s:nvim_cb'),
1208 \ })
1209 let jid = jobstart(argv, job)
1210 if jid > 0
1211 let job.jobid = jid
1212 else
1213 let job.running = 0
1214 let job.error = 1
1215 let job.lines = [jid < 0 ? argv[0].' is not executable' :
1216 \ 'Invalid arguments (or job table is full)']
1217 endif
1218 elseif s:vim8
1219 let jid = job_start(s:is_win ? join(argv, ' ') : argv, {
1220 \ 'out_cb': function('s:job_cb', ['s:job_out_cb', job]),
1221 \ 'exit_cb': function('s:job_cb', ['s:job_exit_cb', job]),
1222 \ 'out_mode': 'raw'
1223 \})
1224 if job_status(jid) == 'run'
1225 let job.jobid = jid
1226 else
1227 let job.running = 0
1228 let job.error = 1
1229 let job.lines = ['Failed to start job']
1230 endif
1231 else
1232 let job.lines = s:lines(call('s:system', [cmd]))
1233 let job.error = v:shell_error != 0
1234 let job.running = 0
1235 endif
1236 endfunction
1237
1238 function! s:reap(name)
1239 let job = s:jobs[a:name]
1240 if job.error
1241 call add(s:update.errors, a:name)
1242 elseif get(job, 'new', 0)
1243 let s:update.new[a:name] = 1
1244 endif
1245 let s:update.bar .= job.error ? 'x' : '='
1246
1247 let bullet = job.error ? 'x' : '-'
1248 let result = job.error ? join(job.lines, "\n") : s:last_non_empty_line(job.lines)
1249 call s:log(bullet, a:name, empty(result) ? 'OK' : result)
1250 call s:bar()
1251
1252 if has_key(job, 'batchfile') && !empty(job.batchfile)
1253 call delete(job.batchfile)
1254 endif
1255 call remove(s:jobs, a:name)
1256 endfunction
1257
1258 function! s:bar()
1259 if s:switch_in()
1260 let total = len(s:update.all)
1261 call setline(1, (s:update.pull ? 'Updating' : 'Installing').
1262 \ ' plugins ('.len(s:update.bar).'/'.total.')')
1263 call s:progress_bar(2, s:update.bar, total)
1264 call s:switch_out()
1265 endif
1266 endfunction
1267
1268 function! s:logpos(name)
1269 for i in range(4, line('$'))
1270 if getline(i) =~# '^[-+x*] '.a:name.':'
1271 for j in range(i + 1, line('$'))
1272 if getline(j) !~ '^ '
1273 return [i, j - 1]
1274 endif
1275 endfor
1276 return [i, i]
1277 endif
1278 endfor
1279 return [0, 0]
1280 endfunction
1281
1282 function! s:log(bullet, name, lines)
1283 if s:switch_in()
1284 let [b, e] = s:logpos(a:name)
1285 if b > 0
1286 silent execute printf('%d,%d d _', b, e)
1287 if b > winheight('.')
1288 let b = 4
1289 endif
1290 else
1291 let b = 4
1292 endif
1293 " FIXME For some reason, nomodifiable is set after :d in vim8
1294 setlocal modifiable
1295 call append(b - 1, s:format_message(a:bullet, a:name, a:lines))
1296 call s:switch_out()
1297 endif
1298 endfunction
1299
1300 function! s:update_vim()
1301 let s:jobs = {}
1302
1303 call s:bar()
1304 call s:tick()
1305 endfunction
1306
1307 function! s:tick()
1308 let pull = s:update.pull
1309 let prog = s:progress_opt(s:nvim || s:vim8)
1310 while 1 " Without TCO, Vim stack is bound to explode
1311 if empty(s:update.todo)
1312 if empty(s:jobs) && !s:update.fin
1313 call s:update_finish()
1314 let s:update.fin = 1
1315 endif
1316 return
1317 endif
1318
1319 let name = keys(s:update.todo)[0]
1320 let spec = remove(s:update.todo, name)
1321 let new = !isdirectory(spec.dir)
1322
1323 call s:log(new ? '+' : '*', name, pull ? 'Updating ...' : 'Installing ...')
1324 redraw
1325
1326 let has_tag = has_key(spec, 'tag')
1327 if !new
1328 let [error, _] = s:git_validate(spec, 0)
1329 if empty(error)
1330 if pull
1331 let fetch_opt = (has_tag && !empty(globpath(spec.dir, '.git/shallow'))) ? '--depth 99999999' : ''
1332 call s:spawn(name, printf('git fetch %s %s 2>&1', fetch_opt, prog), { 'dir': spec.dir })
1333 else
1334 let s:jobs[name] = { 'running': 0, 'lines': ['Already installed'], 'error': 0 }
1335 endif
1336 else
1337 let s:jobs[name] = { 'running': 0, 'lines': s:lines(error), 'error': 1 }
1338 endif
1339 else
1340 call s:spawn(name,
1341 \ printf('git clone %s %s %s %s 2>&1',
1342 \ has_tag ? '' : s:clone_opt,
1343 \ prog,
1344 \ s:shellesc(spec.uri),
1345 \ s:shellesc(s:trim(spec.dir))), { 'new': 1 })
1346 endif
1347
1348 if !s:jobs[name].running
1349 call s:reap(name)
1350 endif
1351 if len(s:jobs) >= s:update.threads
1352 break
1353 endif
1354 endwhile
1355 endfunction
1356
1357 function! s:update_python()
1358 let py_exe = has('python') ? 'python' : 'python3'
1359 execute py_exe "<< EOF"
1360 import datetime
1361 import functools
1362 import os
1363 try:
1364 import queue
1365 except ImportError:
1366 import Queue as queue
1367 import random
1368 import re
1369 import shutil
1370 import signal
1371 import subprocess
1372 import tempfile
1373 import threading as thr
1374 import time
1375 import traceback
1376 import vim
1377
1378 G_NVIM = vim.eval("has('nvim')") == '1'
1379 G_PULL = vim.eval('s:update.pull') == '1'
1380 G_RETRIES = int(vim.eval('get(g:, "plug_retries", 2)')) + 1
1381 G_TIMEOUT = int(vim.eval('get(g:, "plug_timeout", 60)'))
1382 G_CLONE_OPT = vim.eval('s:clone_opt')
1383 G_PROGRESS = vim.eval('s:progress_opt(1)')
1384 G_LOG_PROB = 1.0 / int(vim.eval('s:update.threads'))
1385 G_STOP = thr.Event()
1386 G_IS_WIN = vim.eval('s:is_win') == '1'
1387
1388 class PlugError(Exception):
1389 def __init__(self, msg):
1390 self.msg = msg
1391 class CmdTimedOut(PlugError):
1392 pass
1393 class CmdFailed(PlugError):
1394 pass
1395 class InvalidURI(PlugError):
1396 pass
1397 class Action(object):
1398 INSTALL, UPDATE, ERROR, DONE = ['+', '*', 'x', '-']
1399
1400 class Buffer(object):
1401 def __init__(self, lock, num_plugs, is_pull):
1402 self.bar = ''
1403 self.event = 'Updating' if is_pull else 'Installing'
1404 self.lock = lock
1405 self.maxy = int(vim.eval('winheight(".")'))
1406 self.num_plugs = num_plugs
1407
1408 def __where(self, name):
1409 """ Find first line with name in current buffer. Return line num. """
1410 found, lnum = False, 0
1411 matcher = re.compile('^[-+x*] {0}:'.format(name))
1412 for line in vim.current.buffer:
1413 if matcher.search(line) is not None:
1414 found = True
1415 break
1416 lnum += 1
1417
1418 if not found:
1419 lnum = -1
1420 return lnum
1421
1422 def header(self):
1423 curbuf = vim.current.buffer
1424 curbuf[0] = self.event + ' plugins ({0}/{1})'.format(len(self.bar), self.num_plugs)
1425
1426 num_spaces = self.num_plugs - len(self.bar)
1427 curbuf[1] = '[{0}{1}]'.format(self.bar, num_spaces * ' ')
1428
1429 with self.lock:
1430 vim.command('normal! 2G')
1431 vim.command('redraw')
1432
1433 def write(self, action, name, lines):
1434 first, rest = lines[0], lines[1:]
1435 msg = ['{0} {1}{2}{3}'.format(action, name, ': ' if first else '', first)]
1436 msg.extend([' ' + line for line in rest])
1437
1438 try:
1439 if action == Action.ERROR:
1440 self.bar += 'x'
1441 vim.command("call add(s:update.errors, '{0}')".format(name))
1442 elif action == Action.DONE:
1443 self.bar += '='
1444
1445 curbuf = vim.current.buffer
1446 lnum = self.__where(name)
1447 if lnum != -1: # Found matching line num
1448 del curbuf[lnum]
1449 if lnum > self.maxy and action in set([Action.INSTALL, Action.UPDATE]):
1450 lnum = 3
1451 else:
1452 lnum = 3
1453 curbuf.append(msg, lnum)
1454
1455 self.header()
1456 except vim.error:
1457 pass
1458
1459 class Command(object):
1460 CD = 'cd /d' if G_IS_WIN else 'cd'
1461
1462 def __init__(self, cmd, cmd_dir=None, timeout=60, cb=None, clean=None):
1463 self.cmd = cmd
1464 if cmd_dir:
1465 self.cmd = '{0} {1} && {2}'.format(Command.CD, cmd_dir, self.cmd)
1466 self.timeout = timeout
1467 self.callback = cb if cb else (lambda msg: None)
1468 self.clean = clean if clean else (lambda: None)
1469 self.proc = None
1470
1471 @property
1472 def alive(self):
1473 """ Returns true only if command still running. """
1474 return self.proc and self.proc.poll() is None
1475
1476 def execute(self, ntries=3):
1477 """ Execute the command with ntries if CmdTimedOut.
1478 Returns the output of the command if no Exception.
1479 """
1480 attempt, finished, limit = 0, False, self.timeout
1481
1482 while not finished:
1483 try:
1484 attempt += 1
1485 result = self.try_command()
1486 finished = True
1487 return result
1488 except CmdTimedOut:
1489 if attempt != ntries:
1490 self.notify_retry()
1491 self.timeout += limit
1492 else:
1493 raise
1494
1495 def notify_retry(self):
1496 """ Retry required for command, notify user. """
1497 for count in range(3, 0, -1):
1498 if G_STOP.is_set():
1499 raise KeyboardInterrupt
1500 msg = 'Timeout. Will retry in {0} second{1} ...'.format(
1501 count, 's' if count != 1 else '')
1502 self.callback([msg])
1503 time.sleep(1)
1504 self.callback(['Retrying ...'])
1505
1506 def try_command(self):
1507 """ Execute a cmd & poll for callback. Returns list of output.
1508 Raises CmdFailed -> return code for Popen isn't 0
1509 Raises CmdTimedOut -> command exceeded timeout without new output
1510 """
1511 first_line = True
1512
1513 try:
1514 tfile = tempfile.NamedTemporaryFile(mode='w+b')
1515 preexec_fn = not G_IS_WIN and os.setsid or None
1516 self.proc = subprocess.Popen(self.cmd, stdout=tfile,
1517 stderr=subprocess.STDOUT,
1518 stdin=subprocess.PIPE, shell=True,
1519 preexec_fn=preexec_fn)
1520 thrd = thr.Thread(target=(lambda proc: proc.wait()), args=(self.proc,))
1521 thrd.start()
1522
1523 thread_not_started = True
1524 while thread_not_started:
1525 try:
1526 thrd.join(0.1)
1527 thread_not_started = False
1528 except RuntimeError:
1529 pass
1530
1531 while self.alive:
1532 if G_STOP.is_set():
1533 raise KeyboardInterrupt
1534
1535 if first_line or random.random() < G_LOG_PROB:
1536 first_line = False
1537 line = '' if G_IS_WIN else nonblock_read(tfile.name)
1538 if line:
1539 self.callback([line])
1540
1541 time_diff = time.time() - os.path.getmtime(tfile.name)
1542 if time_diff > self.timeout:
1543 raise CmdTimedOut(['Timeout!'])
1544
1545 thrd.join(0.5)
1546
1547 tfile.seek(0)
1548 result = [line.decode('utf-8', 'replace').rstrip() for line in tfile]
1549
1550 if self.proc.returncode != 0:
1551 raise CmdFailed([''] + result)
1552
1553 return result
1554 except:
1555 self.terminate()
1556 raise
1557
1558 def terminate(self):
1559 """ Terminate process and cleanup. """
1560 if self.alive:
1561 if G_IS_WIN:
1562 os.kill(self.proc.pid, signal.SIGINT)
1563 else:
1564 os.killpg(self.proc.pid, signal.SIGTERM)
1565 self.clean()
1566
1567 class Plugin(object):
1568 def __init__(self, name, args, buf_q, lock):
1569 self.name = name
1570 self.args = args
1571 self.buf_q = buf_q
1572 self.lock = lock
1573 self.tag = args.get('tag', 0)
1574
1575 def manage(self):
1576 try:
1577 if os.path.exists(self.args['dir']):
1578 self.update()
1579 else:
1580 self.install()
1581 with self.lock:
1582 thread_vim_command("let s:update.new['{0}'] = 1".format(self.name))
1583 except PlugError as exc:
1584 self.write(Action.ERROR, self.name, exc.msg)
1585 except KeyboardInterrupt:
1586 G_STOP.set()
1587 self.write(Action.ERROR, self.name, ['Interrupted!'])
1588 except:
1589 # Any exception except those above print stack trace
1590 msg = 'Trace:\n{0}'.format(traceback.format_exc().rstrip())
1591 self.write(Action.ERROR, self.name, msg.split('\n'))
1592 raise
1593
1594 def install(self):
1595 target = self.args['dir']
1596 if target[-1] == '\\':
1597 target = target[0:-1]
1598
1599 def clean(target):
1600 def _clean():
1601 try:
1602 shutil.rmtree(target)
1603 except OSError:
1604 pass
1605 return _clean
1606
1607 self.write(Action.INSTALL, self.name, ['Installing ...'])
1608 callback = functools.partial(self.write, Action.INSTALL, self.name)
1609 cmd = 'git clone {0} {1} {2} {3} 2>&1'.format(
1610 '' if self.tag else G_CLONE_OPT, G_PROGRESS, self.args['uri'],
1611 esc(target))
1612 com = Command(cmd, None, G_TIMEOUT, callback, clean(target))
1613 result = com.execute(G_RETRIES)
1614 self.write(Action.DONE, self.name, result[-1:])
1615
1616 def repo_uri(self):
1617 cmd = 'git rev-parse --abbrev-ref HEAD 2>&1 && git config -f .git/config remote.origin.url'
1618 command = Command(cmd, self.args['dir'], G_TIMEOUT,)
1619 result = command.execute(G_RETRIES)
1620 return result[-1]
1621
1622 def update(self):
1623 actual_uri = self.repo_uri()
1624 expect_uri = self.args['uri']
1625 regex = re.compile(r'^(?:\w+://)?(?:[^@/]*@)?([^:/]*(?::[0-9]*)?)[:/](.*?)(?:\.git)?/?$')
1626 ma = regex.match(actual_uri)
1627 mb = regex.match(expect_uri)
1628 if ma is None or mb is None or ma.groups() != mb.groups():
1629 msg = ['',
1630 'Invalid URI: {0}'.format(actual_uri),
1631 'Expected {0}'.format(expect_uri),
1632 'PlugClean required.']
1633 raise InvalidURI(msg)
1634
1635 if G_PULL:
1636 self.write(Action.UPDATE, self.name, ['Updating ...'])
1637 callback = functools.partial(self.write, Action.UPDATE, self.name)
1638 fetch_opt = '--depth 99999999' if self.tag and os.path.isfile(os.path.join(self.args['dir'], '.git/shallow')) else ''
1639 cmd = 'git fetch {0} {1} 2>&1'.format(fetch_opt, G_PROGRESS)
1640 com = Command(cmd, self.args['dir'], G_TIMEOUT, callback)
1641 result = com.execute(G_RETRIES)
1642 self.write(Action.DONE, self.name, result[-1:])
1643 else:
1644 self.write(Action.DONE, self.name, ['Already installed'])
1645
1646 def write(self, action, name, msg):
1647 self.buf_q.put((action, name, msg))
1648
1649 class PlugThread(thr.Thread):
1650 def __init__(self, tname, args):
1651 super(PlugThread, self).__init__()
1652 self.tname = tname
1653 self.args = args
1654
1655 def run(self):
1656 thr.current_thread().name = self.tname
1657 buf_q, work_q, lock = self.args
1658
1659 try:
1660 while not G_STOP.is_set():
1661 name, args = work_q.get_nowait()
1662 plug = Plugin(name, args, buf_q, lock)
1663 plug.manage()
1664 work_q.task_done()
1665 except queue.Empty:
1666 pass
1667
1668 class RefreshThread(thr.Thread):
1669 def __init__(self, lock):
1670 super(RefreshThread, self).__init__()
1671 self.lock = lock
1672 self.running = True
1673
1674 def run(self):
1675 while self.running:
1676 with self.lock:
1677 thread_vim_command('noautocmd normal! a')
1678 time.sleep(0.33)
1679
1680 def stop(self):
1681 self.running = False
1682
1683 if G_NVIM:
1684 def thread_vim_command(cmd):
1685 vim.session.threadsafe_call(lambda: vim.command(cmd))
1686 else:
1687 def thread_vim_command(cmd):
1688 vim.command(cmd)
1689
1690 def esc(name):
1691 return '"' + name.replace('"', '\"') + '"'
1692
1693 def nonblock_read(fname):
1694 """ Read a file with nonblock flag. Return the last line. """
1695 fread = os.open(fname, os.O_RDONLY | os.O_NONBLOCK)
1696 buf = os.read(fread, 100000).decode('utf-8', 'replace')
1697 os.close(fread)
1698
1699 line = buf.rstrip('\r\n')
1700 left = max(line.rfind('\r'), line.rfind('\n'))
1701 if left != -1:
1702 left += 1
1703 line = line[left:]
1704
1705 return line
1706
1707 def main():
1708 thr.current_thread().name = 'main'
1709 nthreads = int(vim.eval('s:update.threads'))
1710 plugs = vim.eval('s:update.todo')
1711 mac_gui = vim.eval('s:mac_gui') == '1'
1712
1713 lock = thr.Lock()
1714 buf = Buffer(lock, len(plugs), G_PULL)
1715 buf_q, work_q = queue.Queue(), queue.Queue()
1716 for work in plugs.items():
1717 work_q.put(work)
1718
1719 start_cnt = thr.active_count()
1720 for num in range(nthreads):
1721 tname = 'PlugT-{0:02}'.format(num)
1722 thread = PlugThread(tname, (buf_q, work_q, lock))
1723 thread.start()
1724 if mac_gui:
1725 rthread = RefreshThread(lock)
1726 rthread.start()
1727
1728 while not buf_q.empty() or thr.active_count() != start_cnt:
1729 try:
1730 action, name, msg = buf_q.get(True, 0.25)
1731 buf.write(action, name, ['OK'] if not msg else msg)
1732 buf_q.task_done()
1733 except queue.Empty:
1734 pass
1735 except KeyboardInterrupt:
1736 G_STOP.set()
1737
1738 if mac_gui:
1739 rthread.stop()
1740 rthread.join()
1741
1742 main()
1743 EOF
1744 endfunction
1745
1746 function! s:update_ruby()
1747 ruby << EOF
1748 module PlugStream
1749 SEP = ["\r", "\n", nil]
1750 def get_line
1751 buffer = ''
1752 loop do
1753 char = readchar rescue return
1754 if SEP.include? char.chr
1755 buffer << $/
1756 break
1757 else
1758 buffer << char
1759 end
1760 end
1761 buffer
1762 end
1763 end unless defined?(PlugStream)
1764
1765 def esc arg
1766 %["#{arg.gsub('"', '\"')}"]
1767 end
1768
1769 def killall pid
1770 pids = [pid]
1771 if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
1772 pids.each { |pid| Process.kill 'INT', pid.to_i rescue nil }
1773 else
1774 unless `which pgrep 2> /dev/null`.empty?
1775 children = pids
1776 until children.empty?
1777 children = children.map { |pid|
1778 `pgrep -P #{pid}`.lines.map { |l| l.chomp }
1779 }.flatten
1780 pids += children
1781 end
1782 end
1783 pids.each { |pid| Process.kill 'TERM', pid.to_i rescue nil }
1784 end
1785 end
1786
1787 def compare_git_uri a, b
1788 regex = %r{^(?:\w+://)?(?:[^@/]*@)?([^:/]*(?::[0-9]*)?)[:/](.*?)(?:\.git)?/?$}
1789 regex.match(a).to_a.drop(1) == regex.match(b).to_a.drop(1)
1790 end
1791
1792 require 'thread'
1793 require 'fileutils'
1794 require 'timeout'
1795 running = true
1796 iswin = VIM::evaluate('s:is_win').to_i == 1
1797 pull = VIM::evaluate('s:update.pull').to_i == 1
1798 base = VIM::evaluate('g:plug_home')
1799 all = VIM::evaluate('s:update.todo')
1800 limit = VIM::evaluate('get(g:, "plug_timeout", 60)')
1801 tries = VIM::evaluate('get(g:, "plug_retries", 2)') + 1
1802 nthr = VIM::evaluate('s:update.threads').to_i
1803 maxy = VIM::evaluate('winheight(".")').to_i
1804 vim7 = VIM::evaluate('v:version').to_i <= 703 && RUBY_PLATFORM =~ /darwin/
1805 cd = iswin ? 'cd /d' : 'cd'
1806 tot = VIM::evaluate('len(s:update.todo)') || 0
1807 bar = ''
1808 skip = 'Already installed'
1809 mtx = Mutex.new
1810 take1 = proc { mtx.synchronize { running && all.shift } }
1811 logh = proc {
1812 cnt = bar.length
1813 $curbuf[1] = "#{pull ? 'Updating' : 'Installing'} plugins (#{cnt}/#{tot})"
1814 $curbuf[2] = '[' + bar.ljust(tot) + ']'
1815 VIM::command('normal! 2G')
1816 VIM::command('redraw')
1817 }
1818 where = proc { |name| (1..($curbuf.length)).find { |l| $curbuf[l] =~ /^[-+x*] #{name}:/ } }
1819 log = proc { |name, result, type|
1820 mtx.synchronize do
1821 ing = ![true, false].include?(type)
1822 bar += type ? '=' : 'x' unless ing
1823 b = case type
1824 when :install then '+' when :update then '*'
1825 when true, nil then '-' else
1826 VIM::command("call add(s:update.errors, '#{name}')")
1827 'x'
1828 end
1829 result =
1830 if type || type.nil?
1831 ["#{b} #{name}: #{result.lines.to_a.last || 'OK'}"]
1832 elsif result =~ /^Interrupted|^Timeout/
1833 ["#{b} #{name}: #{result}"]
1834 else
1835 ["#{b} #{name}"] + result.lines.map { |l| " " << l }
1836 end
1837 if lnum = where.call(name)
1838 $curbuf.delete lnum
1839 lnum = 4 if ing && lnum > maxy
1840 end
1841 result.each_with_index do |line, offset|
1842 $curbuf.append((lnum || 4) - 1 + offset, line.gsub(/\e\[./, '').chomp)
1843 end
1844 logh.call
1845 end
1846 }
1847 bt = proc { |cmd, name, type, cleanup|
1848 tried = timeout = 0
1849 begin
1850 tried += 1
1851 timeout += limit
1852 fd = nil
1853 data = ''
1854 if iswin
1855 Timeout::timeout(timeout) do
1856 tmp = VIM::evaluate('tempname()')
1857 system("(#{cmd}) > #{tmp}")
1858 data = File.read(tmp).chomp
1859 File.unlink tmp rescue nil
1860 end
1861 else
1862 fd = IO.popen(cmd).extend(PlugStream)
1863 first_line = true
1864 log_prob = 1.0 / nthr
1865 while line = Timeout::timeout(timeout) { fd.get_line }
1866 data << line
1867 log.call name, line.chomp, type if name && (first_line || rand < log_prob)
1868 first_line = false
1869 end
1870 fd.close
1871 end
1872 [$? == 0, data.chomp]
1873 rescue Timeout::Error, Interrupt => e
1874 if fd && !fd.closed?
1875 killall fd.pid
1876 fd.close
1877 end
1878 cleanup.call if cleanup
1879 if e.is_a?(Timeout::Error) && tried < tries
1880 3.downto(1) do |countdown|
1881 s = countdown > 1 ? 's' : ''
1882 log.call name, "Timeout. Will retry in #{countdown} second#{s} ...", type
1883 sleep 1
1884 end
1885 log.call name, 'Retrying ...', type
1886 retry
1887 end
1888 [false, e.is_a?(Interrupt) ? "Interrupted!" : "Timeout!"]
1889 end
1890 }
1891 main = Thread.current
1892 threads = []
1893 watcher = Thread.new {
1894 if vim7
1895 while VIM::evaluate('getchar(1)')
1896 sleep 0.1
1897 end
1898 else
1899 require 'io/console' # >= Ruby 1.9
1900 nil until IO.console.getch == 3.chr
1901 end
1902 mtx.synchronize do
1903 running = false
1904 threads.each { |t| t.raise Interrupt } unless vim7
1905 end
1906 threads.each { |t| t.join rescue nil }
1907 main.kill
1908 }
1909 refresh = Thread.new {
1910 while true
1911 mtx.synchronize do
1912 break unless running
1913 VIM::command('noautocmd normal! a')
1914 end
1915 sleep 0.2
1916 end
1917 } if VIM::evaluate('s:mac_gui') == 1
1918
1919 clone_opt = VIM::evaluate('s:clone_opt')
1920 progress = VIM::evaluate('s:progress_opt(1)')
1921 nthr.times do
1922 mtx.synchronize do
1923 threads << Thread.new {
1924 while pair = take1.call
1925 name = pair.first
1926 dir, uri, tag = pair.last.values_at *%w[dir uri tag]
1927 exists = File.directory? dir
1928 ok, result =
1929 if exists
1930 chdir = "#{cd} #{iswin ? dir : esc(dir)}"
1931 ret, data = bt.call "#{chdir} && git rev-parse --abbrev-ref HEAD 2>&1 && git config -f .git/config remote.origin.url", nil, nil, nil
1932 current_uri = data.lines.to_a.last
1933 if !ret
1934 if data =~ /^Interrupted|^Timeout/
1935 [false, data]
1936 else
1937 [false, [data.chomp, "PlugClean required."].join($/)]
1938 end
1939 elsif !compare_git_uri(current_uri, uri)
1940 [false, ["Invalid URI: #{current_uri}",
1941 "Expected: #{uri}",
1942 "PlugClean required."].join($/)]
1943 else
1944 if pull
1945 log.call name, 'Updating ...', :update
1946 fetch_opt = (tag && File.exist?(File.join(dir, '.git/shallow'))) ? '--depth 99999999' : ''
1947 bt.call "#{chdir} && git fetch #{fetch_opt} #{progress} 2>&1", name, :update, nil
1948 else
1949 [true, skip]
1950 end
1951 end
1952 else
1953 d = esc dir.sub(%r{[\\/]+$}, '')
1954 log.call name, 'Installing ...', :install
1955 bt.call "git clone #{clone_opt unless tag} #{progress} #{uri} #{d} 2>&1", name, :install, proc {
1956 FileUtils.rm_rf dir
1957 }
1958 end
1959 mtx.synchronize { VIM::command("let s:update.new['#{name}'] = 1") } if !exists && ok
1960 log.call name, result, ok
1961 end
1962 } if running
1963 end
1964 end
1965 threads.each { |t| t.join rescue nil }
1966 logh.call
1967 refresh.kill if refresh
1968 watcher.kill
1969 EOF
1970 endfunction
1971
1972 function! s:shellesc_cmd(arg)
1973 let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
1974 let escaped = substitute(escaped, '%', '%%', 'g')
1975 let escaped = substitute(escaped, '"', '\\^&', 'g')
1976 let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
1977 return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
1978 endfunction
1979
1980 function! s:shellesc(arg)
1981 if &shell =~# 'cmd.exe$'
1982 return s:shellesc_cmd(a:arg)
1983 endif
1984 return shellescape(a:arg)
1985 endfunction
1986
1987 function! s:glob_dir(path)
1988 return map(filter(s:glob(a:path, '**'), 'isdirectory(v:val)'), 's:dirpath(v:val)')
1989 endfunction
1990
1991 function! s:progress_bar(line, bar, total)
1992 call setline(a:line, '[' . s:lpad(a:bar, a:total) . ']')
1993 endfunction
1994
1995 function! s:compare_git_uri(a, b)
1996 " See `git help clone'
1997 " https:// [user@] github.com[:port] / junegunn/vim-plug [.git]
1998 " [git@] github.com[:port] : junegunn/vim-plug [.git]
1999 " file:// / junegunn/vim-plug [/]
2000 " / junegunn/vim-plug [/]
2001 let pat = '^\%(\w\+://\)\='.'\%([^@/]*@\)\='.'\([^:/]*\%(:[0-9]*\)\=\)'.'[:/]'.'\(.\{-}\)'.'\%(\.git\)\=/\?$'
2002 let ma = matchlist(a:a, pat)
2003 let mb = matchlist(a:b, pat)
2004 return ma[1:2] ==# mb[1:2]
2005 endfunction
2006
2007 function! s:format_message(bullet, name, message)
2008 if a:bullet != 'x'
2009 return [printf('%s %s: %s', a:bullet, a:name, s:lastline(a:message))]
2010 else
2011 let lines = map(s:lines(a:message), '" ".v:val')
2012 return extend([printf('x %s:', a:name)], lines)
2013 endif
2014 endfunction
2015
2016 function! s:with_cd(cmd, dir)
2017 return printf('cd%s %s && %s', s:is_win ? ' /d' : '', s:shellesc(a:dir), a:cmd)
2018 endfunction
2019
2020 function! s:system(cmd, ...)
2021 try
2022 let [sh, shellcmdflag, shrd] = s:chsh(1)
2023 let cmd = a:0 > 0 ? s:with_cd(a:cmd, a:1) : a:cmd
2024 if s:is_win
2025 let batchfile = tempname().'.bat'
2026 call writefile(["@echo off\r", cmd . "\r"], batchfile)
2027 let cmd = batchfile
2028 endif
2029 return system(s:is_win ? '('.cmd.')' : cmd)
2030 finally
2031 let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd]
2032 if s:is_win
2033 call delete(batchfile)
2034 endif
2035 endtry
2036 endfunction
2037
2038 function! s:system_chomp(...)
2039 let ret = call('s:system', a:000)
2040 return v:shell_error ? '' : substitute(ret, '\n$', '', '')
2041 endfunction
2042
2043 function! s:git_validate(spec, check_branch)
2044 let err = ''
2045 if isdirectory(a:spec.dir)
2046 let result = s:lines(s:system('git rev-parse --abbrev-ref HEAD 2>&1 && git config -f .git/config remote.origin.url', a:spec.dir))
2047 let remote = result[-1]
2048 if v:shell_error
2049 let err = join([remote, 'PlugClean required.'], "\n")
2050 elseif !s:compare_git_uri(remote, a:spec.uri)
2051 let err = join(['Invalid URI: '.remote,
2052 \ 'Expected: '.a:spec.uri,
2053 \ 'PlugClean required.'], "\n")
2054 elseif a:check_branch && has_key(a:spec, 'commit')
2055 let result = s:lines(s:system('git rev-parse HEAD 2>&1', a:spec.dir))
2056 let sha = result[-1]
2057 if v:shell_error
2058 let err = join(add(result, 'PlugClean required.'), "\n")
2059 elseif !s:hash_match(sha, a:spec.commit)
2060 let err = join([printf('Invalid HEAD (expected: %s, actual: %s)',
2061 \ a:spec.commit[:6], sha[:6]),
2062 \ 'PlugUpdate required.'], "\n")
2063 endif
2064 elseif a:check_branch
2065 let branch = result[0]
2066 " Check tag
2067 if has_key(a:spec, 'tag')
2068 let tag = s:system_chomp('git describe --exact-match --tags HEAD 2>&1', a:spec.dir)
2069 if a:spec.tag !=# tag && a:spec.tag !~ '\*'
2070 let err = printf('Invalid tag: %s (expected: %s). Try PlugUpdate.',
2071 \ (empty(tag) ? 'N/A' : tag), a:spec.tag)
2072 endif
2073 " Check branch
2074 elseif a:spec.branch !=# branch
2075 let err = printf('Invalid branch: %s (expected: %s). Try PlugUpdate.',
2076 \ branch, a:spec.branch)
2077 endif
2078 if empty(err)
2079 let [ahead, behind] = split(s:lastline(s:system(printf(
2080 \ 'git rev-list --count --left-right HEAD...origin/%s',
2081 \ a:spec.branch), a:spec.dir)), '\t')
2082 if !v:shell_error && ahead
2083 if behind
2084 " Only mention PlugClean if diverged, otherwise it's likely to be
2085 " pushable (and probably not that messed up).
2086 let err = printf(
2087 \ "Diverged from origin/%s (%d commit(s) ahead and %d commit(s) behind!\n"
2088 \ .'Backup local changes and run PlugClean and PlugUpdate to reinstall it.', a:spec.branch, ahead, behind)
2089 else
2090 let err = printf("Ahead of origin/%s by %d commit(s).\n"
2091 \ .'Cannot update until local changes are pushed.',
2092 \ a:spec.branch, ahead)
2093 endif
2094 endif
2095 endif
2096 endif
2097 else
2098 let err = 'Not found'
2099 endif
2100 return [err, err =~# 'PlugClean']
2101 endfunction
2102
2103 function! s:rm_rf(dir)
2104 if isdirectory(a:dir)
2105 call s:system((s:is_win ? 'rmdir /S /Q ' : 'rm -rf ') . s:shellesc(a:dir))
2106 endif
2107 endfunction
2108
2109 function! s:clean(force)
2110 call s:prepare()
2111 call append(0, 'Searching for invalid plugins in '.g:plug_home)
2112 call append(1, '')
2113
2114 " List of valid directories
2115 let dirs = []
2116 let errs = {}
2117 let [cnt, total] = [0, len(g:plugs)]
2118 for [name, spec] in items(g:plugs)
2119 if !s:is_managed(name)
2120 call add(dirs, spec.dir)
2121 else
2122 let [err, clean] = s:git_validate(spec, 1)
2123 if clean
2124 let errs[spec.dir] = s:lines(err)[0]
2125 else
2126 call add(dirs, spec.dir)
2127 endif
2128 endif
2129 let cnt += 1
2130 call s:progress_bar(2, repeat('=', cnt), total)
2131 normal! 2G
2132 redraw
2133 endfor
2134
2135 let allowed = {}
2136 for dir in dirs
2137 let allowed[s:dirpath(fnamemodify(dir, ':h:h'))] = 1
2138 let allowed[dir] = 1
2139 for child in s:glob_dir(dir)
2140 let allowed[child] = 1
2141 endfor
2142 endfor
2143
2144 let todo = []
2145 let found = sort(s:glob_dir(g:plug_home))
2146 while !empty(found)
2147 let f = remove(found, 0)
2148 if !has_key(allowed, f) && isdirectory(f)
2149 call add(todo, f)
2150 call append(line('$'), '- ' . f)
2151 if has_key(errs, f)
2152 call append(line('$'), ' ' . errs[f])
2153 endif
2154 let found = filter(found, 'stridx(v:val, f) != 0')
2155 end
2156 endwhile
2157
2158 4
2159 redraw
2160 if empty(todo)
2161 call append(line('$'), 'Already clean.')
2162 else
2163 let s:clean_count = 0
2164 call append(3, ['Directories to delete:', ''])
2165 redraw!
2166 if a:force || s:ask_no_interrupt('Delete all directories?')
2167 call s:delete([6, line('$')], 1)
2168 else
2169 call setline(4, 'Cancelled.')
2170 nnoremap <silent> <buffer> d :set opfunc=<sid>delete_op<cr>g@
2171 nmap <silent> <buffer> dd d_
2172 xnoremap <silent> <buffer> d :<c-u>call <sid>delete_op(visualmode(), 1)<cr>
2173 echo 'Delete the lines (d{motion}) to delete the corresponding directories'
2174 endif
2175 endif
2176 4
2177 setlocal nomodifiable
2178 endfunction
2179
2180 function! s:delete_op(type, ...)
2181 call s:delete(a:0 ? [line("'<"), line("'>")] : [line("'["), line("']")], 0)
2182 endfunction
2183
2184 function! s:delete(range, force)
2185 let [l1, l2] = a:range
2186 let force = a:force
2187 while l1 <= l2
2188 let line = getline(l1)
2189 if line =~ '^- ' && isdirectory(line[2:])
2190 execute l1
2191 redraw!
2192 let answer = force ? 1 : s:ask('Delete '.line[2:].'?', 1)
2193 let force = force || answer > 1
2194 if answer
2195 call s:rm_rf(line[2:])
2196 setlocal modifiable
2197 call setline(l1, '~'.line[1:])
2198 let s:clean_count += 1
2199 call setline(4, printf('Removed %d directories.', s:clean_count))
2200 setlocal nomodifiable
2201 endif
2202 endif
2203 let l1 += 1
2204 endwhile
2205 endfunction
2206
2207 function! s:upgrade()
2208 echo 'Downloading the latest version of vim-plug'
2209 redraw
2210 let tmp = tempname()
2211 let new = tmp . '/plug.vim'
2212
2213 try
2214 let out = s:system(printf('git clone --depth 1 %s %s', s:plug_src, tmp))
2215 if v:shell_error
2216 return s:err('Error upgrading vim-plug: '. out)
2217 endif
2218
2219 if readfile(s:me) ==# readfile(new)
2220 echo 'vim-plug is already up-to-date'
2221 return 0
2222 else
2223 call rename(s:me, s:me . '.old')
2224 call rename(new, s:me)
2225 unlet g:loaded_plug
2226 echo 'vim-plug has been upgraded'
2227 return 1
2228 endif
2229 finally
2230 silent! call s:rm_rf(tmp)
2231 endtry
2232 endfunction
2233
2234 function! s:upgrade_specs()
2235 for spec in values(g:plugs)
2236 let spec.frozen = get(spec, 'frozen', 0)
2237 endfor
2238 endfunction
2239
2240 function! s:status()
2241 call s:prepare()
2242 call append(0, 'Checking plugins')
2243 call append(1, '')
2244
2245 let ecnt = 0
2246 let unloaded = 0
2247 let [cnt, total] = [0, len(g:plugs)]
2248 for [name, spec] in items(g:plugs)
2249 let is_dir = isdirectory(spec.dir)
2250 if has_key(spec, 'uri')
2251 if is_dir
2252 let [err, _] = s:git_validate(spec, 1)
2253 let [valid, msg] = [empty(err), empty(err) ? 'OK' : err]
2254 else
2255 let [valid, msg] = [0, 'Not found. Try PlugInstall.']
2256 endif
2257 else
2258 if is_dir
2259 let [valid, msg] = [1, 'OK']
2260 else
2261 let [valid, msg] = [0, 'Not found.']
2262 endif
2263 endif
2264 let cnt += 1
2265 let ecnt += !valid
2266 " `s:loaded` entry can be missing if PlugUpgraded
2267 if is_dir && get(s:loaded, name, -1) == 0
2268 let unloaded = 1
2269 let msg .= ' (not loaded)'
2270 endif
2271 call s:progress_bar(2, repeat('=', cnt), total)
2272 call append(3, s:format_message(valid ? '-' : 'x', name, msg))
2273 normal! 2G
2274 redraw
2275 endfor
2276 call setline(1, 'Finished. '.ecnt.' error(s).')
2277 normal! gg
2278 setlocal nomodifiable
2279 if unloaded
2280 echo "Press 'L' on each line to load plugin, or 'U' to update"
2281 nnoremap <silent> <buffer> L :call <SID>status_load(line('.'))<cr>
2282 xnoremap <silent> <buffer> L :call <SID>status_load(line('.'))<cr>
2283 end
2284 endfunction
2285
2286 function! s:extract_name(str, prefix, suffix)
2287 return matchstr(a:str, '^'.a:prefix.' \zs[^:]\+\ze:.*'.a:suffix.'$')
2288 endfunction
2289
2290 function! s:status_load(lnum)
2291 let line = getline(a:lnum)
2292 let name = s:extract_name(line, '-', '(not loaded)')
2293 if !empty(name)
2294 call plug#load(name)
2295 setlocal modifiable
2296 call setline(a:lnum, substitute(line, ' (not loaded)$', '', ''))
2297 setlocal nomodifiable
2298 endif
2299 endfunction
2300
2301 function! s:status_update() range
2302 let lines = getline(a:firstline, a:lastline)
2303 let names = filter(map(lines, 's:extract_name(v:val, "[x-]", "")'), '!empty(v:val)')
2304 if !empty(names)
2305 echo
2306 execute 'PlugUpdate' join(names)
2307 endif
2308 endfunction
2309
2310 function! s:is_preview_window_open()
2311 silent! wincmd P
2312 if &previewwindow
2313 wincmd p
2314 return 1
2315 endif
2316 endfunction
2317
2318 function! s:find_name(lnum)
2319 for lnum in reverse(range(1, a:lnum))
2320 let line = getline(lnum)
2321 if empty(line)
2322 return ''
2323 endif
2324 let name = s:extract_name(line, '-', '')
2325 if !empty(name)
2326 return name
2327 endif
2328 endfor
2329 return ''
2330 endfunction
2331
2332 function! s:preview_commit()
2333 if b:plug_preview < 0
2334 let b:plug_preview = !s:is_preview_window_open()
2335 endif
2336
2337 let sha = matchstr(getline('.'), '^ \X*\zs[0-9a-f]\{7,9}')
2338 if empty(sha)
2339 return
2340 endif
2341
2342 let name = s:find_name(line('.'))
2343 if empty(name) || !has_key(g:plugs, name) || !isdirectory(g:plugs[name].dir)
2344 return
2345 endif
2346
2347 if exists('g:plug_pwindow') && !s:is_preview_window_open()
2348 execute g:plug_pwindow
2349 execute 'e' sha
2350 else
2351 execute 'pedit' sha
2352 wincmd P
2353 endif
2354 setlocal previewwindow filetype=git buftype=nofile nobuflisted modifiable
2355 try
2356 let [sh, shellcmdflag, shrd] = s:chsh(1)
2357 let cmd = 'cd '.s:shellesc(g:plugs[name].dir).' && git show --no-color --pretty=medium '.sha
2358 if s:is_win
2359 let batchfile = tempname().'.bat'
2360 call writefile(["@echo off\r", cmd . "\r"], batchfile)
2361 let cmd = batchfile
2362 endif
2363 execute 'silent %!' cmd
2364 finally
2365 let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd]
2366 if s:is_win
2367 call delete(batchfile)
2368 endif
2369 endtry
2370 setlocal nomodifiable
2371 nnoremap <silent> <buffer> q :q<cr>
2372 wincmd p
2373 endfunction
2374
2375 function! s:section(flags)
2376 call search('\(^[x-] \)\@<=[^:]\+:', a:flags)
2377 endfunction
2378
2379 function! s:format_git_log(line)
2380 let indent = ' '
2381 let tokens = split(a:line, nr2char(1))
2382 if len(tokens) != 5
2383 return indent.substitute(a:line, '\s*$', '', '')
2384 endif
2385 let [graph, sha, refs, subject, date] = tokens
2386 let tag = matchstr(refs, 'tag: [^,)]\+')
2387 let tag = empty(tag) ? ' ' : ' ('.tag.') '
2388 return printf('%s%s%s%s%s (%s)', indent, graph, sha, tag, subject, date)
2389 endfunction
2390
2391 function! s:append_ul(lnum, text)
2392 call append(a:lnum, ['', a:text, repeat('-', len(a:text))])
2393 endfunction
2394
2395 function! s:diff()
2396 call s:prepare()
2397 call append(0, ['Collecting changes ...', ''])
2398 let cnts = [0, 0]
2399 let bar = ''
2400 let total = filter(copy(g:plugs), 's:is_managed(v:key) && isdirectory(v:val.dir)')
2401 call s:progress_bar(2, bar, len(total))
2402 for origin in [1, 0]
2403 let plugs = reverse(sort(items(filter(copy(total), (origin ? '' : '!').'(has_key(v:val, "commit") || has_key(v:val, "tag"))'))))
2404 if empty(plugs)
2405 continue
2406 endif
2407 call s:append_ul(2, origin ? 'Pending updates:' : 'Last update:')
2408 for [k, v] in plugs
2409 let range = origin ? '..origin/'.v.branch : 'HEAD@{1}..'
2410 let diff = s:system_chomp('git log --graph --color=never '.join(map(['--pretty=format:%x01%h%x01%d%x01%s%x01%cr', range], 's:shellesc(v:val)')), v.dir)
2411 if !empty(diff)
2412 let ref = has_key(v, 'tag') ? (' (tag: '.v.tag.')') : has_key(v, 'commit') ? (' '.v.commit) : ''
2413 call append(5, extend(['', '- '.k.':'.ref], map(s:lines(diff), 's:format_git_log(v:val)')))
2414 let cnts[origin] += 1
2415 endif
2416 let bar .= '='
2417 call s:progress_bar(2, bar, len(total))
2418 normal! 2G
2419 redraw
2420 endfor
2421 if !cnts[origin]
2422 call append(5, ['', 'N/A'])
2423 endif
2424 endfor
2425 call setline(1, printf('%d plugin(s) updated.', cnts[0])
2426 \ . (cnts[1] ? printf(' %d plugin(s) have pending updates.', cnts[1]) : ''))
2427
2428 if cnts[0] || cnts[1]
2429 nnoremap <silent> <buffer> <cr> :silent! call <SID>preview_commit()<cr>
2430 nnoremap <silent> <buffer> o :silent! call <SID>preview_commit()<cr>
2431 endif
2432 if cnts[0]
2433 nnoremap <silent> <buffer> X :call <SID>revert()<cr>
2434 echo "Press 'X' on each block to revert the update"
2435 endif
2436 normal! gg
2437 setlocal nomodifiable
2438 endfunction
2439
2440 function! s:revert()
2441 if search('^Pending updates', 'bnW')
2442 return
2443 endif
2444
2445 let name = s:find_name(line('.'))
2446 if empty(name) || !has_key(g:plugs, name) ||
2447 \ input(printf('Revert the update of %s? (y/N) ', name)) !~? '^y'
2448 return
2449 endif
2450
2451 call s:system('git reset --hard HEAD@{1} && git checkout '.s:esc(g:plugs[name].branch).' --', g:plugs[name].dir)
2452 setlocal modifiable
2453 normal! "_dap
2454 setlocal nomodifiable
2455 echo 'Reverted'
2456 endfunction
2457
2458 function! s:snapshot(force, ...) abort
2459 call s:prepare()
2460 setf vim
2461 call append(0, ['" Generated by vim-plug',
2462 \ '" '.strftime("%c"),
2463 \ '" :source this file in vim to restore the snapshot',
2464 \ '" or execute: vim -S snapshot.vim',
2465 \ '', '', 'PlugUpdate!'])
2466 1
2467 let anchor = line('$') - 3
2468 let names = sort(keys(filter(copy(g:plugs),
2469 \'has_key(v:val, "uri") && !has_key(v:val, "commit") && isdirectory(v:val.dir)')))
2470 for name in reverse(names)
2471 let sha = s:system_chomp('git rev-parse --short HEAD', g:plugs[name].dir)
2472 if !empty(sha)
2473 call append(anchor, printf("silent! let g:plugs['%s'].commit = '%s'", name, sha))
2474 redraw
2475 endif
2476 endfor
2477
2478 if a:0 > 0
2479 let fn = expand(a:1)
2480 if filereadable(fn) && !(a:force || s:ask(a:1.' already exists. Overwrite?'))
2481 return
2482 endif
2483 call writefile(getline(1, '$'), fn)
2484 echo 'Saved as '.a:1
2485 silent execute 'e' s:esc(fn)
2486 setf vim
2487 endif
2488 endfunction
2489
2490 function! s:split_rtp()
2491 return split(&rtp, '\\\@<!,')
2492 endfunction
2493
2494 let s:first_rtp = s:escrtp(get(s:split_rtp(), 0, ''))
2495 let s:last_rtp = s:escrtp(get(s:split_rtp(), -1, ''))
2496
2497 if exists('g:plugs')
2498 let g:plugs_order = get(g:, 'plugs_order', keys(g:plugs))
2499 call s:upgrade_specs()
2500 call s:define_commands()
2501 endif
2502
2503 let &cpo = s:cpo_save
2504 unlet s:cpo_save