diff --git a/README.md b/README.md index bb1ff69..67f21a5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,30 @@ -vim-plug[![build](https://img.shields.io/github/actions/workflow/status/junegunn/vim-plug/test.yml?branch=master)](https://github.com/junegunn/vim-plug/actions/workflows/test.yml?query=branch%3Amaster) -=== +
+Special thanks to: +
+
+ +
+ Warp +
+ Warp is a modern, Rust-based terminal with AI built in so you and your team can build great software, faster. +
+ Visit warp.dev to learn more. +
+
+
+
+
+
+ +

+ + + vim-plug + + + + +

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)