A minimalist Vim plugin manager.
@@ -92,7 +117,7 @@ Add a vim-plug section to your `~/.vimrc` (or `stdpath('config') . '/init.vim'`
1. `call plug#end()` to update `&runtimepath` and initialize plugin system
- Automatically executes `filetype plugin indent on` and `syntax enable`.
You can revert the settings after the call. e.g. `filetype indent off`, `syntax off`, etc.
-1. Reload .vimrc (`:source ~/.vimrc`) and `:PlugInstall` to install plugins.
+1. Reload .vimrc (`:source ~/.vimrc`) and `:PlugInstall` to install plugins.
#### Example
@@ -144,6 +169,53 @@ call plug#end()
" syntax off " Disable syntax highlighting
```
+#### Example (Lua configuration for Neovim)
+
+In Neovim, you can write your configuration in a Lua script file named
+`init.lua`. The following code is the Lua script equivalent to the VimScript
+example above.
+
+```lua
+local vim = vim
+local Plug = vim.fn['plug#']
+
+vim.call('plug#begin')
+
+-- Shorthand notation; fetches https://github.com/junegunn/vim-easy-align
+Plug('junegunn/vim-easy-align')
+
+-- Any valid git URL is allowed
+Plug('https://github.com/junegunn/vim-github-dashboard.git')
+
+-- Multiple Plug commands can be written in a single line using ; separators
+Plug('SirVer/ultisnips'); Plug('honza/vim-snippets')
+
+-- On-demand loading
+Plug('preservim/nerdtree', { ['on'] = 'NERDTreeToggle' })
+Plug('tpope/vim-fireplace', { ['for'] = 'clojure' })
+
+-- Using a non-default branch
+Plug('rdnetto/YCM-Generator', { ['branch'] = 'stable' })
+
+-- Using a tagged release; wildcard allowed (requires git 1.9.2 or above)
+Plug('fatih/vim-go', { ['tag'] = '*' })
+
+-- Plugin options
+Plug('nsf/gocode', { ['tag'] = 'v.20150303', ['rtp'] = 'vim' })
+
+-- Plugin outside ~/.vim/plugged with post-update hook
+Plug('junegunn/fzf', { ['dir'] = '~/.fzf', ['do'] = './install --all' })
+
+-- Unmanaged plugin (manually installed and updated)
+Plug('~/my-prototype-plugin')
+
+vim.call('plug#end')
+```
+
+More examples can be found in:
+
+* https://gitlab.com/sultanahamer/dotfiles/-/blob/master/nvim/lua/plugins.lua?ref_type=heads
+
### Commands
| Command | Description |
@@ -177,8 +249,8 @@ call plug#end()
| `g:plug_timeout` | 60 | Time limit of each task in seconds (*Ruby & Python*) |
| `g:plug_retries` | 2 | Number of retries in case of timeout (*Ruby & Python*) |
| `g:plug_shallow` | 1 | Use shallow clone |
-| `g:plug_window` | `vertical topleft new` | Command to open plug window |
-| `g:plug_pwindow` | `above 12new` | Command to open preview window in `PlugDiff` |
+| `g:plug_window` | `-tabnew` | Command to open plug window |
+| `g:plug_pwindow` | `vertical rightbelow new` | Command to open preview window in `PlugDiff` |
| `g:plug_url_format` | `https://git::@github.com/%s.git` | `printf` format to build repo URL (Only applies to the subsequent `Plug` commands) |
@@ -245,8 +317,14 @@ If the value starts with `:`, it will be recognized as a Vim command.
Plug 'fatih/vim-go', { 'do': ':GoInstallBinaries' }
```
+To call a Vim function, you can pass a lambda expression like so:
+
+```vim
+Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
+```
+
If you need more control, you can pass a reference to a Vim function that
-takes a single argument.
+takes a dictionary argument.
```vim
function! BuildYCM(info)
@@ -262,14 +340,13 @@ endfunction
Plug 'ycm-core/YouCompleteMe', { 'do': function('BuildYCM') }
```
-Both forms of post-update hook are executed inside the directory of the plugin
-and only run when the repository has changed, but you can force it to run
-unconditionally with the bang-versions of the commands: `PlugInstall!` and
-`PlugUpdate!`.
+A post-update hook is executed inside the directory of the plugin and only run
+when the repository has changed, but you can force it to run unconditionally
+with the bang-versions of the commands: `PlugInstall!` and `PlugUpdate!`.
-Make sure to escape BARs and double-quotes when you write the `do` option inline
-as they are mistakenly recognized as command separator or the start of the
-trailing comment.
+Make sure to escape BARs and double-quotes when you write the `do` option
+inline as they are mistakenly recognized as command separator or the start of
+the trailing comment.
```vim
Plug 'junegunn/fzf', { 'do': 'yes \| ./install' }
diff --git a/doc/plug.txt b/doc/plug.txt
index e4131c1..18ba7a5 100644
--- a/doc/plug.txt
+++ b/doc/plug.txt
@@ -1,4 +1,4 @@
-plug.txt plug Last change: January 19 2023
+plug.txt plug Last change: March 7 2024
PLUG - TABLE OF CONTENTS *plug* *plug-toc*
==============================================================================
@@ -230,8 +230,8 @@ Reload .vimrc and `:PlugInstall` to install plugins.
`g:plug_timeout` | 60 | Time limit of each task in seconds (Ruby & Python)
`g:plug_retries` | 2 | Number of retries in case of timeout (Ruby & Python)
`g:plug_shallow` | 1 | Use shallow clone
- `g:plug_window` | `vertical topleft new` | Command to open plug window
- `g:plug_pwindow` | `above 12new` | Command to open preview window in `PlugDiff`
+ `g:plug_window` | `-tabnew` | Command to open plug window
+ `g:plug_pwindow` | `vertical rightbelow new` | Command to open preview window in `PlugDiff`
`g:plug_url_format` | `https://git::@github.com/%s.git` | `printf` format to build repo URL (Only applies to the subsequent `Plug` commands)
--------------------+-----------------------------------+-----------------------------------------------------------------------------------
@@ -297,13 +297,15 @@ In that case, use the `do` option to describe the task to be performed.
Plug 'ycm-core/YouCompleteMe', { 'do': './install.py' }
<
If the value starts with `:`, it will be recognized as a Vim command.
-
- *:GoInstallBinaries*
>
Plug 'fatih/vim-go', { 'do': ':GoInstallBinaries' }
<
+To call a Vim function, you can pass a lambda expression like so:
+>
+ Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
+<
If you need more control, you can pass a reference to a Vim function that
-takes a single argument.
+takes a dictionary argument.
>
function! BuildYCM(info)
" info is a dictionary with 3 fields
@@ -317,10 +319,9 @@ takes a single argument.
Plug 'ycm-core/YouCompleteMe', { 'do': function('BuildYCM') }
<
-Both forms of post-update hook are executed inside the directory of the plugin
-and only run when the repository has changed, but you can force it to run
-unconditionally with the bang-versions of the commands: `PlugInstall!` and
-`PlugUpdate!`.
+A post-update hook is executed inside the directory of the plugin and only run
+when the repository has changed, but you can force it to run unconditionally
+with the bang-versions of the commands: `PlugInstall!` and `PlugUpdate!`.
Make sure to escape BARs and double-quotes when you write the `do` option
inline as they are mistakenly recognized as command separator or the start of
diff --git a/plug-dark.png b/plug-dark.png
new file mode 100644
index 0000000..457bcd6
Binary files /dev/null and b/plug-dark.png differ
diff --git a/plug.vim b/plug.vim
index 9c3011f..3badb4d 100644
--- a/plug.vim
+++ b/plug.vim
@@ -391,6 +391,9 @@ function! plug#end()
if !empty(types)
augroup filetypedetect
call s:source(s:rtp(plug), 'ftdetect/**/*.vim', 'after/ftdetect/**/*.vim')
+ if has('nvim-0.5.0')
+ call s:source(s:rtp(plug), 'ftdetect/**/*.lua', 'after/ftdetect/**/*.lua')
+ endif
augroup END
endif
for type in types
@@ -438,6 +441,9 @@ endfunction
function! s:load_plugin(spec)
call s:source(s:rtp(a:spec), 'plugin/**/*.vim', 'after/plugin/**/*.vim')
+ if has('nvim-0.5.0')
+ call s:source(s:rtp(a:spec), 'plugin/**/*.lua', 'after/plugin/**/*.lua')
+ endif
endfunction
function! s:reload_plugins()
@@ -655,6 +661,9 @@ function! s:lod(names, types, ...)
let rtp = s:rtp(g:plugs[name])
for dir in a:types
call s:source(rtp, dir.'/**/*.vim')
+ if has('nvim-0.5.0') " see neovim#14686
+ call s:source(rtp, dir.'/**/*.lua')
+ endif
endfor
if a:0
if !s:source(rtp, a:1) && !empty(s:glob(rtp, a:2))
@@ -869,7 +878,7 @@ function! s:lastline(msg)
endfunction
function! s:new_window()
- execute get(g:, 'plug_window', 'vertical topleft new')
+ execute get(g:, 'plug_window', '-tabnew')
endfunction
function! s:plug_window_exists()
@@ -1031,6 +1040,11 @@ function! s:is_updated(dir)
endfunction
function! s:do(pull, force, todo)
+ if has('nvim')
+ " Reset &rtp to invalidate Neovim cache of loaded Lua modules
+ " See https://github.com/junegunn/vim-plug/pull/1157#issuecomment-1809226110
+ let &rtp = &rtp
+ endif
for [name, spec] in items(a:todo)
if !isdirectory(spec.dir)
continue
@@ -1092,12 +1106,14 @@ endfunction
function! s:checkout(spec)
let sha = a:spec.commit
let output = s:git_revision(a:spec.dir)
+ let error = 0
if !empty(output) && !s:hash_match(sha, s:lines(output)[0])
let credential_helper = s:git_version_requirement(2) ? '-c credential.helper= ' : ''
let output = s:system(
\ 'git '.credential_helper.'fetch --depth 999999 && git checkout '.plug#shellescape(sha).' --', a:spec.dir)
+ let error = v:shell_error
endif
- return output
+ return [output, error]
endfunction
function! s:finish(pull)
@@ -1158,7 +1174,7 @@ function! s:update_impl(pull, force, args) abort
let threads = (len(args) > 0 && args[-1] =~ '^[1-9][0-9]*$') ?
\ remove(args, -1) : get(g:, 'plug_threads', 16)
- let managed = filter(copy(g:plugs), 's:is_managed(v:key)')
+ let managed = filter(deepcopy(g:plugs), 's:is_managed(v:key)')
let todo = empty(args) ? filter(managed, '!v:val.frozen || !isdirectory(v:val.dir)') :
\ filter(managed, 'index(args, v:key) >= 0')
@@ -1292,9 +1308,11 @@ function! s:update_finish()
if !pos
continue
endif
+ let out = ''
+ let error = 0
if has_key(spec, 'commit')
call s:log4(name, 'Checking out '.spec.commit)
- let out = s:checkout(spec)
+ let [out, error] = s:checkout(spec)
elseif has_key(spec, 'tag')
let tag = spec.tag
if tag =~ '\*'
@@ -1307,19 +1325,16 @@ function! s:update_finish()
endif
call s:log4(name, 'Checking out '.tag)
let out = s:system('git checkout -q '.plug#shellescape(tag).' -- 2>&1', spec.dir)
- else
- let branch = s:git_origin_branch(spec)
- call s:log4(name, 'Merging origin/'.s:esc(branch))
- let out = s:system('git checkout -q '.plug#shellescape(branch).' -- 2>&1'
- \. (has_key(s:update.new, name) ? '' : ('&& git merge --ff-only '.plug#shellescape('origin/'.branch).' 2>&1')), spec.dir)
+ let error = v:shell_error
endif
- if !v:shell_error && filereadable(spec.dir.'/.gitmodules') &&
+ if !error && filereadable(spec.dir.'/.gitmodules') &&
\ (s:update.force || has_key(s:update.new, name) || s:is_updated(spec.dir))
call s:log4(name, 'Updating submodules. This may take a while.')
let out .= s:bang('git submodule update --init --recursive'.s:submodule_opt.' 2>&1', spec.dir)
+ let error = v:shell_error
endif
let msg = s:format_message(v:shell_error ? 'x': '-', name, out)
- if v:shell_error
+ if error
call add(s:update.errors, name)
call s:regress_bar()
silent execute pos 'd _'
@@ -1382,7 +1397,9 @@ function! s:job_out_cb(self, data) abort
if !self.running || self.tick % len(s:jobs) == 0
let bullet = self.running ? (self.new ? '+' : '*') : (self.error ? 'x' : '-')
let result = self.error ? join(self.lines, "\n") : s:last_non_empty_line(self.lines)
- call s:log(bullet, self.name, result)
+ if len(result)
+ call s:log(bullet, self.name, result)
+ endif
endif
endfunction
@@ -1406,16 +1423,17 @@ function! s:nvim_cb(job_id, data, event) dict abort
\ s:job_cb('s:job_exit_cb', self, 0, a:data)
endfunction
-function! s:spawn(name, cmd, opts)
- let job = { 'name': a:name, 'running': 1, 'error': 0, 'lines': [''],
- \ 'new': get(a:opts, 'new', 0) }
+function! s:spawn(name, spec, queue, opts)
+ let job = { 'name': a:name, 'spec': a:spec, 'running': 1, 'error': 0, 'lines': [''],
+ \ 'new': get(a:opts, 'new', 0), 'queue': copy(a:queue) }
+ let Item = remove(job.queue, 0)
+ let argv = type(Item) == s:TYPE.funcref ? call(Item, [a:spec]) : Item
let s:jobs[a:name] = job
if s:nvim
if has_key(a:opts, 'dir')
let job.cwd = a:opts.dir
endif
- let argv = a:cmd
call extend(job, {
\ 'on_stdout': function('s:nvim_cb'),
\ 'on_stderr': function('s:nvim_cb'),
@@ -1431,7 +1449,7 @@ function! s:spawn(name, cmd, opts)
\ 'Invalid arguments (or job table is full)']
endif
elseif s:vim8
- let cmd = join(map(copy(a:cmd), 'plug#shellescape(v:val, {"script": 0})'))
+ let cmd = join(map(copy(argv), 'plug#shellescape(v:val, {"script": 0})'))
if has_key(a:opts, 'dir')
let cmd = s:with_cd(cmd, a:opts.dir, 0)
endif
@@ -1451,27 +1469,34 @@ function! s:spawn(name, cmd, opts)
let job.lines = ['Failed to start job']
endif
else
- let job.lines = s:lines(call('s:system', has_key(a:opts, 'dir') ? [a:cmd, a:opts.dir] : [a:cmd]))
+ let job.lines = s:lines(call('s:system', has_key(a:opts, 'dir') ? [argv, a:opts.dir] : [argv]))
let job.error = v:shell_error != 0
let job.running = 0
endif
endfunction
function! s:reap(name)
- let job = s:jobs[a:name]
+ let job = remove(s:jobs, a:name)
if job.error
call add(s:update.errors, a:name)
elseif get(job, 'new', 0)
let s:update.new[a:name] = 1
endif
- let s:update.bar .= job.error ? 'x' : '='
- let bullet = job.error ? 'x' : '-'
+ let more = len(get(job, 'queue', []))
+ let bullet = job.error ? 'x' : more ? (job.new ? '+' : '*') : '-'
let result = job.error ? join(job.lines, "\n") : s:last_non_empty_line(job.lines)
- call s:log(bullet, a:name, empty(result) ? 'OK' : result)
- call s:bar()
+ if len(result)
+ call s:log(bullet, a:name, result)
+ endif
- call remove(s:jobs, a:name)
+ if !job.error && more
+ let job.spec.queue = job.queue
+ let s:update.todo[a:name] = job.spec
+ else
+ let s:update.bar .= job.error ? 'x' : '='
+ call s:bar()
+ endif
endfunction
function! s:bar()
@@ -1524,6 +1549,16 @@ function! s:update_vim()
call s:tick()
endfunction
+function! s:checkout_command(spec)
+ let a:spec.branch = s:git_origin_branch(a:spec)
+ return ['git', 'checkout', '-q', a:spec.branch, '--']
+endfunction
+
+function! s:merge_command(spec)
+ let a:spec.branch = s:git_origin_branch(a:spec)
+ return ['git', 'merge', '--ff-only', 'origin/'.a:spec.branch]
+endfunction
+
function! s:tick()
let pull = s:update.pull
let prog = s:progress_opt(s:nvim || s:vim8)
@@ -1538,13 +1573,18 @@ while 1 " Without TCO, Vim stack is bound to explode
let name = keys(s:update.todo)[0]
let spec = remove(s:update.todo, name)
- let new = empty(globpath(spec.dir, '.git', 1))
+ let queue = get(spec, 'queue', [])
+ let new = empty(globpath(spec.dir, '.git', 1))
- call s:log(new ? '+' : '*', name, pull ? 'Updating ...' : 'Installing ...')
- redraw
+ if empty(queue)
+ call s:log(new ? '+' : '*', name, pull ? 'Updating ...' : 'Installing ...')
+ redraw
+ endif
let has_tag = has_key(spec, 'tag')
- if !new
+ if len(queue)
+ call s:spawn(name, spec, queue, { 'dir': spec.dir })
+ elseif !new
let [error, _] = s:git_validate(spec, 0)
if empty(error)
if pull
@@ -1555,7 +1595,11 @@ while 1 " Without TCO, Vim stack is bound to explode
if !empty(prog)
call add(cmd, prog)
endif
- call s:spawn(name, cmd, { 'dir': spec.dir })
+ let queue = [cmd, split('git remote set-head origin -a')]
+ if !has_tag && !has_key(spec, 'commit')
+ call extend(queue, [function('s:checkout_command'), function('s:merge_command')])
+ endif
+ call s:spawn(name, spec, queue, { 'dir': spec.dir })
else
let s:jobs[name] = { 'running': 0, 'lines': ['Already installed'], 'error': 0 }
endif
@@ -1570,7 +1614,7 @@ while 1 " Without TCO, Vim stack is bound to explode
if !empty(prog)
call add(cmd, prog)
endif
- call s:spawn(name, extend(cmd, [spec.uri, s:trim(spec.dir)]), { 'new': 1 })
+ call s:spawn(name, spec, [extend(cmd, [spec.uri, s:trim(spec.dir)]), function('s:checkout_command'), function('s:merge_command')], { 'new': 1 })
endif
if !s:jobs[name].running
@@ -2346,18 +2390,21 @@ function! s:git_validate(spec, check_branch)
\ current_branch, origin_branch)
endif
if empty(err)
- let [ahead, behind] = split(s:lastline(s:system([
- \ 'git', 'rev-list', '--count', '--left-right',
- \ printf('HEAD...origin/%s', origin_branch)
- \ ], a:spec.dir)), '\t')
- if !v:shell_error && ahead
- if behind
+ let ahead_behind = split(s:lastline(s:system([
+ \ 'git', 'rev-list', '--count', '--left-right',
+ \ printf('HEAD...origin/%s', origin_branch)
+ \ ], a:spec.dir)), '\t')
+ if v:shell_error || len(ahead_behind) != 2
+ let err = "Failed to compare with the origin. The default branch might have changed.\nPlugClean required."
+ else
+ let [ahead, behind] = ahead_behind
+ if ahead && behind
" Only mention PlugClean if diverged, otherwise it's likely to be
" pushable (and probably not that messed up).
let err = printf(
\ "Diverged from origin/%s (%d commit(s) ahead and %d commit(s) behind!\n"
\ .'Backup local changes and run PlugClean and PlugUpdate to reinstall it.', origin_branch, ahead, behind)
- else
+ elseif ahead
let err = printf("Ahead of origin/%s by %d commit(s).\n"
\ .'Cannot update until local changes are pushed.',
\ origin_branch, ahead)
@@ -2637,8 +2684,8 @@ function! s:preview_commit()
return
endif
- if exists('g:plug_pwindow') && !s:is_preview_window_open()
- execute g:plug_pwindow
+ if !s:is_preview_window_open()
+ execute get(g:, 'plug_pwindow', 'vertical rightbelow new')
execute 'e' title
else
execute 'pedit' title
diff --git a/test/workflow.vader b/test/workflow.vader
index 8f17315..83d075e 100644
--- a/test/workflow.vader
+++ b/test/workflow.vader
@@ -371,6 +371,9 @@ Execute (PlugDiff - 'No updates.'):
q
Execute (New commits on remote, PlugUpdate, then PlugDiff):
+ let g:plug_window = 'vertical topleft new'
+ let g:plug_pwindow = 'above 12new'
+
for repo in ['seoul256.vim', 'vim-emoji']
for _ in range(2)
call system(printf('cd /tmp/vim-plug-test/junegunn/%s && git commit --allow-empty -m "update"', repo))
@@ -458,6 +461,8 @@ Execute (New commits on remote, PlugUpdate, then PlugDiff):
AssertEqual 1, &previewwindow
pclose
+ unlet g:plug_window g:plug_pwindow
+
Execute (Test g:plug_pwindow):
let g:plug_pwindow = 'below 5new'
PlugDiff
@@ -983,7 +988,8 @@ Execute (PlugInstall!):
Assert filereadable(g:plugs['vim-easy-align'].dir.'/installed2'),
\ 'vim-easy-align/installed2 should exist'
AssertEqual '7f8cd78cb1fe52185b98b16a3749811f0cc508af', GitCommit('vim-pseudocl')
- AssertEqual 'no-t_co', GitBranch('seoul256.vim')
+ " Was updated to the default branch of origin by previous PlugUpdate
+ AssertEqual 'master', GitBranch('seoul256.vim')
AssertEqual '1.5.3', GitTag('goyo.vim')
Execute (When submodules are not initialized):
@@ -1708,6 +1714,8 @@ Execute (#530 - Comparison of incompatible git URIs):
Assert !CompareURI('https://github.com/junegunn/vim-plug.git', 'https://github.com:12345/junegunn/vim-plug.git')
Execute (#532 - Reuse plug window):
+ let g:plug_window = 'vertical topleft new'
+ let g:plug_pwindow = 'above 12new'
call plug#begin()
Plug 'junegunn/goyo.vim'
call plug#end()
@@ -1734,6 +1742,8 @@ Execute (#532 - Reuse plug window):
AssertEqual 2, winnr('$'), 'Three windows after PlugStatus (but got '.winnr('$').')'
q
+ unlet g:plug_window g:plug_pwindow
+
Execute (#766 - Allow cloning into an empty directory):
let d = '/tmp/vim-plug-test/goyo-already'
call system('rm -rf ' . d)