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