pack.lua (51731B)
1 --- @brief 2 --- 3 --- Install, update, and delete external plugins. WARNING: It is still considered 4 --- experimental, yet should be stable enough for daily use. 5 --- 6 ---Manages plugins only in a dedicated [vim.pack-directory]() (see |packages|): 7 ---`$XDG_DATA_HOME/nvim/site/pack/core/opt`. `$XDG_DATA_HOME/nvim/site` needs to 8 ---be part of 'packpath'. It usually is, but might not be in cases like |--clean| or 9 ---setting |$XDG_DATA_HOME| during startup. 10 ---Plugin's subdirectory name matches plugin's name in specification. 11 ---It is assumed that all plugins in the directory are managed exclusively by `vim.pack`. 12 --- 13 ---Uses Git to manage plugins and requires present `git` executable. 14 ---Target plugins should be Git repositories with versions as named tags 15 ---following semver convention `v<major>.<minor>.<patch>`. 16 --- 17 ---The latest state of all managed plugins is stored inside a [vim.pack-lockfile]() 18 ---located at `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`. It is a JSON file that 19 ---is used to persistently track data about plugins. 20 ---For a more robust config treat lockfile like its part: put under version control, etc. 21 ---In this case all plugins from the lockfile will be installed at once and at lockfile's revision 22 ---(instead of inferring from `version`). This is done on the very first `vim.pack` function call 23 ---to ensure that lockfile is aligned with what is actually on the disk. 24 ---Lockfile should not be edited by hand. Corrupted data for installed plugins is repaired 25 ---(including after deleting whole file), but `version` fields will be missing 26 ---for not yet added plugins. 27 --- 28 ---[vim.pack-examples]() 29 --- 30 ---Basic install and management ~ 31 --- 32 ---- Add |vim.pack.add()| call(s) to 'init.lua': 33 ---```lua 34 --- 35 ---vim.pack.add({ 36 --- -- Install "plugin1" and use default branch (usually `main` or `master`) 37 --- 'https://github.com/user/plugin1', 38 --- 39 --- -- Same as above, but using a table (allows setting other options) 40 --- { src = 'https://github.com/user/plugin1' }, 41 --- 42 --- -- Specify plugin's name (here the plugin will be called "plugin2" 43 --- -- instead of "generic-name") 44 --- { src = 'https://github.com/user/generic-name', name = 'plugin2' }, 45 --- 46 --- -- Specify version to follow during install and update 47 --- { 48 --- src = 'https://github.com/user/plugin3', 49 --- -- Version constraint, see |vim.version.range()| 50 --- version = vim.version.range('1.0'), 51 --- }, 52 --- { 53 --- src = 'https://github.com/user/plugin4', 54 --- -- Git branch, tag, or commit hash 55 --- version = 'main', 56 --- }, 57 ---}) 58 --- 59 ----- Plugin's code can be used directly after `add()` 60 ---plugin1 = require('plugin1') 61 ---``` 62 --- 63 ---- Restart Nvim (for example, with |:restart|). Plugins that were not yet 64 ---installed will be available on disk after `add()` call. Their revision is 65 ---taken from |vim.pack-lockfile| (if present) or inferred from the `version`. 66 --- 67 ---- To update all plugins with new changes: 68 --- - Execute |vim.pack.update()|. This will download updates from source and 69 --- show confirmation buffer in a separate tabpage. 70 --- - Review changes. To confirm all updates execute |:write|. 71 --- To discard updates execute |:quit|. 72 --- - (Optionally) |:restart| to start using code from updated plugins. 73 --- 74 ---Use shorter source ~ 75 --- 76 --- Create custom Lua helpers: 77 --- 78 ---```lua 79 --- 80 ---local gh = function(x) return 'https://github.com/' .. x end 81 ---local cb = function(x) return 'https://codeberg.org/' .. x end 82 ---vim.pack.add({ gh('user/plugin1'), cb('user/plugin2') }) 83 ---``` 84 --- 85 ---Another approach is to utilize Git's `insteadOf` configuration: 86 ---- `git config --global url."https://github.com/".insteadOf "gh:"` 87 ---- `git config --global url."https://codeberg.org/".insteadOf "cb:"` 88 ---- In 'init.lua': `vim.pack.add({ 'gh:user/plugin1', 'cb:user/plugin2' })`. 89 --- These sources will be used verbatim in |vim.pack-lockfile|, so reusing 90 --- the config on different machine will require the same Git configuration. 91 --- 92 ---Explore installed plugins ~ 93 --- 94 ---- `vim.pack.update(nil, { offline = true })` 95 ---- Navigate between plugins with `[[` and `]]`. List them with `gO` 96 --- (|vim.lsp.buf.document_symbol()|). 97 --- 98 ---Switch plugin's version and/or source ~ 99 --- 100 ---- Update 'init.lua' for plugin to have desired `version` and/or `src`. 101 --- Let's say, the switch is for plugin named 'plugin1'. 102 ---- |:restart|. The plugin's state on disk (revision and/or tracked source) 103 --- is not yet changed. Only plugin's `version` in |vim.pack-lockfile| is updated. 104 ---- Execute `vim.pack.update({ 'plugin1' })`. The plugin's source is updated. 105 --- If only switching version, use `{ offline = true }` option table. 106 ---- Review changes and either confirm or discard them. If discarded, revert 107 --- `version` change in 'init.lua' as well or you will be prompted again next time 108 --- you run |vim.pack.update()|. 109 --- 110 ---Freeze plugin from being updated ~ 111 --- 112 ---- Update 'init.lua' for plugin to have `version` set to current revision. 113 ---Get it from |vim.pack-lockfile| (plugin's field `rev`; looks like `abc12345`). 114 ---- |:restart|. 115 --- 116 ---Unfreeze plugin to start receiving updates ~ 117 --- 118 ---- Update 'init.lua' for plugin to have `version` set to whichever version 119 ---you want it to be updated. 120 ---- |:restart|. 121 --- 122 ---Revert plugin after an update ~ 123 --- 124 ---- Revert the |vim.pack-lockfile| to the state before the update: 125 --- - If Git tracked: `git checkout HEAD -- nvim-pack-lock.json` 126 --- - If not tracked: examine log file ("nvim-pack.log" at "log" |stdpath()|), 127 --- locate the revisions before the latest update, and (carefully) adjust 128 --- current lockfile to have those revisions. 129 ---- |:restart|. 130 ---- `vim.pack.update({ 'plugin' }, { offline = true, target = 'lockfile' })`. 131 --- Read and confirm. 132 --- 133 ---Synchronize config across machines ~ 134 --- 135 ---- On main machine: 136 --- - Add |vim.pack-lockfile| to VCS. 137 --- - Push to the remote server. 138 ---- On secondary machine: 139 --- - Pull from the server. 140 --- - |:restart|. New plugins (not present locally, but present in the lockfile) 141 --- are installed at proper revision. 142 --- - `vim.pack.update(nil, { target = 'lockfile' })`. Read and confirm. 143 --- - Manually delete outdated plugins (present locally, but were not present 144 --- in the lockfile prior to restart) with `vim.pack.del( { 'plugin' })`. 145 --- They can be located by examining the VCS difference of the lockfile 146 --- (`git diff -- nvim-pack-lock.json` for Git). 147 --- 148 ---Remove plugins from disk ~ 149 --- 150 ---- Remove plugin specs from |vim.pack.add()| calls in 'init.lua' or they will be 151 --- reinstalled later. 152 ---- |:restart|. 153 ---- Use |vim.pack.del()| with a list of plugin names to remove. Use |vim.pack.get()| 154 --- to get all non-active plugins: 155 ---```lua 156 ---vim.iter(vim.pack.get()) 157 --- :filter(function(x) return not x.active end) 158 --- :map(function(x) return x.spec.name end) 159 --- :totable() 160 ---``` 161 --- 162 ---[vim.pack-events]() 163 --- 164 ---Performing actions via `vim.pack` functions can trigger these events: 165 ---- [PackChangedPre]() - before trying to change plugin's state. 166 ---- [PackChanged]() - after plugin's state has changed. 167 --- 168 ---Each event populates the following |event-data| fields: 169 ---- `active` - whether plugin was added via |vim.pack.add()| to current session. 170 ---- `kind` - one of "install" (install on disk; before loading), 171 --- "update" (update already installed plugin; might be not loaded), 172 --- "delete" (delete from disk). 173 ---- `spec` - plugin's specification with defaults made explicit. 174 ---- `path` - full path to plugin's directory. 175 --- 176 --- These events can be used to execute plugin hooks. For example: 177 ---```lua 178 ---local hooks = function(ev) 179 --- -- Use available |event-data| 180 --- local name, kind = ev.data.spec.name, ev.data.kind 181 --- 182 --- -- Run build script after plugin's code has changed 183 --- if name == 'plug-1' and (kind == 'install' or kind == 'update') then 184 --- -- Append `:wait()` if you need synchronous execution 185 --- vim.system({ 'make' }, { cwd = ev.data.path }) 186 --- end 187 --- 188 --- -- If action relies on code from the plugin (like user command or 189 --- -- Lua code), make sure to explicitly load it first 190 --- if name == 'plug-2' and kind == 'update' then 191 --- if not ev.data.active then 192 --- vim.cmd.packadd('plug-2') 193 --- end 194 --- vim.cmd('PlugTwoUpdate') 195 --- require('plug2').after_update() 196 --- end 197 ---end 198 --- 199 ----- If hooks need to run on install, run this before `vim.pack.add()` 200 ----- To act on install from lockfile, run before very first `vim.pack.add()` 201 ---vim.api.nvim_create_autocmd('PackChanged', { callback = hooks }) 202 ---``` 203 204 local api = vim.api 205 local uv = vim.uv 206 local async = require('vim._async') 207 208 local M = {} 209 210 --- @class (private) vim.pack.LockData 211 --- @field rev string Latest recorded revision. 212 --- @field src string Plugin source. 213 --- @field version? string|vim.VersionRange Plugin `version`, as supplied in `spec`. 214 215 --- @class (private) vim.pack.Lock 216 --- @field plugins table<string, vim.pack.LockData> Map from plugin name to its lock data. 217 218 --- @type vim.pack.Lock 219 local plugin_lock 220 221 --- @return string 222 local function get_plug_dir() 223 return vim.fs.joinpath(vim.fn.stdpath('data'), 'site', 'pack', 'core', 'opt') 224 end 225 226 local function lock_get_path() 227 return vim.fs.joinpath(vim.fn.stdpath('config'), 'nvim-pack-lock.json') 228 end 229 230 -- Git ------------------------------------------------------------------------ 231 232 --- @async 233 --- @param cmd string[] 234 --- @param cwd? string 235 --- @return string 236 local function git_cmd(cmd, cwd) 237 -- Use '-c gc.auto=0' to disable `stderr` "Auto packing..." messages 238 cmd = vim.list_extend({ 'git', '-c', 'gc.auto=0' }, cmd) 239 local env = vim.fn.environ() --- @type table<string,string> 240 env.GIT_DIR, env.GIT_WORK_TREE = nil, nil 241 local sys_opts = { cwd = cwd, text = true, env = env, clear_env = true } 242 local out = async.await(3, vim.system, cmd, sys_opts) --- @type vim.SystemCompleted 243 async.await(1, vim.schedule) 244 if out.code ~= 0 then 245 error(out.stderr) 246 end 247 local stdout, stderr = assert(out.stdout), assert(out.stderr) 248 if stderr ~= '' then 249 vim.schedule(function() 250 vim.notify(stderr:gsub('\n+$', ''), vim.log.levels.WARN) 251 end) 252 end 253 return (stdout:gsub('\n+$', '')) 254 end 255 256 local git_version = vim.version.parse('1') 257 258 local function git_ensure_exec() 259 local ok, sys = pcall(vim.system, { 'git', 'version' }) 260 if not ok then 261 error('No `git` executable') 262 end 263 git_version = vim.version.parse(sys:wait().stdout) 264 end 265 266 --- @async 267 --- @param url string 268 --- @param path string 269 local function git_clone(url, path) 270 local cmd = { 'clone', '--quiet', '--no-checkout' } 271 272 if vim.startswith(url, 'file://') then 273 cmd[#cmd + 1] = '--no-hardlinks' 274 elseif git_version >= vim.version.parse('2.27') then 275 cmd[#cmd + 1] = '--filter=blob:none' 276 end 277 278 vim.list_extend(cmd, { '--origin', 'origin', url, path }) 279 git_cmd(cmd, uv.cwd()) 280 end 281 282 --- @async 283 --- @param ref string 284 --- @param cwd string 285 --- @return string 286 local function git_get_hash(ref, cwd) 287 -- Using `rev-list -1` shows a commit of reference, while `rev-parse` shows 288 -- hash of reference. Those are different for annotated tags. 289 return git_cmd({ 'rev-list', '-1', ref }, cwd) 290 end 291 292 --- @async 293 --- @param cwd string 294 --- @return string 295 local function git_get_default_branch(cwd) 296 local res = git_cmd({ 'rev-parse', '--abbrev-ref', 'origin/HEAD' }, cwd) 297 return (res:gsub('^origin/', '')) 298 end 299 300 --- @async 301 --- @param cwd string 302 --- @return string[] 303 local function git_get_branches(cwd) 304 local def_branch = git_get_default_branch(cwd) 305 local cmd = { 'branch', '--remote', '--list', '--format=%(refname:short)', '--', 'origin/**' } 306 local stdout = git_cmd(cmd, cwd) 307 local res = {} --- @type string[] 308 for l in vim.gsplit(stdout, '\n') do 309 local branch = l:match('^origin/(.+)$') 310 local pos = branch == def_branch and 1 or (#res + 1) 311 table.insert(res, pos, branch) 312 end 313 return res 314 end 315 316 --- @async 317 --- @param cwd string 318 --- @return string[] 319 local function git_get_tags(cwd) 320 local tags = git_cmd({ 'tag', '--list', '--sort=-v:refname' }, cwd) 321 return tags == '' and {} or vim.split(tags, '\n') 322 end 323 324 -- Plugin operations ---------------------------------------------------------- 325 326 --- @param msg string|string[] 327 --- @param level ('DEBUG'|'TRACE'|'INFO'|'WARN'|'ERROR')? 328 local function notify(msg, level) 329 msg = type(msg) == 'table' and table.concat(msg, '\n') or msg 330 vim.notify('vim.pack: ' .. msg, vim.log.levels[level or 'INFO']) 331 vim.cmd.redraw() 332 end 333 334 --- @param x string|vim.VersionRange 335 --- @return boolean 336 local function is_version(x) 337 return type(x) == 'string' or (type(x) == 'table' and pcall(x.has, x, '1')) 338 end 339 340 --- @param x string 341 --- @return boolean 342 local function is_semver(x) 343 return vim.version.parse(x) ~= nil 344 end 345 346 local function is_nonempty_string(x) 347 return type(x) == 'string' and x ~= '' 348 end 349 350 --- @return string 351 local function get_timestamp() 352 return vim.fn.strftime('%Y-%m-%d %H:%M:%S') 353 end 354 355 --- @class vim.pack.Spec 356 --- 357 --- URI from which to install and pull updates. Any format supported by `git clone` is allowed. 358 --- @field src string 359 --- 360 --- Name of plugin. Will be used as directory name. Default: `src` repository name. 361 --- @field name? string 362 --- 363 --- Version to use for install and updates. Can be: 364 --- - `nil` (no value, default) to use repository's default branch (usually `main` or `master`). 365 --- - String to use specific branch, tag, or commit hash. 366 --- - Output of |vim.version.range()| to install the greatest/last semver tag 367 --- inside the version constraint. 368 --- @field version? string|vim.VersionRange 369 --- 370 --- @field data? any Arbitrary data associated with a plugin. 371 372 --- @alias vim.pack.SpecResolved { src: string, name: string, version: nil|string|vim.VersionRange, data: any|nil } 373 374 --- @param spec string|vim.pack.Spec 375 --- @return vim.pack.SpecResolved 376 local function normalize_spec(spec) 377 spec = type(spec) == 'string' and { src = spec } or spec 378 vim.validate('spec', spec, 'table') 379 vim.validate('spec.src', spec.src, is_nonempty_string, false, 'non-empty string') 380 local name = spec.name or spec.src:gsub('%.git$', '') 381 name = (type(name) == 'string' and name or ''):match('[^/]+$') or '' 382 vim.validate('spec.name', name, is_nonempty_string, true, 'non-empty string') 383 vim.validate('spec.version', spec.version, is_version, true, 'string or vim.VersionRange') 384 return { src = spec.src, name = name, version = spec.version, data = spec.data } 385 end 386 387 --- @class (private) vim.pack.PlugInfo 388 --- @field err string The latest error when working on plugin. If non-empty, 389 --- all further actions should not be done (including triggering events). 390 --- @field installed? boolean Whether plugin was successfully installed. 391 --- @field version_str? string `spec.version` with resolved version range. 392 --- @field version_ref? string Resolved version as Git reference (if different 393 --- from `version_str`). 394 --- @field sha_head? string Git hash of HEAD. 395 --- @field sha_target? string Git hash of `version_ref`. 396 --- @field update_details? string Details about the update:: changelog if HEAD 397 --- and target are different, available newer tags otherwise. 398 399 --- @class (private) vim.pack.Plug 400 --- @field spec vim.pack.SpecResolved 401 --- @field path string 402 --- @field info vim.pack.PlugInfo Gathered information about plugin. 403 404 --- @param spec string|vim.pack.Spec 405 --- @param plug_dir string? 406 --- @return vim.pack.Plug 407 local function new_plug(spec, plug_dir) 408 local spec_resolved = normalize_spec(spec) 409 local path = vim.fs.joinpath(plug_dir or get_plug_dir(), spec_resolved.name) 410 local info = { err = '', installed = plugin_lock.plugins[spec_resolved.name] ~= nil } 411 return { spec = spec_resolved, path = path, info = info } 412 end 413 414 --- Normalize plug array: gather non-conflicting data from duplicated entries. 415 --- @param plugs vim.pack.Plug[] 416 --- @return vim.pack.Plug[] 417 local function normalize_plugs(plugs) 418 --- @type table<string, { plug: vim.pack.Plug, id: integer }> 419 local plug_map = {} 420 local n = 0 421 for _, p in ipairs(plugs) do 422 -- Collect 423 if not plug_map[p.path] then 424 n = n + 1 425 plug_map[p.path] = { plug = p, id = n } 426 end 427 local p_data = plug_map[p.path] 428 -- TODO(echasnovski): if both versions are `vim.VersionRange`, collect as 429 -- their intersection. Needs `vim.version.intersect`. 430 p_data.plug.spec.version = vim.F.if_nil(p_data.plug.spec.version, p.spec.version) 431 432 -- Ensure no conflicts 433 local spec_ref = p_data.plug.spec 434 local spec = p.spec 435 if spec_ref.src ~= spec.src then 436 local src_1 = tostring(spec_ref.src) 437 local src_2 = tostring(spec.src) 438 error(('Conflicting `src` for `%s`:\n%s\n%s'):format(spec.name, src_1, src_2)) 439 end 440 if spec_ref.version ~= spec.version then 441 local ver_1 = tostring(spec_ref.version) 442 local ver_2 = tostring(spec.version) 443 error(('Conflicting `version` for `%s`:\n%s\n%s'):format(spec.name, ver_1, ver_2)) 444 end 445 end 446 447 --- @type vim.pack.Plug[] 448 local res = {} 449 for _, p_data in pairs(plug_map) do 450 res[p_data.id] = p_data.plug 451 end 452 assert(#res == n) 453 return res 454 end 455 456 --- @param names? string[] 457 --- @return vim.pack.Plug[] 458 local function plug_list_from_names(names) 459 local p_data_list = M.get(names, { info = false }) 460 local plug_dir = get_plug_dir() 461 local plugs = {} --- @type vim.pack.Plug[] 462 for _, p_data in ipairs(p_data_list) do 463 plugs[#plugs + 1] = new_plug(p_data.spec, plug_dir) 464 end 465 return plugs 466 end 467 468 --- Map from plugin path to its data. 469 --- Use map and not array to avoid linear lookup during startup. 470 --- @type table<string, { plug: vim.pack.Plug, id: integer }?> 471 local active_plugins = {} 472 local n_active_plugins = 0 473 474 --- @param p vim.pack.Plug 475 --- @param event_name 'PackChangedPre'|'PackChanged' 476 --- @param kind 'install'|'update'|'delete' 477 local function trigger_event(p, event_name, kind) 478 local active = active_plugins[p.path] ~= nil 479 local data = { active = active, kind = kind, spec = vim.deepcopy(p.spec), path = p.path } 480 api.nvim_exec_autocmds(event_name, { pattern = p.path, data = data }) 481 end 482 483 --- @param action string 484 --- @return fun(kind: 'begin'|'report'|'end', percent: integer, fmt: string, ...:any): nil 485 local function new_progress_report(action) 486 local progress = { kind = 'progress', title = 'vim.pack' } 487 local headless = #api.nvim_list_uis() == 0 488 489 return vim.schedule_wrap(function(kind, percent, fmt, ...) 490 progress.status = kind == 'end' and 'success' or 'running' 491 progress.percent = percent 492 local msg = ('%s %s'):format(action, fmt:format(...)) 493 progress.id = api.nvim_echo({ { msg } }, kind ~= 'report', progress) 494 -- Force redraw to show installation progress during startup 495 -- TODO: redraw! not needed with ui2. 496 if not headless then 497 vim.cmd.redraw({ bang = true }) 498 end 499 end) 500 end 501 502 local n_threads = 2 * #(uv.cpu_info() or { {} }) 503 local copcall = package.loaded.jit and pcall or require('coxpcall').pcall 504 505 --- Execute function in parallel for each non-errored plugin in the list 506 --- @param plug_list vim.pack.Plug[] 507 --- @param f async fun(p: vim.pack.Plug) 508 --- @param progress_action string 509 local function run_list(plug_list, f, progress_action) 510 local report_progress = new_progress_report(progress_action) 511 512 -- Construct array of functions to execute in parallel 513 local n_finished = 0 514 local funs = {} --- @type (async fun())[] 515 for _, p in ipairs(plug_list) do 516 -- Run only for plugins which didn't error before 517 if p.info.err == '' then 518 --- @async 519 funs[#funs + 1] = function() 520 local ok, err = copcall(f, p) --[[@as string]] 521 if not ok then 522 p.info.err = err --- @as string 523 end 524 525 -- Show progress 526 n_finished = n_finished + 1 527 local percent = math.floor(100 * n_finished / #funs) 528 report_progress('report', percent, '(%d/%d) - %s', n_finished, #funs, p.spec.name) 529 end 530 end 531 end 532 533 if #funs == 0 then 534 return 535 end 536 537 -- Run async in parallel but wait for all to finish/timeout 538 report_progress('begin', 0, '(0/%d)', #funs) 539 540 --- @async 541 local function joined_f() 542 async.join(n_threads, funs) 543 end 544 async.run(joined_f):wait() 545 546 report_progress('end', 100, '(%d/%d)', #funs, #funs) 547 end 548 549 local confirm_all = false 550 551 --- @param plug_list vim.pack.Plug[] 552 --- @return boolean 553 local function confirm_install(plug_list) 554 if confirm_all then 555 return true 556 end 557 558 -- Gather pretty aligned list of plugins to install 559 local name_width, name_max_width = {}, 0 --- @type integer[], integer 560 for i, p in ipairs(plug_list) do 561 name_width[i] = api.nvim_strwidth(p.spec.name) 562 name_max_width = math.max(name_max_width, name_width[i]) 563 end 564 local lines = {} --- @type string[] 565 for i, p in ipairs(plug_list) do 566 local pad = (' '):rep(name_max_width - name_width[i] + 1) 567 lines[i] = ('%s%sfrom %s'):format(p.spec.name, pad, p.spec.src) 568 end 569 570 local text = table.concat(lines, '\n') 571 local confirm_msg = ('These plugins will be installed:\n\n%s\n'):format(text) 572 local choice = vim.fn.confirm(confirm_msg, 'Proceed? &Yes\n&No\n&Always', 1, 'Question') 573 confirm_all = choice == 3 574 vim.cmd.redraw() 575 return choice ~= 2 576 end 577 578 --- @param tags string[] 579 --- @param version_range vim.VersionRange 580 local function get_last_semver_tag(tags, version_range) 581 local last_tag, last_ver_tag --- @type string, vim.Version 582 for _, tag in ipairs(tags) do 583 local ver_tag = vim.version.parse(tag) 584 if ver_tag then 585 if version_range:has(ver_tag) and (not last_ver_tag or ver_tag > last_ver_tag) then 586 last_tag, last_ver_tag = tag, ver_tag 587 end 588 end 589 end 590 return last_tag 591 end 592 593 --- @async 594 --- @param p vim.pack.Plug 595 local function resolve_version(p) 596 local function list_in_line(name, list) 597 return ('\n%s: %s'):format(name, table.concat(list, ', ')) 598 end 599 600 -- Resolve only once 601 if p.info.version_str then 602 return 603 end 604 local version = p.spec.version 605 606 -- Default branch 607 if not version then 608 p.info.version_str = git_get_default_branch(p.path) 609 p.info.version_ref = 'origin/' .. p.info.version_str 610 return 611 end 612 613 -- Non-version-range like version: branch, tag, or commit hash 614 local branches = git_get_branches(p.path) 615 local tags = git_get_tags(p.path) 616 if type(version) == 'string' then 617 local is_branch = vim.tbl_contains(branches, version) 618 local is_tag_or_hash = copcall(git_get_hash, version, p.path) 619 if not (is_branch or is_tag_or_hash) then 620 local err = ('`%s` is not a branch/tag/commit. Available:'):format(version) 621 .. list_in_line('Tags', tags) 622 .. list_in_line('Branches', branches) 623 error(err) 624 end 625 626 p.info.version_str = version 627 p.info.version_ref = (is_branch and 'origin/' or '') .. version 628 return 629 end 630 --- @cast version vim.VersionRange 631 632 -- Choose the greatest/last version among all matching semver tags 633 p.info.version_str = get_last_semver_tag(tags, version) 634 if p.info.version_str == nil then 635 local semver_tags = vim.tbl_filter(is_semver, tags) 636 table.sort(semver_tags, vim.version.gt) 637 local err = 'No versions fit constraint. Relax it or switch to branch. Available:' 638 .. list_in_line('Versions', semver_tags) 639 .. list_in_line('Branches', branches) 640 error(err) 641 end 642 end 643 644 --- @async 645 --- @param p vim.pack.Plug 646 local function infer_revisions(p) 647 p.info.sha_head = p.info.sha_head or git_get_hash('HEAD', p.path) 648 649 resolve_version(p) 650 local target_ref = p.info.version_ref or p.info.version_str --[[@as string]] 651 p.info.sha_target = p.info.sha_target or git_get_hash(target_ref, p.path) 652 end 653 654 --- Keep repos in detached HEAD state. Infer commit from resolved version. 655 --- No local branches are created, branches from "origin" remote are used directly. 656 --- @async 657 --- @param p vim.pack.Plug 658 --- @param timestamp string 659 --- @param skip_stash? boolean 660 local function checkout(p, timestamp, skip_stash) 661 infer_revisions(p) 662 663 if not skip_stash then 664 local stash_cmd = { 'stash', '--quiet' } 665 if git_version > vim.version.parse('2.13') then 666 stash_cmd[#stash_cmd + 1] = '--message' 667 stash_cmd[#stash_cmd + 1] = ('vim.pack: %s Stash before checkout'):format(timestamp) 668 end 669 git_cmd(stash_cmd, p.path) 670 end 671 672 git_cmd({ 'checkout', '--quiet', p.info.sha_target }, p.path) 673 674 local submodule_cmd = { 'submodule', 'update', '--init', '--recursive' } 675 if git_version >= vim.version.parse('2.36') then 676 submodule_cmd[#submodule_cmd + 1] = '--filter=blob:none' 677 end 678 git_cmd(submodule_cmd, p.path) 679 680 plugin_lock.plugins[p.spec.name].rev = p.info.sha_target 681 682 -- (Re)Generate help tags according to the current help files. 683 -- Also use `pcall()` because `:helptags` errors if there is no 'doc/' 684 -- directory or if it is empty. 685 local doc_dir = vim.fs.joinpath(p.path, 'doc') 686 vim.fn.delete(vim.fs.joinpath(doc_dir, 'tags')) 687 copcall(vim.cmd.helptags, { doc_dir, magic = { file = false } }) 688 end 689 690 --- @param plug_list vim.pack.Plug[] 691 local function install_list(plug_list, confirm) 692 local timestamp = get_timestamp() 693 --- @async 694 --- @param p vim.pack.Plug 695 local function do_install(p) 696 trigger_event(p, 'PackChangedPre', 'install') 697 698 git_clone(p.spec.src, p.path) 699 700 plugin_lock.plugins[p.spec.name].src = p.spec.src 701 702 -- Prefer revision from the lockfile instead of using `version` 703 p.info.sha_target = (plugin_lock.plugins[p.spec.name] or {}).rev 704 705 checkout(p, timestamp, true) 706 p.info.installed = true 707 708 trigger_event(p, 'PackChanged', 'install') 709 end 710 711 -- Install possibly after user confirmation 712 if not confirm or confirm_install(plug_list) then 713 run_list(plug_list, do_install, 'Installing plugins') 714 end 715 716 -- Ensure that not fully installed plugins are absent on disk and in lockfile 717 for _, p in ipairs(plug_list) do 718 if not (p.info.installed and uv.fs_stat(p.path) ~= nil) then 719 plugin_lock.plugins[p.spec.name] = nil 720 vim.fs.rm(p.path, { recursive = true, force = true }) 721 end 722 end 723 end 724 725 --- @async 726 --- @param p vim.pack.Plug 727 local function infer_update_details(p) 728 p.info.update_details = '' 729 infer_revisions(p) 730 local sha_head = assert(p.info.sha_head) 731 local sha_target = assert(p.info.sha_target) 732 733 -- Try showing log of changes (if any) 734 if sha_head ~= sha_target then 735 local range = sha_head .. '...' .. sha_target 736 local format = '--pretty=format:%m %h │ %s%d' 737 -- Show only tags near commits (not `origin/main`, etc.) 738 local decorate = '--decorate-refs=refs/tags' 739 -- `--topo-order` makes showing divergent branches nicer, but by itself 740 -- doesn't ensure that reverted ("left", shown with `<`) and added 741 -- ("right", shown with `>`) commits have fixed order. 742 local l = git_cmd({ 'log', format, '--topo-order', '--left-only', decorate, range }, p.path) 743 local r = git_cmd({ 'log', format, '--topo-order', '--right-only', decorate, range }, p.path) 744 p.info.update_details = l == '' and r or (r == '' and l or (l .. '\n' .. r)) 745 return 746 end 747 748 -- Suggest newer semver tags (i.e. greater than greatest past semver tag) 749 local all_semver_tags = vim.tbl_filter(is_semver, git_get_tags(p.path)) 750 if #all_semver_tags == 0 then 751 return 752 end 753 754 local older_tags = '' 755 if git_version >= vim.version.parse('2.13') then 756 older_tags = git_cmd({ 'tag', '--list', '--no-contains', sha_head }, p.path) 757 end 758 local cur_tags = git_cmd({ 'tag', '--list', '--points-at', sha_head }, p.path) 759 local past_tags = vim.split(older_tags, '\n') 760 vim.list_extend(past_tags, vim.split(cur_tags, '\n')) 761 762 local any_version = vim.version.range('*') --[[@as vim.VersionRange]] 763 local last_version = get_last_semver_tag(past_tags, any_version) 764 765 local newer_semver_tags = vim.tbl_filter(function(x) --- @param x string 766 return vim.version.gt(x, last_version) 767 end, all_semver_tags) 768 769 table.sort(newer_semver_tags, vim.version.gt) 770 p.info.update_details = table.concat(newer_semver_tags, '\n') 771 end 772 773 --- @param plug vim.pack.Plug 774 --- @param load boolean|fun(plug_data: {spec: vim.pack.Spec, path: string}) 775 local function pack_add(plug, load) 776 -- Add plugin only once, i.e. no overriding of spec. This allows users to put 777 -- plugin first to fully control its spec. 778 if active_plugins[plug.path] then 779 return 780 end 781 782 n_active_plugins = n_active_plugins + 1 783 active_plugins[plug.path] = { plug = plug, id = n_active_plugins } 784 785 if vim.is_callable(load) then 786 load({ spec = vim.deepcopy(plug.spec), path = plug.path }) 787 return 788 end 789 790 -- NOTE: The `:packadd` specifically seems to not handle spaces in dir name 791 vim.cmd.packadd({ vim.fn.escape(plug.spec.name, ' '), bang = not load, magic = { file = false } }) 792 793 -- The `:packadd` only sources plain 'plugin/' files. Execute 'after/' scripts 794 -- if not during startup (when they will be sourced later, even if 795 -- `vim.pack.add` is inside user's 'plugin/') 796 -- See https://github.com/vim/vim/issues/15584 797 -- Deliberately do so after executing all currently known 'plugin/' files. 798 if vim.v.vim_did_enter == 1 and load then 799 local after_paths = vim.fn.glob(plug.path .. '/after/plugin/**/*.{vim,lua}', false, true) 800 --- @param path string 801 vim.tbl_map(function(path) 802 vim.cmd.source({ path, magic = { file = false } }) 803 end, after_paths) 804 end 805 end 806 807 local function lock_write() 808 -- Serialize `version` 809 local lock = vim.deepcopy(plugin_lock) 810 for _, l_data in pairs(lock.plugins) do 811 local version = l_data.version 812 if version then 813 l_data.version = type(version) == 'string' and ("'%s'"):format(version) or tostring(version) 814 end 815 end 816 817 local path = lock_get_path() 818 vim.fn.mkdir(vim.fs.dirname(path), 'p') 819 local fd = assert(uv.fs_open(path, 'w', 438)) 820 821 local data = vim.json.encode(lock, { indent = ' ', sort_keys = true }) 822 assert(uv.fs_write(fd, data)) 823 assert(uv.fs_close(fd)) 824 end 825 826 --- @param names string[] 827 local function lock_repair(names, plug_dir) 828 --- @async 829 local function f() 830 for _, name in ipairs(names) do 831 local path = vim.fs.joinpath(plug_dir, name) 832 -- Try reusing existing table to preserve maybe present `version` 833 local data = plugin_lock.plugins[name] or {} 834 data.rev = git_get_hash('HEAD', path) 835 data.src = git_cmd({ 'remote', 'get-url', 'origin' }, path) 836 plugin_lock.plugins[name] = data 837 end 838 end 839 async.run(f):wait() 840 end 841 842 --- Sync lockfile data and installed plugins: 843 --- - Install plugins that have proper lockfile data but are not on disk. 844 --- - Repair corrupted lock data for installed plugins. 845 --- - Remove unrepairable corrupted lock data and plugins. 846 local function lock_sync(confirm) 847 if type(plugin_lock.plugins) ~= 'table' then 848 plugin_lock.plugins = {} 849 end 850 851 -- Compute installed plugins 852 local plug_dir = get_plug_dir() 853 if vim.uv.fs_stat(plug_dir) == nil then 854 vim.fn.mkdir(plug_dir, 'p') 855 end 856 857 -- NOTE: The directory traversal is done on every startup, but it is very fast. 858 -- Also, single `vim.fs.dir()` scales better than on demand `uv.fs_stat()` checks. 859 local installed = {} --- @type table<string,string> 860 for name, fs_type in vim.fs.dir(plug_dir) do 861 installed[name] = fs_type 862 plugin_lock.plugins[name] = plugin_lock.plugins[name] or {} 863 end 864 865 -- Traverse once optimizing for "regular startup" (no repair, no install) 866 local to_install = {} --- @type vim.pack.Plug[] 867 local to_repair = {} --- @type string[] 868 local to_remove = {} --- @type string[] 869 for name, data in pairs(plugin_lock.plugins) do 870 if type(data) ~= 'table' then 871 data = {} ---@diagnostic disable-line: missing-fields 872 plugin_lock.plugins[name] = data 873 end 874 875 -- Deserialize `version` 876 local version = data.version 877 if type(version) == 'string' then 878 data.version = version:match("^'(.+)'$") or vim.version.range(version) 879 end 880 881 -- Synchronize 882 local is_bad_lock = type(data.rev) ~= 'string' or type(data.src) ~= 'string' 883 local is_bad_plugin = installed[name] and installed[name] ~= 'directory' 884 if is_bad_lock or is_bad_plugin then 885 local t = installed[name] == 'directory' and to_repair or to_remove 886 t[#t + 1] = name 887 elseif not installed[name] then 888 local spec = { src = data.src, name = name, version = data.version } 889 to_install[#to_install + 1] = new_plug(spec, plug_dir) 890 end 891 end 892 893 -- Perform actions if needed 894 if #to_install > 0 then 895 table.sort(to_install, function(a, b) 896 return a.spec.name < b.spec.name 897 end) 898 install_list(to_install, confirm) 899 lock_write() 900 end 901 902 if #to_repair > 0 then 903 lock_repair(to_repair, plug_dir) 904 table.sort(to_repair) 905 notify('Repaired corrupted lock data for plugins: ' .. table.concat(to_repair, ', '), 'WARN') 906 lock_write() 907 end 908 909 if #to_remove > 0 then 910 for _, name in ipairs(to_remove) do 911 plugin_lock.plugins[name] = nil 912 vim.fs.rm(vim.fs.joinpath(plug_dir, name), { recursive = true, force = true }) 913 end 914 table.sort(to_remove) 915 notify('Removed corrupted lock data for plugins: ' .. table.concat(to_remove, ', '), 'WARN') 916 lock_write() 917 end 918 end 919 920 local function lock_read(confirm) 921 if plugin_lock then 922 return 923 end 924 925 local fd = uv.fs_open(lock_get_path(), 'r', 438) 926 if fd then 927 local stat = assert(uv.fs_fstat(fd)) 928 local data = assert(uv.fs_read(fd, stat.size, 0)) 929 assert(uv.fs_close(fd)) 930 plugin_lock = vim.json.decode(data) 931 else 932 plugin_lock = { plugins = {} } 933 end 934 935 lock_sync(vim.F.if_nil(confirm, true)) 936 end 937 938 --- @class vim.pack.keyset.add 939 --- @inlinedoc 940 --- Load `plugin/` files and `ftdetect/` scripts. If `false`, works like `:packadd!`. 941 --- If function, called with plugin data and is fully responsible for loading plugin. 942 --- Default `false` during |init.lua| sourcing and `true` afterwards. 943 --- @field load? boolean|fun(plug_data: {spec: vim.pack.Spec, path: string}) 944 --- 945 --- @field confirm? boolean Whether to ask user to confirm initial install. Default `true`. 946 947 --- Add plugin to current session 948 --- 949 --- - For each specification check that plugin exists on disk in |vim.pack-directory|: 950 --- - If exists, check if its `src` is the same as input. If not - delete 951 --- immediately to clean install from the new source. Otherwise do nothing. 952 --- - If doesn't exist, install it by downloading from `src` into `name` 953 --- subdirectory (via partial blobless `git clone`) and update revision 954 --- to match `version` (via `git checkout`). Plugin will not be on disk if 955 --- any step resulted in an error. 956 --- - For each plugin execute |:packadd| (or customizable `load` function) making 957 --- it reachable by Nvim. 958 --- 959 --- Notes: 960 --- - Installation is done in parallel, but waits for all to finish before 961 --- continuing next code execution. 962 --- - If plugin is already present on disk, there are no checks about its current revision. 963 --- The specified `version` can be not the one actually present on disk. 964 --- Execute |vim.pack.update()| to synchronize. 965 --- - Adding plugin second and more times during single session does nothing: 966 --- only the data from the first adding is registered. 967 --- 968 --- @param specs (string|vim.pack.Spec)[] List of plugin specifications. String item 969 --- is treated as `src`. 970 --- @param opts? vim.pack.keyset.add 971 function M.add(specs, opts) 972 vim.validate('specs', specs, vim.islist, false, 'list') 973 opts = vim.tbl_extend('force', { load = vim.v.vim_did_init == 1, confirm = true }, opts or {}) 974 vim.validate('opts', opts, 'table') 975 976 lock_read(opts.confirm) 977 978 local plug_dir = get_plug_dir() 979 local plugs = {} --- @type vim.pack.Plug[] 980 for i = 1, #specs do 981 plugs[i] = new_plug(specs[i], plug_dir) 982 end 983 plugs = normalize_plugs(plugs) 984 985 -- Pre-process 986 local plugs_to_install = {} --- @type vim.pack.Plug[] 987 local needs_lock_write = false 988 for _, p in ipairs(plugs) do 989 -- Detect `version` change 990 local p_lock = plugin_lock.plugins[p.spec.name] or {} 991 needs_lock_write = needs_lock_write or p_lock.version ~= p.spec.version 992 p_lock.version = p.spec.version 993 plugin_lock.plugins[p.spec.name] = p_lock 994 995 -- Register for install 996 if not p.info.installed then 997 plugs_to_install[#plugs_to_install + 1] = p 998 needs_lock_write = true 999 end 1000 end 1001 1002 -- Install 1003 if #plugs_to_install > 0 then 1004 git_ensure_exec() 1005 install_list(plugs_to_install, opts.confirm) 1006 end 1007 1008 if needs_lock_write then 1009 lock_write() 1010 end 1011 1012 -- Register and load those actually on disk while collecting errors 1013 -- Delay showing all errors to have "good" plugins added first 1014 local errors = {} --- @type string[] 1015 for _, p in ipairs(plugs) do 1016 if p.info.installed then 1017 local ok, err = pcall(pack_add, p, opts.load) --[[@as string]] 1018 if not ok then 1019 p.info.err = err 1020 end 1021 end 1022 if p.info.err ~= '' then 1023 errors[#errors + 1] = ('`%s`:\n%s'):format(p.spec.name, p.info.err) 1024 end 1025 end 1026 1027 if #errors > 0 then 1028 local error_str = table.concat(errors, '\n\n') 1029 error(('vim.pack:\n\n%s'):format(error_str)) 1030 end 1031 end 1032 1033 --- @param p vim.pack.Plug 1034 --- @return string 1035 local function compute_feedback_lines_single(p) 1036 local active_suffix = active_plugins[p.path] ~= nil and '' or ' (not active)' 1037 if p.info.err ~= '' then 1038 return ('## %s%s\n\n %s'):format(p.spec.name, active_suffix, p.info.err:gsub('\n', '\n ')) 1039 end 1040 1041 local parts = { ('## %s%s\n'):format(p.spec.name, active_suffix) } 1042 local version_suffix = p.info.version_str == '' and '' or (' (%s)'):format(p.info.version_str) 1043 1044 if p.info.sha_head == p.info.sha_target then 1045 parts[#parts + 1] = table.concat({ 1046 'Path: ' .. p.path, 1047 'Source: ' .. p.spec.src, 1048 'Revision: ' .. p.info.sha_target .. version_suffix, 1049 }, '\n') 1050 1051 if p.info.update_details ~= '' then 1052 local details = p.info.update_details:gsub('\n', '\n• ') 1053 parts[#parts + 1] = '\n\nAvailable newer versions:\n• ' .. details 1054 end 1055 else 1056 parts[#parts + 1] = table.concat({ 1057 'Path: ' .. p.path, 1058 'Source: ' .. p.spec.src, 1059 'Revision before: ' .. p.info.sha_head, 1060 'Revision after: ' .. p.info.sha_target .. version_suffix, 1061 '', 1062 'Pending updates:', 1063 p.info.update_details, 1064 }, '\n') 1065 end 1066 1067 return table.concat(parts, '') 1068 end 1069 1070 --- @param plug_list vim.pack.Plug[] 1071 --- @param skip_same_sha boolean 1072 --- @return string[] 1073 local function compute_feedback_lines(plug_list, skip_same_sha) 1074 -- Construct plugin line groups for better report 1075 local report_err, report_update, report_same = {}, {}, {} 1076 for _, p in ipairs(plug_list) do 1077 --- @type string[] 1078 local group_arr = p.info.err ~= '' and report_err 1079 or (p.info.sha_head ~= p.info.sha_target and report_update or report_same) 1080 group_arr[#group_arr + 1] = compute_feedback_lines_single(p) 1081 end 1082 1083 local lines = {} 1084 --- @param header string 1085 --- @param arr string[] 1086 local function append_report(header, arr) 1087 if #arr == 0 then 1088 return 1089 end 1090 header = header .. ' ' .. string.rep('─', 79 - header:len()) 1091 table.insert(lines, header) 1092 vim.list_extend(lines, arr) 1093 end 1094 append_report('# Error', report_err) 1095 append_report('# Update', report_update) 1096 if not skip_same_sha then 1097 append_report('# Same', report_same) 1098 end 1099 1100 return vim.split(table.concat(lines, '\n\n'), '\n') 1101 end 1102 1103 --- @param plug_list vim.pack.Plug[] 1104 local function feedback_log(plug_list) 1105 local lines = { ('========== Update %s =========='):format(get_timestamp()) } 1106 vim.list_extend(lines, compute_feedback_lines(plug_list, true)) 1107 lines[#lines + 1] = '' 1108 1109 local log_path = vim.fn.stdpath('log') .. '/nvim-pack.log' 1110 vim.fn.mkdir(vim.fs.dirname(log_path), 'p') 1111 vim.fn.writefile(lines, log_path, 'a') 1112 end 1113 1114 --- @param lines string[] 1115 --- @param on_finish fun(bufnr: integer) 1116 local function show_confirm_buf(lines, on_finish) 1117 -- Show buffer in a separate tabpage 1118 local bufnr = api.nvim_create_buf(true, true) 1119 api.nvim_buf_set_name(bufnr, 'nvim-pack://confirm#' .. bufnr) 1120 api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) 1121 vim.cmd.sbuffer({ bufnr, mods = { tab = vim.fn.tabpagenr() } }) 1122 local win_id = api.nvim_get_current_win() 1123 1124 local delete_buffer = vim.schedule_wrap(function() 1125 pcall(api.nvim_win_close, win_id, true) 1126 pcall(api.nvim_buf_delete, bufnr, { force = true }) 1127 vim.cmd.redraw() 1128 end) 1129 1130 -- Define action on accepting confirm 1131 local function finish() 1132 on_finish(bufnr) 1133 delete_buffer() 1134 end 1135 -- - Use `nested` to allow other events (useful for statuslines) 1136 api.nvim_create_autocmd('BufWriteCmd', { buffer = bufnr, nested = true, callback = finish }) 1137 1138 -- Define action to cancel confirm 1139 --- @type integer 1140 local cancel_au_id 1141 local function on_cancel(data) 1142 if tonumber(data.match) ~= win_id then 1143 return 1144 end 1145 pcall(api.nvim_del_autocmd, cancel_au_id) 1146 delete_buffer() 1147 end 1148 cancel_au_id = api.nvim_create_autocmd('WinClosed', { nested = true, callback = on_cancel }) 1149 1150 -- Set buffer-local options last (so that user autocmmands could override) 1151 vim.bo[bufnr].modified = false 1152 vim.bo[bufnr].modifiable = false 1153 vim.bo[bufnr].buftype = 'acwrite' 1154 vim.bo[bufnr].filetype = 'nvim-pack' 1155 1156 -- Attach in-process LSP for more capabilities 1157 vim.lsp.buf_attach_client(bufnr, require('vim.pack._lsp').client_id) 1158 end 1159 1160 --- Get map of plugin names that need update based on confirmation buffer 1161 --- content: all plugin sections present in "# Update" section. 1162 --- @param bufnr integer 1163 --- @return table<string,boolean> 1164 local function get_update_map(bufnr) 1165 local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 1166 --- @type table<string,boolean>, boolean 1167 local res, is_in_update = {}, false 1168 for _, l in ipairs(lines) do 1169 local name = l:match('^## (.+)$') 1170 if name and is_in_update then 1171 res[name:gsub(' %(not active%)$', '')] = true 1172 end 1173 1174 local group = l:match('^# (%S+)') 1175 if group then 1176 is_in_update = group == 'Update' 1177 end 1178 end 1179 return res 1180 end 1181 1182 --- @class vim.pack.keyset.update 1183 --- @inlinedoc 1184 --- @field force? boolean Whether to skip confirmation and make updates immediately. Default `false`. 1185 --- 1186 --- @field offline? boolean Whether to skip downloading new updates. Default: `false`. 1187 --- 1188 --- How to compute a new plugin revision. One of: 1189 --- - "version" (default) - use latest revision matching `version` from plugin specification. 1190 --- - "lockfile" - use revision from the lockfile. Useful for reverting or performing controlled 1191 --- update. 1192 --- @field target? string 1193 1194 --- Update plugins 1195 --- 1196 --- - Download new changes from source. 1197 --- - Infer update info (current/target revisions, changelog, etc.). 1198 --- - Depending on `force`: 1199 --- - If `false`, show confirmation buffer. It lists data about all set to 1200 --- update plugins. Pending changes starting with `>` will be applied while 1201 --- the ones starting with `<` will be reverted. 1202 --- It has dedicated buffer-local mappings: 1203 --- - |]]| and |[[| to navigate through plugin sections. 1204 --- 1205 --- Some features are provided via LSP: 1206 --- - 'textDocument/documentSymbol' (`gO` via |lsp-defaults| 1207 --- or |vim.lsp.buf.document_symbol()|) - show structure of the buffer. 1208 --- - 'textDocument/hover' (`K` via |lsp-defaults| or |vim.lsp.buf.hover()|) - 1209 --- show more information at cursor. Like details of particular pending 1210 --- change or newer tag. 1211 --- - 'textDocument/codeAction' (`gra` via |lsp-defaults| or |vim.lsp.buf.code_action()|) - 1212 --- show code actions available for "plugin at cursor". 1213 --- Like "delete" (if plugin is not active), "update" or "skip updating" 1214 --- (if there are pending updates). 1215 --- 1216 --- Execute |:write| to confirm update, execute |:quit| to discard the update. 1217 --- - If `true`, make updates right away. 1218 --- 1219 --- Notes: 1220 --- - Every actual update is logged in "nvim-pack.log" file inside "log" |stdpath()|. 1221 --- - It doesn't update source's default branch if it has changed (like from `master` to `main`). 1222 --- To have `version = nil` point to a new default branch, re-install the plugin 1223 --- (|vim.pack.del()| + |vim.pack.add()|). 1224 --- 1225 --- @param names? string[] List of plugin names to update. Must be managed 1226 --- by |vim.pack|, not necessarily already added to current session. 1227 --- Default: names of all plugins managed by |vim.pack|. 1228 --- @param opts? vim.pack.keyset.update 1229 function M.update(names, opts) 1230 vim.validate('names', names, vim.islist, true, 'list') 1231 opts = vim.tbl_extend('force', { force = false, offline = false, target = 'version' }, opts or {}) 1232 1233 local plug_list = plug_list_from_names(names) 1234 if #plug_list == 0 then 1235 notify('Nothing to update', 'WARN') 1236 return 1237 end 1238 git_ensure_exec() 1239 lock_read() 1240 1241 -- Perform update 1242 local timestamp = get_timestamp() 1243 local needs_lock_write = opts.force --- @type boolean 1244 1245 --- @async 1246 --- @param p vim.pack.Plug 1247 local function do_update(p) 1248 local l_data = plugin_lock.plugins[p.spec.name] 1249 -- Ensure proper `origin` if needed 1250 if l_data.src ~= p.spec.src then 1251 git_cmd({ 'remote', 'set-url', 'origin', p.spec.src }, p.path) 1252 plugin_lock.plugins[p.spec.name].src = p.spec.src 1253 needs_lock_write = true 1254 end 1255 1256 -- Fetch 1257 if not opts.offline then 1258 -- Using '--tags --force' means conflicting tags will be synced with remote 1259 local args = { 'fetch', '--quiet', '--tags', '--force', '--recurse-submodules=yes', 'origin' } 1260 git_cmd(args, p.path) 1261 end 1262 1263 -- Compute change info: changelog if any, new tags if nothing to update 1264 if opts.target == 'lockfile' then 1265 p.info.version_str = '*lockfile*' 1266 p.info.sha_target = l_data.rev 1267 end 1268 infer_update_details(p) 1269 1270 -- Checkout immediately if no need to confirm 1271 if opts.force and p.info.sha_head ~= p.info.sha_target then 1272 trigger_event(p, 'PackChangedPre', 'update') 1273 checkout(p, timestamp) 1274 trigger_event(p, 'PackChanged', 'update') 1275 end 1276 end 1277 local progress_title = opts.force and (opts.offline and 'Applying updates' or 'Updating') 1278 or (opts.offline and 'Computing updates' or 'Downloading updates') 1279 run_list(plug_list, do_update, progress_title) 1280 1281 if needs_lock_write then 1282 lock_write() 1283 end 1284 1285 if opts.force then 1286 feedback_log(plug_list) 1287 return 1288 end 1289 1290 -- Show report in new buffer in separate tabpage 1291 local lines = compute_feedback_lines(plug_list, false) 1292 show_confirm_buf(lines, function(bufnr) 1293 local to_update = get_update_map(bufnr) 1294 if not next(to_update) then 1295 notify('Nothing to update', 'WARN') 1296 return 1297 end 1298 1299 --- @param p vim.pack.Plug 1300 local plugs_to_checkout = vim.tbl_filter(function(p) 1301 return to_update[p.spec.name] 1302 end, plug_list) 1303 1304 local timestamp2 = get_timestamp() 1305 --- @async 1306 --- @param p vim.pack.Plug 1307 local function do_checkout(p) 1308 trigger_event(p, 'PackChangedPre', 'update') 1309 checkout(p, timestamp2) 1310 trigger_event(p, 'PackChanged', 'update') 1311 end 1312 run_list(plugs_to_checkout, do_checkout, 'Applying updates') 1313 1314 lock_write() 1315 feedback_log(plugs_to_checkout) 1316 end) 1317 end 1318 1319 --- @class vim.pack.keyset.del 1320 --- @inlinedoc 1321 --- @field force? boolean Whether to allow deleting an active plugin. Default `false`. 1322 1323 --- Remove plugins from disk 1324 --- 1325 --- @param names string[] List of plugin names to remove from disk. Must be managed 1326 --- by |vim.pack|, not necessarily already added to current session. 1327 --- @param opts? vim.pack.keyset.del 1328 function M.del(names, opts) 1329 vim.validate('names', names, vim.islist, false, 'list') 1330 opts = vim.tbl_extend('force', { force = false }, opts or {}) 1331 1332 local plug_list = plug_list_from_names(names) 1333 if #plug_list == 0 then 1334 notify('Nothing to remove', 'WARN') 1335 return 1336 end 1337 1338 lock_read() 1339 1340 local fail_to_delete = {} --- @type string[] 1341 for _, p in ipairs(plug_list) do 1342 if not active_plugins[p.path] or opts.force then 1343 trigger_event(p, 'PackChangedPre', 'delete') 1344 1345 vim.fs.rm(p.path, { recursive = true, force = true }) 1346 active_plugins[p.path] = nil 1347 notify(("Removed plugin '%s'"):format(p.spec.name), 'INFO') 1348 1349 plugin_lock.plugins[p.spec.name] = nil 1350 1351 trigger_event(p, 'PackChanged', 'delete') 1352 else 1353 fail_to_delete[#fail_to_delete + 1] = p.spec.name 1354 end 1355 end 1356 1357 lock_write() 1358 1359 if #fail_to_delete > 0 then 1360 local plugs = table.concat(fail_to_delete, ', ') 1361 local msg = ('Some plugins are active and were not deleted: %s.'):format(plugs) 1362 .. ' Remove them from init.lua, restart, and try again.' 1363 error(msg) 1364 end 1365 end 1366 1367 --- @inlinedoc 1368 --- @class vim.pack.PlugData 1369 --- @field active boolean Whether plugin was added via |vim.pack.add()| to current session. 1370 --- @field branches? string[] Available Git branches (first is default). Missing if `info=false`. 1371 --- @field path string Plugin's path on disk. 1372 --- @field rev string Current Git revision. 1373 --- @field spec vim.pack.SpecResolved A |vim.pack.Spec| with resolved `name`. 1374 --- @field tags? string[] Available Git tags. Missing if `info=false`. 1375 1376 --- @class vim.pack.keyset.get 1377 --- @inlinedoc 1378 --- @field info boolean Whether to include extra plugin info. Default `true`. 1379 1380 --- @param p_data_list vim.pack.PlugData[] 1381 local function add_p_data_info(p_data_list) 1382 local funs = {} --- @type (async fun())[] 1383 for i, p_data in ipairs(p_data_list) do 1384 local path = p_data.path 1385 --- @async 1386 funs[i] = function() 1387 p_data.branches = git_get_branches(path) 1388 p_data.tags = git_get_tags(path) 1389 end 1390 end 1391 --- @async 1392 local function joined_f() 1393 async.join(n_threads, funs) 1394 end 1395 async.run(joined_f):wait() 1396 end 1397 1398 --- Gets |vim.pack| plugin info, optionally filtered by `names`. 1399 --- @param names? string[] List of plugin names. Default: all plugins managed by |vim.pack|. 1400 --- @param opts? vim.pack.keyset.get 1401 --- @return vim.pack.PlugData[] 1402 function M.get(names, opts) 1403 vim.validate('names', names, vim.islist, true, 'list') 1404 opts = vim.tbl_extend('force', { info = true }, opts or {}) 1405 1406 -- Process active plugins in order they were added. Take into account that 1407 -- there might be "holes" after `vim.pack.del()`. 1408 local active = {} --- @type table<integer,vim.pack.Plug?> 1409 for _, p_active in pairs(active_plugins) do 1410 active[p_active.id] = p_active.plug 1411 end 1412 1413 lock_read() 1414 local res = {} --- @type vim.pack.PlugData[] 1415 local used_names = {} --- @type table<string,boolean> 1416 for i = 1, n_active_plugins do 1417 if active[i] and (not names or vim.tbl_contains(names, active[i].spec.name)) then 1418 local name = active[i].spec.name 1419 local spec = vim.deepcopy(active[i].spec) 1420 local rev = (plugin_lock.plugins[name] or {}).rev 1421 res[#res + 1] = { spec = spec, path = active[i].path, rev = rev, active = true } 1422 used_names[name] = true 1423 end 1424 end 1425 1426 local plug_dir = get_plug_dir() 1427 for name, l_data in vim.spairs(plugin_lock.plugins) do 1428 local path = vim.fs.joinpath(plug_dir, name) 1429 local is_in_names = not names or vim.tbl_contains(names, name) 1430 if not active_plugins[path] and is_in_names then 1431 local spec = { name = name, src = l_data.src, version = l_data.version } 1432 res[#res + 1] = { spec = spec, path = path, rev = l_data.rev, active = false } 1433 used_names[name] = true 1434 end 1435 end 1436 1437 if names ~= nil then 1438 -- Align result with input 1439 local names_order = {} --- @type table<string,integer> 1440 for i, n in ipairs(names) do 1441 if not used_names[n] then 1442 error(('Plugin `%s` is not installed'):format(tostring(n))) 1443 end 1444 names_order[n] = i 1445 end 1446 table.sort(res, function(a, b) 1447 return names_order[a.spec.name] < names_order[b.spec.name] 1448 end) 1449 end 1450 1451 if opts.info then 1452 add_p_data_info(res) 1453 end 1454 1455 return res 1456 end 1457 1458 return M