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