comparison autoload/plug.vim @ 0:a4ec03f77554

I don't know...
author luka
date Fri, 11 Apr 2025 21:07:37 -0400
parents
children 1a705d7a7521
comparison
equal deleted inserted replaced
-1:000000000000 0:a4ec03f77554
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:disable_credential_helper()
1079 return s:git_version_requirement(2) && get(g:, 'plug_disable_credential_helper', 1)
1080 endfunction
1081
1082 function! s:checkout(spec)
1083 let sha = a:spec.commit
1084 let output = s:git_revision(a:spec.dir)
1085 let error = 0
1086 if !empty(output) && !s:hash_match(sha, s:lines(output)[0])
1087 let credential_helper = s:disable_credential_helper() ? '-c credential.helper= ' : ''
1088 let output = s:system(
1089 \ 'git '.credential_helper.'fetch --depth 999999 && git checkout '.plug#shellescape(sha).' --', a:spec.dir)
1090 let error = v:shell_error
1091 endif
1092 return [output, error]
1093 endfunction
1094
1095 function! s:finish(pull)
1096 let new_frozen = len(filter(keys(s:update.new), 'g:plugs[v:val].frozen'))
1097 if new_frozen
1098 let s = new_frozen > 1 ? 's' : ''
1099 call append(3, printf('- Installed %d frozen plugin%s', new_frozen, s))
1100 endif
1101 call append(3, '- Finishing ... ') | 4
1102 redraw
1103 call plug#helptags()
1104 call plug#end()
1105 call setline(4, getline(4) . 'Done!')
1106 redraw
1107 let msgs = []
1108 if !empty(s:update.errors)
1109 call add(msgs, "Press 'R' to retry.")
1110 endif
1111 if a:pull && len(s:update.new) < len(filter(getline(5, '$'),
1112 \ "v:val =~ '^- ' && v:val !~# 'Already up.to.date'"))
1113 call add(msgs, "Press 'D' to see the updated changes.")
1114 endif
1115 echo join(msgs, ' ')
1116 call s:finish_bindings()
1117 endfunction
1118
1119 function! s:retry()
1120 if empty(s:update.errors)
1121 return
1122 endif
1123 echo
1124 call s:update_impl(s:update.pull, s:update.force,
1125 \ extend(copy(s:update.errors), [s:update.threads]))
1126 endfunction
1127
1128 function! s:is_managed(name)
1129 return has_key(g:plugs[a:name], 'uri')
1130 endfunction
1131
1132 function! s:names(...)
1133 return sort(filter(keys(g:plugs), 'stridx(v:val, a:1) == 0 && s:is_managed(v:val)'))
1134 endfunction
1135
1136 function! s:check_ruby()
1137 silent! ruby require 'thread'; VIM::command("let g:plug_ruby = '#{RUBY_VERSION}'")
1138 if !exists('g:plug_ruby')
1139 redraw!
1140 return s:warn('echom', 'Warning: Ruby interface is broken')
1141 endif
1142 let ruby_version = split(g:plug_ruby, '\.')
1143 unlet g:plug_ruby
1144 return s:version_requirement(ruby_version, [1, 8, 7])
1145 endfunction
1146
1147 function! s:update_impl(pull, force, args) abort
1148 let sync = index(a:args, '--sync') >= 0 || has('vim_starting')
1149 let args = filter(copy(a:args), 'v:val != "--sync"')
1150 let threads = (len(args) > 0 && args[-1] =~ '^[1-9][0-9]*$') ?
1151 \ remove(args, -1) : get(g:, 'plug_threads', 16)
1152
1153 let managed = filter(deepcopy(g:plugs), 's:is_managed(v:key)')
1154 let todo = empty(args) ? filter(managed, '!v:val.frozen || !isdirectory(v:val.dir)') :
1155 \ filter(managed, 'index(args, v:key) >= 0')
1156
1157 if empty(todo)
1158 return s:warn('echo', 'No plugin to '. (a:pull ? 'update' : 'install'))
1159 endif
1160
1161 if !s:is_win && s:git_version_requirement(2, 3)
1162 let s:git_terminal_prompt = exists('$GIT_TERMINAL_PROMPT') ? $GIT_TERMINAL_PROMPT : ''
1163 let $GIT_TERMINAL_PROMPT = 0
1164 for plug in values(todo)
1165 let plug.uri = substitute(plug.uri,
1166 \ '^https://git::@github\.com', 'https://github.com', '')
1167 endfor
1168 endif
1169
1170 if !isdirectory(g:plug_home)
1171 try
1172 call mkdir(g:plug_home, 'p')
1173 catch
1174 return s:err(printf('Invalid plug directory: %s. '.
1175 \ 'Try to call plug#begin with a valid directory', g:plug_home))
1176 endtry
1177 endif
1178
1179 if has('nvim') && !exists('*jobwait') && threads > 1
1180 call s:warn('echom', '[vim-plug] Update Neovim for parallel installer')
1181 endif
1182
1183 let use_job = s:nvim || s:vim8
1184 let python = (has('python') || has('python3')) && !use_job
1185 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()
1186
1187 let s:update = {
1188 \ 'start': reltime(),
1189 \ 'all': todo,
1190 \ 'todo': copy(todo),
1191 \ 'errors': [],
1192 \ 'pull': a:pull,
1193 \ 'force': a:force,
1194 \ 'new': {},
1195 \ 'threads': (python || ruby || use_job) ? min([len(todo), threads]) : 1,
1196 \ 'bar': '',
1197 \ 'fin': 0
1198 \ }
1199
1200 call s:prepare(1)
1201 call append(0, ['', ''])
1202 normal! 2G
1203 silent! redraw
1204
1205 " Set remote name, overriding a possible user git config's clone.defaultRemoteName
1206 let s:clone_opt = ['--origin', 'origin']
1207 if get(g:, 'plug_shallow', 1)
1208 call extend(s:clone_opt, ['--depth', '1'])
1209 if s:git_version_requirement(1, 7, 10)
1210 call add(s:clone_opt, '--no-single-branch')
1211 endif
1212 endif
1213
1214 if has('win32unix') || has('wsl')
1215 call extend(s:clone_opt, ['-c', 'core.eol=lf', '-c', 'core.autocrlf=input'])
1216 endif
1217
1218 let s:submodule_opt = s:git_version_requirement(2, 8) ? ' --jobs='.threads : ''
1219
1220 " Python version requirement (>= 2.7)
1221 if python && !has('python3') && !ruby && !use_job && s:update.threads > 1
1222 redir => pyv
1223 silent python import platform; print platform.python_version()
1224 redir END
1225 let python = s:version_requirement(
1226 \ map(split(split(pyv)[0], '\.'), 'str2nr(v:val)'), [2, 6])
1227 endif
1228
1229 if (python || ruby) && s:update.threads > 1
1230 try
1231 let imd = &imd
1232 if s:mac_gui
1233 set noimd
1234 endif
1235 if ruby
1236 call s:update_ruby()
1237 else
1238 call s:update_python()
1239 endif
1240 catch
1241 let lines = getline(4, '$')
1242 let printed = {}
1243 silent! 4,$d _
1244 for line in lines
1245 let name = s:extract_name(line, '.', '')
1246 if empty(name) || !has_key(printed, name)
1247 call append('$', line)
1248 if !empty(name)
1249 let printed[name] = 1
1250 if line[0] == 'x' && index(s:update.errors, name) < 0
1251 call add(s:update.errors, name)
1252 end
1253 endif
1254 endif
1255 endfor
1256 finally
1257 let &imd = imd
1258 call s:update_finish()
1259 endtry
1260 else
1261 call s:update_vim()
1262 while use_job && sync
1263 sleep 100m
1264 if s:update.fin
1265 break
1266 endif
1267 endwhile
1268 endif
1269 endfunction
1270
1271 function! s:log4(name, msg)
1272 call setline(4, printf('- %s (%s)', a:msg, a:name))
1273 redraw
1274 endfunction
1275
1276 function! s:update_finish()
1277 if exists('s:git_terminal_prompt')
1278 let $GIT_TERMINAL_PROMPT = s:git_terminal_prompt
1279 endif
1280 if s:switch_in()
1281 call append(3, '- Updating ...') | 4
1282 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))'))
1283 let [pos, _] = s:logpos(name)
1284 if !pos
1285 continue
1286 endif
1287 let out = ''
1288 let error = 0
1289 if has_key(spec, 'commit')
1290 call s:log4(name, 'Checking out '.spec.commit)
1291 let [out, error] = s:checkout(spec)
1292 elseif has_key(spec, 'tag')
1293 let tag = spec.tag
1294 if tag =~ '\*'
1295 let tags = s:lines(s:system('git tag --list '.plug#shellescape(tag).' --sort -version:refname 2>&1', spec.dir))
1296 if !v:shell_error && !empty(tags)
1297 let tag = tags[0]
1298 call s:log4(name, printf('Latest tag for %s -> %s', spec.tag, tag))
1299 call append(3, '')
1300 endif
1301 endif
1302 call s:log4(name, 'Checking out '.tag)
1303 let out = s:system('git checkout -q '.plug#shellescape(tag).' -- 2>&1', spec.dir)
1304 let error = v:shell_error
1305 endif
1306 if !error && filereadable(spec.dir.'/.gitmodules') &&
1307 \ (s:update.force || has_key(s:update.new, name) || s:is_updated(spec.dir))
1308 call s:log4(name, 'Updating submodules. This may take a while.')
1309 let out .= s:bang('git submodule update --init --recursive'.s:submodule_opt.' 2>&1', spec.dir)
1310 let error = v:shell_error
1311 endif
1312 let msg = s:format_message(v:shell_error ? 'x': '-', name, out)
1313 if error
1314 call add(s:update.errors, name)
1315 call s:regress_bar()
1316 silent execute pos 'd _'
1317 call append(4, msg) | 4
1318 elseif !empty(out)
1319 call setline(pos, msg[0])
1320 endif
1321 redraw
1322 endfor
1323 silent 4 d _
1324 try
1325 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")'))
1326 catch
1327 call s:warn('echom', v:exception)
1328 call s:warn('echo', '')
1329 return
1330 endtry
1331 call s:finish(s:update.pull)
1332 call setline(1, 'Updated. Elapsed time: ' . split(reltimestr(reltime(s:update.start)))[0] . ' sec.')
1333 call s:switch_out('normal! gg')
1334 endif
1335 endfunction
1336
1337 function! s:mark_aborted(name, message)
1338 let attrs = { 'running': 0, 'error': 1, 'abort': 1, 'lines': [a:message] }
1339 let s:jobs[a:name] = extend(get(s:jobs, a:name, {}), attrs)
1340 endfunction
1341
1342 function! s:job_abort(cancel)
1343 if (!s:nvim && !s:vim8) || !exists('s:jobs')
1344 return
1345 endif
1346
1347 for [name, j] in items(s:jobs)
1348 if s:nvim
1349 silent! call jobstop(j.jobid)
1350 elseif s:vim8
1351 silent! call job_stop(j.jobid)
1352 endif
1353 if j.new
1354 call s:rm_rf(g:plugs[name].dir)
1355 endif
1356 if a:cancel
1357 call s:mark_aborted(name, 'Aborted')
1358 endif
1359 endfor
1360
1361 if a:cancel
1362 for todo in values(s:update.todo)
1363 let todo.abort = 1
1364 endfor
1365 else
1366 let s:jobs = {}
1367 endif
1368 endfunction
1369
1370 function! s:last_non_empty_line(lines)
1371 let len = len(a:lines)
1372 for idx in range(len)
1373 let line = a:lines[len-idx-1]
1374 if !empty(line)
1375 return line
1376 endif
1377 endfor
1378 return ''
1379 endfunction
1380
1381 function! s:bullet_for(job, ...)
1382 if a:job.running
1383 return a:job.new ? '+' : '*'
1384 endif
1385 if get(a:job, 'abort', 0)
1386 return '~'
1387 endif
1388 return a:job.error ? 'x' : get(a:000, 0, '-')
1389 endfunction
1390
1391 function! s:job_out_cb(self, data) abort
1392 let self = a:self
1393 let data = remove(self.lines, -1) . a:data
1394 let lines = map(split(data, "\n", 1), 'split(v:val, "\r", 1)[-1]')
1395 call extend(self.lines, lines)
1396 " To reduce the number of buffer updates
1397 let self.tick = get(self, 'tick', -1) + 1
1398 if !self.running || self.tick % len(s:jobs) == 0
1399 let result = self.error ? join(self.lines, "\n") : s:last_non_empty_line(self.lines)
1400 if len(result)
1401 call s:log(s:bullet_for(self), self.name, result)
1402 endif
1403 endif
1404 endfunction
1405
1406 function! s:job_exit_cb(self, data) abort
1407 let a:self.running = 0
1408 let a:self.error = a:data != 0
1409 call s:reap(a:self.name)
1410 call s:tick()
1411 endfunction
1412
1413 function! s:job_cb(fn, job, ch, data)
1414 if !s:plug_window_exists() " plug window closed
1415 return s:job_abort(0)
1416 endif
1417 call call(a:fn, [a:job, a:data])
1418 endfunction
1419
1420 function! s:nvim_cb(job_id, data, event) dict abort
1421 return (a:event == 'stdout' || a:event == 'stderr') ?
1422 \ s:job_cb('s:job_out_cb', self, 0, join(a:data, "\n")) :
1423 \ s:job_cb('s:job_exit_cb', self, 0, a:data)
1424 endfunction
1425
1426 function! s:spawn(name, spec, queue, opts)
1427 let job = { 'name': a:name, 'spec': a:spec, 'running': 1, 'error': 0, 'lines': [''],
1428 \ 'new': get(a:opts, 'new', 0), 'queue': copy(a:queue) }
1429 let Item = remove(job.queue, 0)
1430 let argv = type(Item) == s:TYPE.funcref ? call(Item, [a:spec]) : Item
1431 let s:jobs[a:name] = job
1432
1433 if s:nvim
1434 if has_key(a:opts, 'dir')
1435 let job.cwd = a:opts.dir
1436 endif
1437 call extend(job, {
1438 \ 'on_stdout': function('s:nvim_cb'),
1439 \ 'on_stderr': function('s:nvim_cb'),
1440 \ 'on_exit': function('s:nvim_cb'),
1441 \ })
1442 let jid = s:plug_call('jobstart', argv, job)
1443 if jid > 0
1444 let job.jobid = jid
1445 else
1446 let job.running = 0
1447 let job.error = 1
1448 let job.lines = [jid < 0 ? argv[0].' is not executable' :
1449 \ 'Invalid arguments (or job table is full)']
1450 endif
1451 elseif s:vim8
1452 let cmd = join(map(copy(argv), 'plug#shellescape(v:val, {"script": 0})'))
1453 if has_key(a:opts, 'dir')
1454 let cmd = s:with_cd(cmd, a:opts.dir, 0)
1455 endif
1456 let argv = s:is_win ? ['cmd', '/s', '/c', '"'.cmd.'"'] : ['sh', '-c', cmd]
1457 let jid = job_start(s:is_win ? join(argv, ' ') : argv, {
1458 \ 'out_cb': function('s:job_cb', ['s:job_out_cb', job]),
1459 \ 'err_cb': function('s:job_cb', ['s:job_out_cb', job]),
1460 \ 'exit_cb': function('s:job_cb', ['s:job_exit_cb', job]),
1461 \ 'err_mode': 'raw',
1462 \ 'out_mode': 'raw'
1463 \})
1464 if job_status(jid) == 'run'
1465 let job.jobid = jid
1466 else
1467 let job.running = 0
1468 let job.error = 1
1469 let job.lines = ['Failed to start job']
1470 endif
1471 else
1472 let job.lines = s:lines(call('s:system', has_key(a:opts, 'dir') ? [argv, a:opts.dir] : [argv]))
1473 let job.error = v:shell_error != 0
1474 let job.running = 0
1475 endif
1476 endfunction
1477
1478 function! s:reap(name)
1479 let job = remove(s:jobs, a:name)
1480 if job.error
1481 call add(s:update.errors, a:name)
1482 elseif get(job, 'new', 0)
1483 let s:update.new[a:name] = 1
1484 endif
1485
1486 let more = len(get(job, 'queue', []))
1487 let result = job.error ? join(job.lines, "\n") : s:last_non_empty_line(job.lines)
1488 if len(result)
1489 call s:log(s:bullet_for(job), a:name, result)
1490 endif
1491
1492 if !job.error && more
1493 let job.spec.queue = job.queue
1494 let s:update.todo[a:name] = job.spec
1495 else
1496 let s:update.bar .= s:bullet_for(job, '=')
1497 call s:bar()
1498 endif
1499 endfunction
1500
1501 function! s:bar()
1502 if s:switch_in()
1503 let total = len(s:update.all)
1504 call setline(1, (s:update.pull ? 'Updating' : 'Installing').
1505 \ ' plugins ('.len(s:update.bar).'/'.total.')')
1506 call s:progress_bar(2, s:update.bar, total)
1507 call s:switch_out()
1508 endif
1509 endfunction
1510
1511 function! s:logpos(name)
1512 let max = line('$')
1513 for i in range(4, max > 4 ? max : 4)
1514 if getline(i) =~# '^[-+x*] '.a:name.':'
1515 for j in range(i + 1, max > 5 ? max : 5)
1516 if getline(j) !~ '^ '
1517 return [i, j - 1]
1518 endif
1519 endfor
1520 return [i, i]
1521 endif
1522 endfor
1523 return [0, 0]
1524 endfunction
1525
1526 function! s:log(bullet, name, lines)
1527 if s:switch_in()
1528 let [b, e] = s:logpos(a:name)
1529 if b > 0
1530 silent execute printf('%d,%d d _', b, e)
1531 if b > winheight('.')
1532 let b = 4
1533 endif
1534 else
1535 let b = 4
1536 endif
1537 " FIXME For some reason, nomodifiable is set after :d in vim8
1538 setlocal modifiable
1539 call append(b - 1, s:format_message(a:bullet, a:name, a:lines))
1540 call s:switch_out()
1541 endif
1542 endfunction
1543
1544 function! s:update_vim()
1545 let s:jobs = {}
1546
1547 call s:bar()
1548 call s:tick()
1549 endfunction
1550
1551 function! s:checkout_command(spec)
1552 let a:spec.branch = s:git_origin_branch(a:spec)
1553 return ['git', 'checkout', '-q', a:spec.branch, '--']
1554 endfunction
1555
1556 function! s:merge_command(spec)
1557 let a:spec.branch = s:git_origin_branch(a:spec)
1558 return ['git', 'merge', '--ff-only', 'origin/'.a:spec.branch]
1559 endfunction
1560
1561 function! s:tick()
1562 let pull = s:update.pull
1563 let prog = s:progress_opt(s:nvim || s:vim8)
1564 while 1 " Without TCO, Vim stack is bound to explode
1565 if empty(s:update.todo)
1566 if empty(s:jobs) && !s:update.fin
1567 call s:update_finish()
1568 let s:update.fin = 1
1569 endif
1570 return
1571 endif
1572
1573 let name = keys(s:update.todo)[0]
1574 let spec = remove(s:update.todo, name)
1575 if get(spec, 'abort', 0)
1576 call s:mark_aborted(name, 'Skipped')
1577 call s:reap(name)
1578 continue
1579 endif
1580
1581 let queue = get(spec, 'queue', [])
1582 let new = empty(globpath(spec.dir, '.git', 1))
1583
1584 if empty(queue)
1585 call s:log(new ? '+' : '*', name, pull ? 'Updating ...' : 'Installing ...')
1586 redraw
1587 endif
1588
1589 let has_tag = has_key(spec, 'tag')
1590 if len(queue)
1591 call s:spawn(name, spec, queue, { 'dir': spec.dir })
1592 elseif !new
1593 let [error, _] = s:git_validate(spec, 0)
1594 if empty(error)
1595 if pull
1596 let cmd = s:disable_credential_helper() ? ['git', '-c', 'credential.helper=', 'fetch'] : ['git', 'fetch']
1597 if has_tag && !empty(globpath(spec.dir, '.git/shallow'))
1598 call extend(cmd, ['--depth', '99999999'])
1599 endif
1600 if !empty(prog)
1601 call add(cmd, prog)
1602 endif
1603 let queue = [cmd, split('git remote set-head origin -a')]
1604 if !has_tag && !has_key(spec, 'commit')
1605 call extend(queue, [function('s:checkout_command'), function('s:merge_command')])
1606 endif
1607 call s:spawn(name, spec, queue, { 'dir': spec.dir })
1608 else
1609 let s:jobs[name] = { 'running': 0, 'lines': ['Already installed'], 'error': 0 }
1610 endif
1611 else
1612 let s:jobs[name] = { 'running': 0, 'lines': s:lines(error), 'error': 1 }
1613 endif
1614 else
1615 let cmd = ['git', 'clone']
1616 if !has_tag
1617 call extend(cmd, s:clone_opt)
1618 endif
1619 if !empty(prog)
1620 call add(cmd, prog)
1621 endif
1622 call s:spawn(name, spec, [extend(cmd, [spec.uri, s:trim(spec.dir)]), function('s:checkout_command'), function('s:merge_command')], { 'new': 1 })
1623 endif
1624
1625 if !s:jobs[name].running
1626 call s:reap(name)
1627 endif
1628 if len(s:jobs) >= s:update.threads
1629 break
1630 endif
1631 endwhile
1632 endfunction
1633
1634 function! s:update_python()
1635 let py_exe = has('python') ? 'python' : 'python3'
1636 execute py_exe "<< EOF"
1637 import datetime
1638 import functools
1639 import os
1640 try:
1641 import queue
1642 except ImportError:
1643 import Queue as queue
1644 import random
1645 import re
1646 import shutil
1647 import signal
1648 import subprocess
1649 import tempfile
1650 import threading as thr
1651 import time
1652 import traceback
1653 import vim
1654
1655 G_NVIM = vim.eval("has('nvim')") == '1'
1656 G_PULL = vim.eval('s:update.pull') == '1'
1657 G_RETRIES = int(vim.eval('get(g:, "plug_retries", 2)')) + 1
1658 G_TIMEOUT = int(vim.eval('get(g:, "plug_timeout", 60)'))
1659 G_CLONE_OPT = ' '.join(vim.eval('s:clone_opt'))
1660 G_PROGRESS = vim.eval('s:progress_opt(1)')
1661 G_LOG_PROB = 1.0 / int(vim.eval('s:update.threads'))
1662 G_STOP = thr.Event()
1663 G_IS_WIN = vim.eval('s:is_win') == '1'
1664
1665 class PlugError(Exception):
1666 def __init__(self, msg):
1667 self.msg = msg
1668 class CmdTimedOut(PlugError):
1669 pass
1670 class CmdFailed(PlugError):
1671 pass
1672 class InvalidURI(PlugError):
1673 pass
1674 class Action(object):
1675 INSTALL, UPDATE, ERROR, DONE = ['+', '*', 'x', '-']
1676
1677 class Buffer(object):
1678 def __init__(self, lock, num_plugs, is_pull):
1679 self.bar = ''
1680 self.event = 'Updating' if is_pull else 'Installing'
1681 self.lock = lock
1682 self.maxy = int(vim.eval('winheight(".")'))
1683 self.num_plugs = num_plugs
1684
1685 def __where(self, name):
1686 """ Find first line with name in current buffer. Return line num. """
1687 found, lnum = False, 0
1688 matcher = re.compile('^[-+x*] {0}:'.format(name))
1689 for line in vim.current.buffer:
1690 if matcher.search(line) is not None:
1691 found = True
1692 break
1693 lnum += 1
1694
1695 if not found:
1696 lnum = -1
1697 return lnum
1698
1699 def header(self):
1700 curbuf = vim.current.buffer
1701 curbuf[0] = self.event + ' plugins ({0}/{1})'.format(len(self.bar), self.num_plugs)
1702
1703 num_spaces = self.num_plugs - len(self.bar)
1704 curbuf[1] = '[{0}{1}]'.format(self.bar, num_spaces * ' ')
1705
1706 with self.lock:
1707 vim.command('normal! 2G')
1708 vim.command('redraw')
1709
1710 def write(self, action, name, lines):
1711 first, rest = lines[0], lines[1:]
1712 msg = ['{0} {1}{2}{3}'.format(action, name, ': ' if first else '', first)]
1713 msg.extend([' ' + line for line in rest])
1714
1715 try:
1716 if action == Action.ERROR:
1717 self.bar += 'x'
1718 vim.command("call add(s:update.errors, '{0}')".format(name))
1719 elif action == Action.DONE:
1720 self.bar += '='
1721
1722 curbuf = vim.current.buffer
1723 lnum = self.__where(name)
1724 if lnum != -1: # Found matching line num
1725 del curbuf[lnum]
1726 if lnum > self.maxy and action in set([Action.INSTALL, Action.UPDATE]):
1727 lnum = 3
1728 else:
1729 lnum = 3
1730 curbuf.append(msg, lnum)
1731
1732 self.header()
1733 except vim.error:
1734 pass
1735
1736 class Command(object):
1737 CD = 'cd /d' if G_IS_WIN else 'cd'
1738
1739 def __init__(self, cmd, cmd_dir=None, timeout=60, cb=None, clean=None):
1740 self.cmd = cmd
1741 if cmd_dir:
1742 self.cmd = '{0} {1} && {2}'.format(Command.CD, cmd_dir, self.cmd)
1743 self.timeout = timeout
1744 self.callback = cb if cb else (lambda msg: None)
1745 self.clean = clean if clean else (lambda: None)
1746 self.proc = None
1747
1748 @property
1749 def alive(self):
1750 """ Returns true only if command still running. """
1751 return self.proc and self.proc.poll() is None
1752
1753 def execute(self, ntries=3):
1754 """ Execute the command with ntries if CmdTimedOut.
1755 Returns the output of the command if no Exception.
1756 """
1757 attempt, finished, limit = 0, False, self.timeout
1758
1759 while not finished:
1760 try:
1761 attempt += 1
1762 result = self.try_command()
1763 finished = True
1764 return result
1765 except CmdTimedOut:
1766 if attempt != ntries:
1767 self.notify_retry()
1768 self.timeout += limit
1769 else:
1770 raise
1771
1772 def notify_retry(self):
1773 """ Retry required for command, notify user. """
1774 for count in range(3, 0, -1):
1775 if G_STOP.is_set():
1776 raise KeyboardInterrupt
1777 msg = 'Timeout. Will retry in {0} second{1} ...'.format(
1778 count, 's' if count != 1 else '')
1779 self.callback([msg])
1780 time.sleep(1)
1781 self.callback(['Retrying ...'])
1782
1783 def try_command(self):
1784 """ Execute a cmd & poll for callback. Returns list of output.
1785 Raises CmdFailed -> return code for Popen isn't 0
1786 Raises CmdTimedOut -> command exceeded timeout without new output
1787 """
1788 first_line = True
1789
1790 try:
1791 tfile = tempfile.NamedTemporaryFile(mode='w+b')
1792 preexec_fn = not G_IS_WIN and os.setsid or None
1793 self.proc = subprocess.Popen(self.cmd, stdout=tfile,
1794 stderr=subprocess.STDOUT,
1795 stdin=subprocess.PIPE, shell=True,
1796 preexec_fn=preexec_fn)
1797 thrd = thr.Thread(target=(lambda proc: proc.wait()), args=(self.proc,))
1798 thrd.start()
1799
1800 thread_not_started = True
1801 while thread_not_started:
1802 try:
1803 thrd.join(0.1)
1804 thread_not_started = False
1805 except RuntimeError:
1806 pass
1807
1808 while self.alive:
1809 if G_STOP.is_set():
1810 raise KeyboardInterrupt
1811
1812 if first_line or random.random() < G_LOG_PROB:
1813 first_line = False
1814 line = '' if G_IS_WIN else nonblock_read(tfile.name)
1815 if line:
1816 self.callback([line])
1817
1818 time_diff = time.time() - os.path.getmtime(tfile.name)
1819 if time_diff > self.timeout:
1820 raise CmdTimedOut(['Timeout!'])
1821
1822 thrd.join(0.5)
1823
1824 tfile.seek(0)
1825 result = [line.decode('utf-8', 'replace').rstrip() for line in tfile]
1826
1827 if self.proc.returncode != 0:
1828 raise CmdFailed([''] + result)
1829
1830 return result
1831 except:
1832 self.terminate()
1833 raise
1834
1835 def terminate(self):
1836 """ Terminate process and cleanup. """
1837 if self.alive:
1838 if G_IS_WIN:
1839 os.kill(self.proc.pid, signal.SIGINT)
1840 else:
1841 os.killpg(self.proc.pid, signal.SIGTERM)
1842 self.clean()
1843
1844 class Plugin(object):
1845 def __init__(self, name, args, buf_q, lock):
1846 self.name = name
1847 self.args = args
1848 self.buf_q = buf_q
1849 self.lock = lock
1850 self.tag = args.get('tag', 0)
1851
1852 def manage(self):
1853 try:
1854 if os.path.exists(self.args['dir']):
1855 self.update()
1856 else:
1857 self.install()
1858 with self.lock:
1859 thread_vim_command("let s:update.new['{0}'] = 1".format(self.name))
1860 except PlugError as exc:
1861 self.write(Action.ERROR, self.name, exc.msg)
1862 except KeyboardInterrupt:
1863 G_STOP.set()
1864 self.write(Action.ERROR, self.name, ['Interrupted!'])
1865 except:
1866 # Any exception except those above print stack trace
1867 msg = 'Trace:\n{0}'.format(traceback.format_exc().rstrip())
1868 self.write(Action.ERROR, self.name, msg.split('\n'))
1869 raise
1870
1871 def install(self):
1872 target = self.args['dir']
1873 if target[-1] == '\\':
1874 target = target[0:-1]
1875
1876 def clean(target):
1877 def _clean():
1878 try:
1879 shutil.rmtree(target)
1880 except OSError:
1881 pass
1882 return _clean
1883
1884 self.write(Action.INSTALL, self.name, ['Installing ...'])
1885 callback = functools.partial(self.write, Action.INSTALL, self.name)
1886 cmd = 'git clone {0} {1} {2} {3} 2>&1'.format(
1887 '' if self.tag else G_CLONE_OPT, G_PROGRESS, self.args['uri'],
1888 esc(target))
1889 com = Command(cmd, None, G_TIMEOUT, callback, clean(target))
1890 result = com.execute(G_RETRIES)
1891 self.write(Action.DONE, self.name, result[-1:])
1892
1893 def repo_uri(self):
1894 cmd = 'git rev-parse --abbrev-ref HEAD 2>&1 && git config -f .git/config remote.origin.url'
1895 command = Command(cmd, self.args['dir'], G_TIMEOUT,)
1896 result = command.execute(G_RETRIES)
1897 return result[-1]
1898
1899 def update(self):
1900 actual_uri = self.repo_uri()
1901 expect_uri = self.args['uri']
1902 regex = re.compile(r'^(?:\w+://)?(?:[^@/]*@)?([^:/]*(?::[0-9]*)?)[:/](.*?)(?:\.git)?/?$')
1903 ma = regex.match(actual_uri)
1904 mb = regex.match(expect_uri)
1905 if ma is None or mb is None or ma.groups() != mb.groups():
1906 msg = ['',
1907 'Invalid URI: {0}'.format(actual_uri),
1908 'Expected {0}'.format(expect_uri),
1909 'PlugClean required.']
1910 raise InvalidURI(msg)
1911
1912 if G_PULL:
1913 self.write(Action.UPDATE, self.name, ['Updating ...'])
1914 callback = functools.partial(self.write, Action.UPDATE, self.name)
1915 fetch_opt = '--depth 99999999' if self.tag and os.path.isfile(os.path.join(self.args['dir'], '.git/shallow')) else ''
1916 cmd = 'git fetch {0} {1} 2>&1'.format(fetch_opt, G_PROGRESS)
1917 com = Command(cmd, self.args['dir'], G_TIMEOUT, callback)
1918 result = com.execute(G_RETRIES)
1919 self.write(Action.DONE, self.name, result[-1:])
1920 else:
1921 self.write(Action.DONE, self.name, ['Already installed'])
1922
1923 def write(self, action, name, msg):
1924 self.buf_q.put((action, name, msg))
1925
1926 class PlugThread(thr.Thread):
1927 def __init__(self, tname, args):
1928 super(PlugThread, self).__init__()
1929 self.tname = tname
1930 self.args = args
1931
1932 def run(self):
1933 thr.current_thread().name = self.tname
1934 buf_q, work_q, lock = self.args
1935
1936 try:
1937 while not G_STOP.is_set():
1938 name, args = work_q.get_nowait()
1939 plug = Plugin(name, args, buf_q, lock)
1940 plug.manage()
1941 work_q.task_done()
1942 except queue.Empty:
1943 pass
1944
1945 class RefreshThread(thr.Thread):
1946 def __init__(self, lock):
1947 super(RefreshThread, self).__init__()
1948 self.lock = lock
1949 self.running = True
1950
1951 def run(self):
1952 while self.running:
1953 with self.lock:
1954 thread_vim_command('noautocmd normal! a')
1955 time.sleep(0.33)
1956
1957 def stop(self):
1958 self.running = False
1959
1960 if G_NVIM:
1961 def thread_vim_command(cmd):
1962 vim.session.threadsafe_call(lambda: vim.command(cmd))
1963 else:
1964 def thread_vim_command(cmd):
1965 vim.command(cmd)
1966
1967 def esc(name):
1968 return '"' + name.replace('"', '\"') + '"'
1969
1970 def nonblock_read(fname):
1971 """ Read a file with nonblock flag. Return the last line. """
1972 fread = os.open(fname, os.O_RDONLY | os.O_NONBLOCK)
1973 buf = os.read(fread, 100000).decode('utf-8', 'replace')
1974 os.close(fread)
1975
1976 line = buf.rstrip('\r\n')
1977 left = max(line.rfind('\r'), line.rfind('\n'))
1978 if left != -1:
1979 left += 1
1980 line = line[left:]
1981
1982 return line
1983
1984 def main():
1985 thr.current_thread().name = 'main'
1986 nthreads = int(vim.eval('s:update.threads'))
1987 plugs = vim.eval('s:update.todo')
1988 mac_gui = vim.eval('s:mac_gui') == '1'
1989
1990 lock = thr.Lock()
1991 buf = Buffer(lock, len(plugs), G_PULL)
1992 buf_q, work_q = queue.Queue(), queue.Queue()
1993 for work in plugs.items():
1994 work_q.put(work)
1995
1996 start_cnt = thr.active_count()
1997 for num in range(nthreads):
1998 tname = 'PlugT-{0:02}'.format(num)
1999 thread = PlugThread(tname, (buf_q, work_q, lock))
2000 thread.start()
2001 if mac_gui:
2002 rthread = RefreshThread(lock)
2003 rthread.start()
2004
2005 while not buf_q.empty() or thr.active_count() != start_cnt:
2006 try:
2007 action, name, msg = buf_q.get(True, 0.25)
2008 buf.write(action, name, ['OK'] if not msg else msg)
2009 buf_q.task_done()
2010 except queue.Empty:
2011 pass
2012 except KeyboardInterrupt:
2013 G_STOP.set()
2014
2015 if mac_gui:
2016 rthread.stop()
2017 rthread.join()
2018
2019 main()
2020 EOF
2021 endfunction
2022
2023 function! s:update_ruby()
2024 ruby << EOF
2025 module PlugStream
2026 SEP = ["\r", "\n", nil]
2027 def get_line
2028 buffer = ''
2029 loop do
2030 char = readchar rescue return
2031 if SEP.include? char.chr
2032 buffer << $/
2033 break
2034 else
2035 buffer << char
2036 end
2037 end
2038 buffer
2039 end
2040 end unless defined?(PlugStream)
2041
2042 def esc arg
2043 %["#{arg.gsub('"', '\"')}"]
2044 end
2045
2046 def killall pid
2047 pids = [pid]
2048 if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
2049 pids.each { |pid| Process.kill 'INT', pid.to_i rescue nil }
2050 else
2051 unless `which pgrep 2> /dev/null`.empty?
2052 children = pids
2053 until children.empty?
2054 children = children.map { |pid|
2055 `pgrep -P #{pid}`.lines.map { |l| l.chomp }
2056 }.flatten
2057 pids += children
2058 end
2059 end
2060 pids.each { |pid| Process.kill 'TERM', pid.to_i rescue nil }
2061 end
2062 end
2063
2064 def compare_git_uri a, b
2065 regex = %r{^(?:\w+://)?(?:[^@/]*@)?([^:/]*(?::[0-9]*)?)[:/](.*?)(?:\.git)?/?$}
2066 regex.match(a).to_a.drop(1) == regex.match(b).to_a.drop(1)
2067 end
2068
2069 require 'thread'
2070 require 'fileutils'
2071 require 'timeout'
2072 running = true
2073 iswin = VIM::evaluate('s:is_win').to_i == 1
2074 pull = VIM::evaluate('s:update.pull').to_i == 1
2075 base = VIM::evaluate('g:plug_home')
2076 all = VIM::evaluate('s:update.todo')
2077 limit = VIM::evaluate('get(g:, "plug_timeout", 60)')
2078 tries = VIM::evaluate('get(g:, "plug_retries", 2)') + 1
2079 nthr = VIM::evaluate('s:update.threads').to_i
2080 maxy = VIM::evaluate('winheight(".")').to_i
2081 vim7 = VIM::evaluate('v:version').to_i <= 703 && RUBY_PLATFORM =~ /darwin/
2082 cd = iswin ? 'cd /d' : 'cd'
2083 tot = VIM::evaluate('len(s:update.todo)') || 0
2084 bar = ''
2085 skip = 'Already installed'
2086 mtx = Mutex.new
2087 take1 = proc { mtx.synchronize { running && all.shift } }
2088 logh = proc {
2089 cnt = bar.length
2090 $curbuf[1] = "#{pull ? 'Updating' : 'Installing'} plugins (#{cnt}/#{tot})"
2091 $curbuf[2] = '[' + bar.ljust(tot) + ']'
2092 VIM::command('normal! 2G')
2093 VIM::command('redraw')
2094 }
2095 where = proc { |name| (1..($curbuf.length)).find { |l| $curbuf[l] =~ /^[-+x*] #{name}:/ } }
2096 log = proc { |name, result, type|
2097 mtx.synchronize do
2098 ing = ![true, false].include?(type)
2099 bar += type ? '=' : 'x' unless ing
2100 b = case type
2101 when :install then '+' when :update then '*'
2102 when true, nil then '-' else
2103 VIM::command("call add(s:update.errors, '#{name}')")
2104 'x'
2105 end
2106 result =
2107 if type || type.nil?
2108 ["#{b} #{name}: #{result.lines.to_a.last || 'OK'}"]
2109 elsif result =~ /^Interrupted|^Timeout/
2110 ["#{b} #{name}: #{result}"]
2111 else
2112 ["#{b} #{name}"] + result.lines.map { |l| " " << l }
2113 end
2114 if lnum = where.call(name)
2115 $curbuf.delete lnum
2116 lnum = 4 if ing && lnum > maxy
2117 end
2118 result.each_with_index do |line, offset|
2119 $curbuf.append((lnum || 4) - 1 + offset, line.gsub(/\e\[./, '').chomp)
2120 end
2121 logh.call
2122 end
2123 }
2124 bt = proc { |cmd, name, type, cleanup|
2125 tried = timeout = 0
2126 begin
2127 tried += 1
2128 timeout += limit
2129 fd = nil
2130 data = ''
2131 if iswin
2132 Timeout::timeout(timeout) do
2133 tmp = VIM::evaluate('tempname()')
2134 system("(#{cmd}) > #{tmp}")
2135 data = File.read(tmp).chomp
2136 File.unlink tmp rescue nil
2137 end
2138 else
2139 fd = IO.popen(cmd).extend(PlugStream)
2140 first_line = true
2141 log_prob = 1.0 / nthr
2142 while line = Timeout::timeout(timeout) { fd.get_line }
2143 data << line
2144 log.call name, line.chomp, type if name && (first_line || rand < log_prob)
2145 first_line = false
2146 end
2147 fd.close
2148 end
2149 [$? == 0, data.chomp]
2150 rescue Timeout::Error, Interrupt => e
2151 if fd && !fd.closed?
2152 killall fd.pid
2153 fd.close
2154 end
2155 cleanup.call if cleanup
2156 if e.is_a?(Timeout::Error) && tried < tries
2157 3.downto(1) do |countdown|
2158 s = countdown > 1 ? 's' : ''
2159 log.call name, "Timeout. Will retry in #{countdown} second#{s} ...", type
2160 sleep 1
2161 end
2162 log.call name, 'Retrying ...', type
2163 retry
2164 end
2165 [false, e.is_a?(Interrupt) ? "Interrupted!" : "Timeout!"]
2166 end
2167 }
2168 main = Thread.current
2169 threads = []
2170 watcher = Thread.new {
2171 if vim7
2172 while VIM::evaluate('getchar(1)')
2173 sleep 0.1
2174 end
2175 else
2176 require 'io/console' # >= Ruby 1.9
2177 nil until IO.console.getch == 3.chr
2178 end
2179 mtx.synchronize do
2180 running = false
2181 threads.each { |t| t.raise Interrupt } unless vim7
2182 end
2183 threads.each { |t| t.join rescue nil }
2184 main.kill
2185 }
2186 refresh = Thread.new {
2187 while true
2188 mtx.synchronize do
2189 break unless running
2190 VIM::command('noautocmd normal! a')
2191 end
2192 sleep 0.2
2193 end
2194 } if VIM::evaluate('s:mac_gui') == 1
2195
2196 clone_opt = VIM::evaluate('s:clone_opt').join(' ')
2197 progress = VIM::evaluate('s:progress_opt(1)')
2198 nthr.times do
2199 mtx.synchronize do
2200 threads << Thread.new {
2201 while pair = take1.call
2202 name = pair.first
2203 dir, uri, tag = pair.last.values_at *%w[dir uri tag]
2204 exists = File.directory? dir
2205 ok, result =
2206 if exists
2207 chdir = "#{cd} #{iswin ? dir : esc(dir)}"
2208 ret, data = bt.call "#{chdir} && git rev-parse --abbrev-ref HEAD 2>&1 && git config -f .git/config remote.origin.url", nil, nil, nil
2209 current_uri = data.lines.to_a.last
2210 if !ret
2211 if data =~ /^Interrupted|^Timeout/
2212 [false, data]
2213 else
2214 [false, [data.chomp, "PlugClean required."].join($/)]
2215 end
2216 elsif !compare_git_uri(current_uri, uri)
2217 [false, ["Invalid URI: #{current_uri}",
2218 "Expected: #{uri}",
2219 "PlugClean required."].join($/)]
2220 else
2221 if pull
2222 log.call name, 'Updating ...', :update
2223 fetch_opt = (tag && File.exist?(File.join(dir, '.git/shallow'))) ? '--depth 99999999' : ''
2224 bt.call "#{chdir} && git fetch #{fetch_opt} #{progress} 2>&1", name, :update, nil
2225 else
2226 [true, skip]
2227 end
2228 end
2229 else
2230 d = esc dir.sub(%r{[\\/]+$}, '')
2231 log.call name, 'Installing ...', :install
2232 bt.call "git clone #{clone_opt unless tag} #{progress} #{uri} #{d} 2>&1", name, :install, proc {
2233 FileUtils.rm_rf dir
2234 }
2235 end
2236 mtx.synchronize { VIM::command("let s:update.new['#{name}'] = 1") } if !exists && ok
2237 log.call name, result, ok
2238 end
2239 } if running
2240 end
2241 end
2242 threads.each { |t| t.join rescue nil }
2243 logh.call
2244 refresh.kill if refresh
2245 watcher.kill
2246 EOF
2247 endfunction
2248
2249 function! s:shellesc_cmd(arg, script)
2250 let escaped = substitute('"'.a:arg.'"', '[&|<>()@^!"]', '^&', 'g')
2251 return substitute(escaped, '%', (a:script ? '%' : '^') . '&', 'g')
2252 endfunction
2253
2254 function! s:shellesc_ps1(arg)
2255 return "'".substitute(escape(a:arg, '\"'), "'", "''", 'g')."'"
2256 endfunction
2257
2258 function! s:shellesc_sh(arg)
2259 return "'".substitute(a:arg, "'", "'\\\\''", 'g')."'"
2260 endfunction
2261
2262 " Escape the shell argument based on the shell.
2263 " Vim and Neovim's shellescape() are insufficient.
2264 " 1. shellslash determines whether to use single/double quotes.
2265 " Double-quote escaping is fragile for cmd.exe.
2266 " 2. It does not work for powershell.
2267 " 3. It does not work for *sh shells if the command is executed
2268 " via cmd.exe (ie. cmd.exe /c sh -c command command_args)
2269 " 4. It does not support batchfile syntax.
2270 "
2271 " Accepts an optional dictionary with the following keys:
2272 " - shell: same as Vim/Neovim 'shell' option.
2273 " If unset, fallback to 'cmd.exe' on Windows or 'sh'.
2274 " - script: If truthy and shell is cmd.exe, escape for batchfile syntax.
2275 function! plug#shellescape(arg, ...)
2276 if a:arg =~# '^[A-Za-z0-9_/:.-]\+$'
2277 return a:arg
2278 endif
2279 let opts = a:0 > 0 && type(a:1) == s:TYPE.dict ? a:1 : {}
2280 let shell = get(opts, 'shell', s:is_win ? 'cmd.exe' : 'sh')
2281 let script = get(opts, 'script', 1)
2282 if shell =~# 'cmd\(\.exe\)\?$'
2283 return s:shellesc_cmd(a:arg, script)
2284 elseif s:is_powershell(shell)
2285 return s:shellesc_ps1(a:arg)
2286 endif
2287 return s:shellesc_sh(a:arg)
2288 endfunction
2289
2290 function! s:glob_dir(path)
2291 return map(filter(s:glob(a:path, '**'), 'isdirectory(v:val)'), 's:dirpath(v:val)')
2292 endfunction
2293
2294 function! s:progress_bar(line, bar, total)
2295 call setline(a:line, '[' . s:lpad(a:bar, a:total) . ']')
2296 endfunction
2297
2298 function! s:compare_git_uri(a, b)
2299 " See `git help clone'
2300 " https:// [user@] github.com[:port] / junegunn/vim-plug [.git]
2301 " [git@] github.com[:port] : junegunn/vim-plug [.git]
2302 " file:// / junegunn/vim-plug [/]
2303 " / junegunn/vim-plug [/]
2304 let pat = '^\%(\w\+://\)\='.'\%([^@/]*@\)\='.'\([^:/]*\%(:[0-9]*\)\=\)'.'[:/]'.'\(.\{-}\)'.'\%(\.git\)\=/\?$'
2305 let ma = matchlist(a:a, pat)
2306 let mb = matchlist(a:b, pat)
2307 return ma[1:2] ==# mb[1:2]
2308 endfunction
2309
2310 function! s:format_message(bullet, name, message)
2311 if a:bullet != 'x'
2312 return [printf('%s %s: %s', a:bullet, a:name, s:lastline(a:message))]
2313 else
2314 let lines = map(s:lines(a:message), '" ".v:val')
2315 return extend([printf('x %s:', a:name)], lines)
2316 endif
2317 endfunction
2318
2319 function! s:with_cd(cmd, dir, ...)
2320 let script = a:0 > 0 ? a:1 : 1
2321 let pwsh = s:is_powershell(&shell)
2322 let cd = s:is_win && !pwsh ? 'cd /d' : 'cd'
2323 let sep = pwsh ? ';' : '&&'
2324 return printf('%s %s %s %s', cd, plug#shellescape(a:dir, {'script': script, 'shell': &shell}), sep, a:cmd)
2325 endfunction
2326
2327 function! s:system(cmd, ...)
2328 let batchfile = ''
2329 try
2330 let [sh, shellcmdflag, shrd] = s:chsh(1)
2331 if type(a:cmd) == s:TYPE.list
2332 " Neovim's system() supports list argument to bypass the shell
2333 " but it cannot set the working directory for the command.
2334 " Assume that the command does not rely on the shell.
2335 if has('nvim') && a:0 == 0
2336 return system(a:cmd)
2337 endif
2338 let cmd = join(map(copy(a:cmd), 'plug#shellescape(v:val, {"shell": &shell, "script": 0})'))
2339 if s:is_powershell(&shell)
2340 let cmd = '& ' . cmd
2341 endif
2342 else
2343 let cmd = a:cmd
2344 endif
2345 if a:0 > 0
2346 let cmd = s:with_cd(cmd, a:1, type(a:cmd) != s:TYPE.list)
2347 endif
2348 if s:is_win && type(a:cmd) != s:TYPE.list
2349 let [batchfile, cmd] = s:batchfile(cmd)
2350 endif
2351 return system(cmd)
2352 finally
2353 let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd]
2354 if s:is_win && filereadable(batchfile)
2355 call delete(batchfile)
2356 endif
2357 endtry
2358 endfunction
2359
2360 function! s:system_chomp(...)
2361 let ret = call('s:system', a:000)
2362 return v:shell_error ? '' : substitute(ret, '\n$', '', '')
2363 endfunction
2364
2365 function! s:git_validate(spec, check_branch)
2366 let err = ''
2367 if isdirectory(a:spec.dir)
2368 let result = [s:git_local_branch(a:spec.dir), s:git_origin_url(a:spec.dir)]
2369 let remote = result[-1]
2370 if empty(remote)
2371 let err = join([remote, 'PlugClean required.'], "\n")
2372 elseif !s:compare_git_uri(remote, a:spec.uri)
2373 let err = join(['Invalid URI: '.remote,
2374 \ 'Expected: '.a:spec.uri,
2375 \ 'PlugClean required.'], "\n")
2376 elseif a:check_branch && has_key(a:spec, 'commit')
2377 let sha = s:git_revision(a:spec.dir)
2378 if empty(sha)
2379 let err = join(add(result, 'PlugClean required.'), "\n")
2380 elseif !s:hash_match(sha, a:spec.commit)
2381 let err = join([printf('Invalid HEAD (expected: %s, actual: %s)',
2382 \ a:spec.commit[:6], sha[:6]),
2383 \ 'PlugUpdate required.'], "\n")
2384 endif
2385 elseif a:check_branch
2386 let current_branch = result[0]
2387 " Check tag
2388 let origin_branch = s:git_origin_branch(a:spec)
2389 if has_key(a:spec, 'tag')
2390 let tag = s:system_chomp('git describe --exact-match --tags HEAD 2>&1', a:spec.dir)
2391 if a:spec.tag !=# tag && a:spec.tag !~ '\*'
2392 let err = printf('Invalid tag: %s (expected: %s). Try PlugUpdate.',
2393 \ (empty(tag) ? 'N/A' : tag), a:spec.tag)
2394 endif
2395 " Check branch
2396 elseif origin_branch !=# current_branch
2397 let err = printf('Invalid branch: %s (expected: %s). Try PlugUpdate.',
2398 \ current_branch, origin_branch)
2399 endif
2400 if empty(err)
2401 let ahead_behind = split(s:lastline(s:system([
2402 \ 'git', 'rev-list', '--count', '--left-right',
2403 \ printf('HEAD...origin/%s', origin_branch)
2404 \ ], a:spec.dir)), '\t')
2405 if v:shell_error || len(ahead_behind) != 2
2406 let err = "Failed to compare with the origin. The default branch might have changed.\nPlugClean required."
2407 else
2408 let [ahead, behind] = ahead_behind
2409 if ahead && behind
2410 " Only mention PlugClean if diverged, otherwise it's likely to be
2411 " pushable (and probably not that messed up).
2412 let err = printf(
2413 \ "Diverged from origin/%s (%d commit(s) ahead and %d commit(s) behind!\n"
2414 \ .'Backup local changes and run PlugClean and PlugUpdate to reinstall it.', origin_branch, ahead, behind)
2415 elseif ahead
2416 let err = printf("Ahead of origin/%s by %d commit(s).\n"
2417 \ .'Cannot update until local changes are pushed.',
2418 \ origin_branch, ahead)
2419 endif
2420 endif
2421 endif
2422 endif
2423 else
2424 let err = 'Not found'
2425 endif
2426 return [err, err =~# 'PlugClean']
2427 endfunction
2428
2429 function! s:rm_rf(dir)
2430 if isdirectory(a:dir)
2431 return s:system(s:is_win
2432 \ ? 'rmdir /S /Q '.plug#shellescape(a:dir)
2433 \ : ['rm', '-rf', a:dir])
2434 endif
2435 endfunction
2436
2437 function! s:clean(force)
2438 call s:prepare()
2439 call append(0, 'Searching for invalid plugins in '.g:plug_home)
2440 call append(1, '')
2441
2442 " List of valid directories
2443 let dirs = []
2444 let errs = {}
2445 let [cnt, total] = [0, len(g:plugs)]
2446 for [name, spec] in items(g:plugs)
2447 if !s:is_managed(name) || get(spec, 'frozen', 0)
2448 call add(dirs, spec.dir)
2449 else
2450 let [err, clean] = s:git_validate(spec, 1)
2451 if clean
2452 let errs[spec.dir] = s:lines(err)[0]
2453 else
2454 call add(dirs, spec.dir)
2455 endif
2456 endif
2457 let cnt += 1
2458 call s:progress_bar(2, repeat('=', cnt), total)
2459 normal! 2G
2460 redraw
2461 endfor
2462
2463 let allowed = {}
2464 for dir in dirs
2465 let allowed[s:dirpath(s:plug_fnamemodify(dir, ':h:h'))] = 1
2466 let allowed[dir] = 1
2467 for child in s:glob_dir(dir)
2468 let allowed[child] = 1
2469 endfor
2470 endfor
2471
2472 let todo = []
2473 let found = sort(s:glob_dir(g:plug_home))
2474 while !empty(found)
2475 let f = remove(found, 0)
2476 if !has_key(allowed, f) && isdirectory(f)
2477 call add(todo, f)
2478 call append(line('$'), '- ' . f)
2479 if has_key(errs, f)
2480 call append(line('$'), ' ' . errs[f])
2481 endif
2482 let found = filter(found, 'stridx(v:val, f) != 0')
2483 end
2484 endwhile
2485
2486 4
2487 redraw
2488 if empty(todo)
2489 call append(line('$'), 'Already clean.')
2490 else
2491 let s:clean_count = 0
2492 call append(3, ['Directories to delete:', ''])
2493 redraw!
2494 if a:force || s:ask_no_interrupt('Delete all directories?')
2495 call s:delete([6, line('$')], 1)
2496 else
2497 call setline(4, 'Cancelled.')
2498 nnoremap <silent> <buffer> d :set opfunc=<sid>delete_op<cr>g@
2499 nmap <silent> <buffer> dd d_
2500 xnoremap <silent> <buffer> d :<c-u>call <sid>delete_op(visualmode(), 1)<cr>
2501 echo 'Delete the lines (d{motion}) to delete the corresponding directories'
2502 endif
2503 endif
2504 4
2505 setlocal nomodifiable
2506 endfunction
2507
2508 function! s:delete_op(type, ...)
2509 call s:delete(a:0 ? [line("'<"), line("'>")] : [line("'["), line("']")], 0)
2510 endfunction
2511
2512 function! s:delete(range, force)
2513 let [l1, l2] = a:range
2514 let force = a:force
2515 let err_count = 0
2516 while l1 <= l2
2517 let line = getline(l1)
2518 if line =~ '^- ' && isdirectory(line[2:])
2519 execute l1
2520 redraw!
2521 let answer = force ? 1 : s:ask('Delete '.line[2:].'?', 1)
2522 let force = force || answer > 1
2523 if answer
2524 let err = s:rm_rf(line[2:])
2525 setlocal modifiable
2526 if empty(err)
2527 call setline(l1, '~'.line[1:])
2528 let s:clean_count += 1
2529 else
2530 delete _
2531 call append(l1 - 1, s:format_message('x', line[1:], err))
2532 let l2 += len(s:lines(err))
2533 let err_count += 1
2534 endif
2535 let msg = printf('Removed %d directories.', s:clean_count)
2536 if err_count > 0
2537 let msg .= printf(' Failed to remove %d directories.', err_count)
2538 endif
2539 call setline(4, msg)
2540 setlocal nomodifiable
2541 endif
2542 endif
2543 let l1 += 1
2544 endwhile
2545 endfunction
2546
2547 function! s:upgrade()
2548 echo 'Downloading the latest version of vim-plug'
2549 redraw
2550 let tmp = s:plug_tempname()
2551 let new = tmp . '/plug.vim'
2552
2553 try
2554 let out = s:system(['git', 'clone', '--depth', '1', s:plug_src, tmp])
2555 if v:shell_error
2556 return s:err('Error upgrading vim-plug: '. out)
2557 endif
2558
2559 if readfile(s:me) ==# readfile(new)
2560 echo 'vim-plug is already up-to-date'
2561 return 0
2562 else
2563 call rename(s:me, s:me . '.old')
2564 call rename(new, s:me)
2565 unlet g:loaded_plug
2566 echo 'vim-plug has been upgraded'
2567 return 1
2568 endif
2569 finally
2570 silent! call s:rm_rf(tmp)
2571 endtry
2572 endfunction
2573
2574 function! s:upgrade_specs()
2575 for spec in values(g:plugs)
2576 let spec.frozen = get(spec, 'frozen', 0)
2577 endfor
2578 endfunction
2579
2580 function! s:status()
2581 call s:prepare()
2582 call append(0, 'Checking plugins')
2583 call append(1, '')
2584
2585 let ecnt = 0
2586 let unloaded = 0
2587 let [cnt, total] = [0, len(g:plugs)]
2588 for [name, spec] in items(g:plugs)
2589 let is_dir = isdirectory(spec.dir)
2590 if has_key(spec, 'uri')
2591 if is_dir
2592 let [err, _] = s:git_validate(spec, 1)
2593 let [valid, msg] = [empty(err), empty(err) ? 'OK' : err]
2594 else
2595 let [valid, msg] = [0, 'Not found. Try PlugInstall.']
2596 endif
2597 else
2598 if is_dir
2599 let [valid, msg] = [1, 'OK']
2600 else
2601 let [valid, msg] = [0, 'Not found.']
2602 endif
2603 endif
2604 let cnt += 1
2605 let ecnt += !valid
2606 " `s:loaded` entry can be missing if PlugUpgraded
2607 if is_dir && get(s:loaded, name, -1) == 0
2608 let unloaded = 1
2609 let msg .= ' (not loaded)'
2610 endif
2611 call s:progress_bar(2, repeat('=', cnt), total)
2612 call append(3, s:format_message(valid ? '-' : 'x', name, msg))
2613 normal! 2G
2614 redraw
2615 endfor
2616 call setline(1, 'Finished. '.ecnt.' error(s).')
2617 normal! gg
2618 setlocal nomodifiable
2619 if unloaded
2620 echo "Press 'L' on each line to load plugin, or 'U' to update"
2621 nnoremap <silent> <buffer> L :call <SID>status_load(line('.'))<cr>
2622 xnoremap <silent> <buffer> L :call <SID>status_load(line('.'))<cr>
2623 end
2624 endfunction
2625
2626 function! s:extract_name(str, prefix, suffix)
2627 return matchstr(a:str, '^'.a:prefix.' \zs[^:]\+\ze:.*'.a:suffix.'$')
2628 endfunction
2629
2630 function! s:status_load(lnum)
2631 let line = getline(a:lnum)
2632 let name = s:extract_name(line, '-', '(not loaded)')
2633 if !empty(name)
2634 call plug#load(name)
2635 setlocal modifiable
2636 call setline(a:lnum, substitute(line, ' (not loaded)$', '', ''))
2637 setlocal nomodifiable
2638 endif
2639 endfunction
2640
2641 function! s:status_update() range
2642 let lines = getline(a:firstline, a:lastline)
2643 let names = filter(map(lines, 's:extract_name(v:val, "[x-]", "")'), '!empty(v:val)')
2644 if !empty(names)
2645 echo
2646 execute 'PlugUpdate' join(names)
2647 endif
2648 endfunction
2649
2650 function! s:is_preview_window_open()
2651 silent! wincmd P
2652 if &previewwindow
2653 wincmd p
2654 return 1
2655 endif
2656 endfunction
2657
2658 function! s:find_name(lnum)
2659 for lnum in reverse(range(1, a:lnum))
2660 let line = getline(lnum)
2661 if empty(line)
2662 return ''
2663 endif
2664 let name = s:extract_name(line, '-', '')
2665 if !empty(name)
2666 return name
2667 endif
2668 endfor
2669 return ''
2670 endfunction
2671
2672 function! s:preview_commit()
2673 if b:plug_preview < 0
2674 let b:plug_preview = !s:is_preview_window_open()
2675 endif
2676
2677 let sha = matchstr(getline('.'), '^ \X*\zs[0-9a-f]\{7,9}')
2678 if empty(sha)
2679 let name = matchstr(getline('.'), '^- \zs[^:]*\ze:$')
2680 if empty(name)
2681 return
2682 endif
2683 let title = 'HEAD@{1}..'
2684 let command = 'git diff --no-color HEAD@{1}'
2685 else
2686 let title = sha
2687 let command = 'git show --no-color --pretty=medium '.sha
2688 let name = s:find_name(line('.'))
2689 endif
2690
2691 if empty(name) || !has_key(g:plugs, name) || !isdirectory(g:plugs[name].dir)
2692 return
2693 endif
2694
2695 if !s:is_preview_window_open()
2696 execute get(g:, 'plug_pwindow', 'vertical rightbelow new')
2697 execute 'e' title
2698 else
2699 execute 'pedit' title
2700 wincmd P
2701 endif
2702 setlocal previewwindow filetype=git buftype=nofile bufhidden=wipe nobuflisted modifiable
2703 let batchfile = ''
2704 try
2705 let [sh, shellcmdflag, shrd] = s:chsh(1)
2706 let cmd = 'cd '.plug#shellescape(g:plugs[name].dir).' && '.command
2707 if s:is_win
2708 let [batchfile, cmd] = s:batchfile(cmd)
2709 endif
2710 execute 'silent %!' cmd
2711 finally
2712 let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd]
2713 if s:is_win && filereadable(batchfile)
2714 call delete(batchfile)
2715 endif
2716 endtry
2717 setlocal nomodifiable
2718 nnoremap <silent> <buffer> q :q<cr>
2719 wincmd p
2720 endfunction
2721
2722 function! s:section(flags)
2723 call search('\(^[x-] \)\@<=[^:]\+:', a:flags)
2724 endfunction
2725
2726 function! s:format_git_log(line)
2727 let indent = ' '
2728 let tokens = split(a:line, nr2char(1))
2729 if len(tokens) != 5
2730 return indent.substitute(a:line, '\s*$', '', '')
2731 endif
2732 let [graph, sha, refs, subject, date] = tokens
2733 let tag = matchstr(refs, 'tag: [^,)]\+')
2734 let tag = empty(tag) ? ' ' : ' ('.tag.') '
2735 return printf('%s%s%s%s%s (%s)', indent, graph, sha, tag, subject, date)
2736 endfunction
2737
2738 function! s:append_ul(lnum, text)
2739 call append(a:lnum, ['', a:text, repeat('-', len(a:text))])
2740 endfunction
2741
2742 function! s:diff()
2743 call s:prepare()
2744 call append(0, ['Collecting changes ...', ''])
2745 let cnts = [0, 0]
2746 let bar = ''
2747 let total = filter(copy(g:plugs), 's:is_managed(v:key) && isdirectory(v:val.dir)')
2748 call s:progress_bar(2, bar, len(total))
2749 for origin in [1, 0]
2750 let plugs = reverse(sort(items(filter(copy(total), (origin ? '' : '!').'(has_key(v:val, "commit") || has_key(v:val, "tag"))'))))
2751 if empty(plugs)
2752 continue
2753 endif
2754 call s:append_ul(2, origin ? 'Pending updates:' : 'Last update:')
2755 for [k, v] in plugs
2756 let branch = s:git_origin_branch(v)
2757 if len(branch)
2758 let range = origin ? '..origin/'.branch : 'HEAD@{1}..'
2759 let cmd = ['git', 'log', '--graph', '--color=never']
2760 if s:git_version_requirement(2, 10, 0)
2761 call add(cmd, '--no-show-signature')
2762 endif
2763 call extend(cmd, ['--pretty=format:%x01%h%x01%d%x01%s%x01%cr', range])
2764 if has_key(v, 'rtp')
2765 call extend(cmd, ['--', v.rtp])
2766 endif
2767 let diff = s:system_chomp(cmd, v.dir)
2768 if !empty(diff)
2769 let ref = has_key(v, 'tag') ? (' (tag: '.v.tag.')') : has_key(v, 'commit') ? (' '.v.commit) : ''
2770 call append(5, extend(['', '- '.k.':'.ref], map(s:lines(diff), 's:format_git_log(v:val)')))
2771 let cnts[origin] += 1
2772 endif
2773 endif
2774 let bar .= '='
2775 call s:progress_bar(2, bar, len(total))
2776 normal! 2G
2777 redraw
2778 endfor
2779 if !cnts[origin]
2780 call append(5, ['', 'N/A'])
2781 endif
2782 endfor
2783 call setline(1, printf('%d plugin(s) updated.', cnts[0])
2784 \ . (cnts[1] ? printf(' %d plugin(s) have pending updates.', cnts[1]) : ''))
2785
2786 if cnts[0] || cnts[1]
2787 nnoremap <silent> <buffer> <plug>(plug-preview) :silent! call <SID>preview_commit()<cr>
2788 if empty(maparg("\<cr>", 'n'))
2789 nmap <buffer> <cr> <plug>(plug-preview)
2790 endif
2791 if empty(maparg('o', 'n'))
2792 nmap <buffer> o <plug>(plug-preview)
2793 endif
2794 endif
2795 if cnts[0]
2796 nnoremap <silent> <buffer> X :call <SID>revert()<cr>
2797 echo "Press 'X' on each block to revert the update"
2798 endif
2799 normal! gg
2800 setlocal nomodifiable
2801 endfunction
2802
2803 function! s:revert()
2804 if search('^Pending updates', 'bnW')
2805 return
2806 endif
2807
2808 let name = s:find_name(line('.'))
2809 if empty(name) || !has_key(g:plugs, name) ||
2810 \ input(printf('Revert the update of %s? (y/N) ', name)) !~? '^y'
2811 return
2812 endif
2813
2814 call s:system('git reset --hard HEAD@{1} && git checkout '.plug#shellescape(g:plugs[name].branch).' --', g:plugs[name].dir)
2815 setlocal modifiable
2816 normal! "_dap
2817 setlocal nomodifiable
2818 echo 'Reverted'
2819 endfunction
2820
2821 function! s:snapshot(force, ...) abort
2822 call s:prepare()
2823 setf vim
2824 call append(0, ['" Generated by vim-plug',
2825 \ '" '.strftime("%c"),
2826 \ '" :source this file in vim to restore the snapshot',
2827 \ '" or execute: vim -S snapshot.vim',
2828 \ '', '', 'PlugUpdate!'])
2829 1
2830 let anchor = line('$') - 3
2831 let names = sort(keys(filter(copy(g:plugs),
2832 \'has_key(v:val, "uri") && isdirectory(v:val.dir)')))
2833 for name in reverse(names)
2834 let sha = has_key(g:plugs[name], 'commit') ? g:plugs[name].commit : s:git_revision(g:plugs[name].dir)
2835 if !empty(sha)
2836 call append(anchor, printf("silent! let g:plugs['%s'].commit = '%s'", name, sha))
2837 redraw
2838 endif
2839 endfor
2840
2841 if a:0 > 0
2842 let fn = s:plug_expand(a:1)
2843 if filereadable(fn) && !(a:force || s:ask(a:1.' already exists. Overwrite?'))
2844 return
2845 endif
2846 call writefile(getline(1, '$'), fn)
2847 echo 'Saved as '.a:1
2848 silent execute 'e' s:esc(fn)
2849 setf vim
2850 endif
2851 endfunction
2852
2853 function! s:split_rtp()
2854 return split(&rtp, '\\\@<!,')
2855 endfunction
2856
2857 let s:first_rtp = s:escrtp(get(s:split_rtp(), 0, ''))
2858 let s:last_rtp = s:escrtp(get(s:split_rtp(), -1, ''))
2859
2860 if exists('g:plugs')
2861 let g:plugs_order = get(g:, 'plugs_order', keys(g:plugs))
2862 call s:upgrade_specs()
2863 call s:define_commands()
2864 endif
2865
2866 let &cpo = s:cpo_save
2867 unlet s:cpo_save