From 4cdd6d76c5ad2714aa5c056c8b16fca408120e61 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 31 Jul 2026 11:28:11 +0300 Subject: [PATCH 1/2] feat(pack): introduce plugin manifest support Problem: There is no agreed way for plugins to provide extra information about themselves. Like required Neovim version, install/update/delete hooks, and dependencies. Solution: Introduce basic concept of plugin manifest file `pkg.json` based on https://packspec.org/. --- runtime/doc/news.txt | 1 + runtime/doc/pack.txt | 32 +++++++++++++++++ runtime/lua/vim/pack.lua | 53 ++++++++++++++++++++++++++++ runtime/lua/vim/pack/health.lua | 30 ++++++++++++++++ test/functional/plugin/pack_spec.lua | 24 +++++++++++++ 5 files changed, 140 insertions(+) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index de1ab37d42..241ce6e058 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -339,6 +339,7 @@ LUA • |vim.log| provides a logging interface. • |vim.pack.get()| output includes revision of a pending update. • |vim.pack.get()| can fetch new updates before computing the output. +• |vim.pack| supports |vim.pack-manifest| of plugins. • |vim.o| now accepts table style values for assignment. • |vim.keycode()| returns structured info as return value 2. • |Iter:count()| counts items in the iterator. diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index ac065c1a1f..00014e806d 100644 --- a/runtime/doc/pack.txt +++ b/runtime/doc/pack.txt @@ -423,6 +423,35 @@ These events can be used to execute plugin hooks. For example: >lua vim.api.nvim_create_autocmd('PackChanged', { callback = hooks }) < + *vim.pack-manifest* + +Plugins can come with a special top level `pkg.json` manifest file with extra +information. If present, `vim.pack` uses it for improved user experience: +• Running |:checkhealth| for `vim.pack` will perform extra checks to ensure + healthy plugin installation. + +Full specification see at https://packspec.org/. See also |vim.pack.Manifest|. +A simple example: >json + { + "name": "best-plugin.nvim", + "description": "The best plugin for Neovim", + "engines": { + "nvim": ">=0.13.0", + "vim": ">=9.1.0" + } + } +< + + +*vim.pack.Manifest* + + Fields: ~ + • {description}? (`string`) Plugin description + • {engines}? (`table`) Supported engine versions. Values should be + |vim.version.range()| compatible specs. + • {nvim}? (`string`) Version range for Nvim. + • {vim}? (`string`) Version range for Vim. + • {name}? (`string`) Plugin name *vim.pack.Spec* @@ -506,6 +535,9 @@ get({names}, {opts}) *vim.pack.get()* to current session. • {branches}? (`string[]`) Available Git branches (first is default). Missing if `info=false`. + • {manifest}? (`vim.pack.Manifest`) Data from the |vim.pack-manifest|. + Empty in case of reading error. Missing if `info=false`. See + |vim.pack.Manifest|. • {path} (`string`) Plugin's path on disk. • {rev} (`string`) Current Git revision. Taken from |vim.pack-lockfile| if `info=false`. diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index 2b456a6dbb..a69684de75 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -232,6 +232,25 @@ ----- To act on install from lockfile, run before very first `vim.pack.add()` ---vim.api.nvim_create_autocmd('PackChanged', { callback = hooks }) ---``` +---[vim.pack-manifest]() +--- +---Plugins can come with a special top level `pkg.json` manifest file with extra +---information. If present, `vim.pack` uses it for improved user experience: +---- Running |:checkhealth| for `vim.pack` will perform extra checks to ensure +--- healthy plugin installation. +--- +---Full specification see at https://packspec.org/. See also |vim.pack.Manifest|. +---A simple example: +---```json +---{ +--- "name": "best-plugin.nvim", +--- "description": "The best plugin for Neovim", +--- "engines": { +--- "nvim": ">=0.13.0", +--- "vim": ">=9.1.0" +--- } +---} +---``` local api = vim.api local uv = vim.uv @@ -516,6 +535,37 @@ end local active_plugins = {} local n_active_plugins = 0 +--- @class vim.pack.ManifestEngines +--- @inlinedoc +--- @field nvim? string Version range for Nvim. +--- @field vim? string Version range for Vim. + +--- @class vim.pack.Manifest +--- @field name? string Plugin name +--- @field description? string Plugin description +--- Supported engine versions. Values should be |vim.version.range()| compatible specs. +--- @field engines? vim.pack.ManifestEngines + +--- @param path string +--- @return vim.pack.Manifest? +local function manifest_read(path) + local manifest_path = vim.fs.joinpath(path, 'pkg.json') + local stat = uv.fs_stat(manifest_path) + if not stat then + return nil + end + + local fd = uv.fs_open(manifest_path, 'r', 438) + if not fd then + return {} + end + + local data = assert(uv.fs_read(fd, stat.size, 0)) + assert(uv.fs_close(fd)) + local ok, res = pcall(vim.json.decode, data) + return (ok and type(res) == 'table') and res or {} +end + --- @param plugs vim.pack.Plug[] --- @param event_name 'PackChangedPre'|'PackChanged' --- @param kind 'install'|'update'|'delete' @@ -1468,6 +1518,8 @@ end --- @class vim.pack.PlugData --- @field active boolean Whether plugin was added via |vim.pack.add()| to current session. --- @field branches? string[] Available Git branches (first is default). Missing if `info=false`. +--- Data from the |vim.pack-manifest|. Empty in case of reading error. Missing if `info=false`. +--- @field manifest? vim.pack.Manifest --- @field path string Plugin's path on disk. --- @field rev string Current Git revision. Taken from |vim.pack-lockfile| if `info=false`. --- Git revision of a pending update. The same as used during |vim.pack.update()| and which @@ -1494,6 +1546,7 @@ local function add_p_data_info(p_data_list, offline) funs[i] = function() p_data.branches = git_get_branches(path) p_data.tags = git_get_tags(path) + p_data.manifest = manifest_read(path) if not offline then git_fetch(path) diff --git a/runtime/lua/vim/pack/health.lua b/runtime/lua/vim/pack/health.lua index 5045c58c8f..366474e39e 100644 --- a/runtime/lua/vim/pack/health.lua +++ b/runtime/lua/vim/pack/health.lua @@ -200,6 +200,31 @@ local function check_lockfile() end end +local function check_manifest(manifest, plug_name) + local name_str = vim.inspect(plug_name) + if vim.tbl_count(manifest) == 0 then + health.warn(('Plugin %s has empty or malformed manifest file'):format(name_str)) + return false + end + + local nvim_engine = (manifest.engines or {}).nvim or '*' + local ok_version, nvim_version_range = pcall(vim.version.range, nvim_engine) + if not ok_version then + health.warn(('Plugin %s has malformed `engines.nvim` in manifest file'):format(name_str)) + return false + end + --- @cast nvim_version_range vim.VersionRange + if not nvim_version_range:has(vim.version()) then + health.warn( + ('Plugin %s Nvim version requirement %s'):format(name_str, tostring(nvim_version_range)) + .. (' does not match current version %s'):format(tostring(vim.version())) + ) + return false + end + + return true +end + --- @return boolean Whether a check is successful local function check_installed_plugin(plug_name) local name_str = vim.inspect(plug_name) @@ -248,6 +273,11 @@ local function check_installed_plugin(plug_name) ) end + -- Manifest + if info[1].manifest then + return check_manifest(info[1].manifest, plug_name) + end + return true end diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index b855233070..a52d2f2473 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -244,6 +244,21 @@ function repos_setup.with_subs() git_add_commit('Second commit for "with_subs"', 'with_subs') end +function repos_setup.with_manifest() + init_test_repo('manifest') + + repo_write_file('manifest', 'lua/manifest.lua', 'return "manifest init"') + + local manifest_tbl = { + name = 'plug', + description = 'Nvim plugin', + engine = { nvim = '>=0.12.0', vim = '>=9.1.0' }, + } + repo_write_file('manifest', 'pkg.json', vim.json.encode(manifest_tbl)) + + git_add_commit('Initial commit', 'manifest') +end + -- Utility -------------------------------------------------------------------- --- Execute `vim.pack.add()` inside `testnvim` instance @@ -2227,6 +2242,15 @@ describe('vim.pack', function() ) eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" }, { info = false })')) eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" }, { info = false })')) + + -- Reports manifest + vim_pack_add({ repos_src.manifest }) + local manifest_tbl = { + name = 'plug', + description = 'Nvim plugin', + engine = { nvim = '>=0.12.0', vim = '>=9.1.0' }, + } + eq(manifest_tbl, exec_lua('return vim.pack.get({ "manifest" })[1].manifest')) end) it('reports potential revision after update', function() From 9d12df0398499d22bb509ad5c209dd608095596d Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 31 Jul 2026 15:17:00 +0300 Subject: [PATCH 2/2] feat(pack): add support for sourcing manifest scripts Problem: No way for plugins to define hooks that would be executed during plugin lifecycle. Like after install/update or before update/delete. Solution: Automatically source scripts defined in plugin manifest file (if any) after triggering corresponding `PackChanged{,Pre}` events. During sourcing make some termporary adjustments: - Current directory is set to plugin's root to simplify execution of CLI calls like `make build`. - Plugin's path is ensured to be inside 'runtimepath' to allow using `require('plugin-module')` inside manifest scripts. The reason to execute after triggering event is so that there is a possibility for users to execute code both before the script (exactly on event) and after the script (more-or-less via `vim.schedule` called on the event). --- runtime/doc/pack.txt | 21 ++++ runtime/lua/vim/pack.lua | 53 ++++++++++ runtime/lua/vim/pack/health.lua | 17 ++- test/functional/plugin/pack_spec.lua | 153 +++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 2 deletions(-) diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index 00014e806d..46b8cf9f0a 100644 --- a/runtime/doc/pack.txt +++ b/runtime/doc/pack.txt @@ -427,6 +427,14 @@ These events can be used to execute plugin hooks. For example: >lua Plugins can come with a special top level `pkg.json` manifest file with extra information. If present, `vim.pack` uses it for improved user experience: +• Apply |:source| for scripts after triggering corresponding + |vim.pack-events|. This allows plugins to define hooks that will be executed + during plugin's lifetime. Sourcing is done with special context: + • The |current-directory| is temporarily set to plugin's root (to make it + easier to run |vim.system()| commands). + • Plugin's path is temporarily ensured to be inside |'runtimepath'| (so + script can use |require()| with plugin's module, possibly with explicit + |package.loaded| reset inside `"update"` scripts). • Running |:checkhealth| for `vim.pack` will perform extra checks to ensure healthy plugin installation. @@ -438,6 +446,12 @@ A simple example: >json "engines": { "nvim": ">=0.13.0", "vim": ">=9.1.0" + }, + "scripts": { + "install": "scripts/install.lua", + "preupdate": "scripts/preupdate.vim", + "update": "scripts/update.vim", + "preuninstall": "scripts/preuninstall.lua" } } < @@ -452,6 +466,13 @@ A simple example: >json • {nvim}? (`string`) Version range for Nvim. • {vim}? (`string`) Version range for Vim. • {name}? (`string`) Plugin name + • {scripts}? (`table`) Script locations (relative to plugin's root) + to |:source| after triggering corresponding + |vim.pack-events|. + • {install}? (`string`) Post install script. + • {preuninstall}? (`string`) Pre delete script. + • {preupdate}? (`string`) Pre update script. + • {update}? (`string`) Post update script. *vim.pack.Spec* diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index a69684de75..0e986d3196 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -236,6 +236,14 @@ --- ---Plugins can come with a special top level `pkg.json` manifest file with extra ---information. If present, `vim.pack` uses it for improved user experience: +---- Apply |:source| for scripts after triggering corresponding |vim.pack-events|. +--- This allows plugins to define hooks that will be executed during plugin's lifetime. +--- Sourcing is done with special context: +--- - The |current-directory| is temporarily set to plugin's root (to make +--- it easier to run |vim.system()| commands). +--- - Plugin's path is temporarily ensured to be inside |'runtimepath'| (so script +--- can use |require()| with plugin's module, possibly with explicit |package.loaded| +--- reset inside `"update"` scripts). ---- Running |:checkhealth| for `vim.pack` will perform extra checks to ensure --- healthy plugin installation. --- @@ -248,6 +256,12 @@ --- "engines": { --- "nvim": ">=0.13.0", --- "vim": ">=9.1.0" +--- }, +--- "scripts": { +--- "install": "scripts/install.lua", +--- "preupdate": "scripts/preupdate.vim", +--- "update": "scripts/update.vim", +--- "preuninstall": "scripts/preuninstall.lua" --- } ---} ---``` @@ -540,11 +554,21 @@ local n_active_plugins = 0 --- @field nvim? string Version range for Nvim. --- @field vim? string Version range for Vim. +--- @class vim.pack.ManifestScripts +--- @inlinedoc +--- @field install? string Post install script. +--- @field preupdate? string Pre update script. +--- @field update? string Post update script. +--- @field preuninstall? string Pre delete script. + --- @class vim.pack.Manifest --- @field name? string Plugin name --- @field description? string Plugin description --- Supported engine versions. Values should be |vim.version.range()| compatible specs. --- @field engines? vim.pack.ManifestEngines +--- Script locations (relative to plugin's root) to |:source| after triggering +--- corresponding |vim.pack-events|. +--- @field scripts? vim.pack.ManifestScripts --- @param path string --- @return vim.pack.Manifest? @@ -566,14 +590,43 @@ local function manifest_read(path) return (ok and type(res) == 'table') and res or {} end +--- @param p vim.pack.Plug +--- @param name string +local function source_manifest_script(p, name) + local manifest = manifest_read(p.path) + if not (type(manifest) == 'table' and (manifest.scripts or {})[name]) then + return + end + + local script_path = vim.fs.joinpath(p.path, (manifest.scripts or {})[name]) + vim._with({ cwd = p.path, o = { runtimepath = vim.o.runtimepath } }, function() + vim.cmd.packadd({ p.spec.name, bang = true }) + ---@diagnostic disable-next-line: no-unknown + local ok, err = pcall(vim.cmd.source, { script_path, magic = { file = false, bar = false } }) + if not ok then + notify(err, 'WARN') + end + end) +end + +local manifest_script_name_map = { + PackChangedPre = { update = 'preupdate', delete = 'preuninstall' }, + PackChanged = { install = 'install', update = 'update' }, +} + --- @param plugs vim.pack.Plug[] --- @param event_name 'PackChangedPre'|'PackChanged' --- @param kind 'install'|'update'|'delete' local function trigger_events(plugs, event_name, kind) + local manifest_script_name = manifest_script_name_map[event_name][kind] for _, p in ipairs(plugs) do local active = active_plugins[p.path] ~= nil local data = { active = active, kind = kind, spec = vim.deepcopy(p.spec), path = p.path } api.nvim_exec_autocmds(event_name, { pattern = p.path, data = data }) + + if manifest_script_name then + source_manifest_script(p, manifest_script_name) + end end end diff --git a/runtime/lua/vim/pack/health.lua b/runtime/lua/vim/pack/health.lua index 366474e39e..6e4749976a 100644 --- a/runtime/lua/vim/pack/health.lua +++ b/runtime/lua/vim/pack/health.lua @@ -200,7 +200,8 @@ local function check_lockfile() end end -local function check_manifest(manifest, plug_name) +--- @param manifest vim.pack.Manifest +local function check_manifest(manifest, plug_name, plug_path) local name_str = vim.inspect(plug_name) if vim.tbl_count(manifest) == 0 then health.warn(('Plugin %s has empty or malformed manifest file'):format(name_str)) @@ -222,6 +223,18 @@ local function check_manifest(manifest, plug_name) return false end + local ok_scripts = true + ---@diagnostic disable-next-line: no-unknown + for name, script_path in pairs(manifest.scripts or {}) do + if vim.fn.filereadable(vim.fs.joinpath(plug_path, script_path)) == 0 then + health.warn(('Plugin %s has no %s script at %s path'):format(name_str, name, script_path)) + ok_scripts = false + end + end + if not ok_scripts then + return false + end + return true end @@ -275,7 +288,7 @@ local function check_installed_plugin(plug_name) -- Manifest if info[1].manifest then - return check_manifest(info[1].manifest, plug_name) + return check_manifest(info[1].manifest, plug_name, plug_path) end return true diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index a52d2f2473..ece1731df7 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -253,10 +253,52 @@ function repos_setup.with_manifest() name = 'plug', description = 'Nvim plugin', engine = { nvim = '>=0.12.0', vim = '>=9.1.0' }, + -- Should handle both `*.lua` and `*.vim` files + scripts = { + install = 'scripts/install.lua', + preupdate = 'scripts/preupdate.vim', + update = 'scripts/update.lua', + preuninstall = 'scirpts/preuninstall.vim', + }, } repo_write_file('manifest', 'pkg.json', vim.json.encode(manifest_tbl)) + -- Create scripts that use Lua module from the same plugin + local hook_module_text = [[ + return { + hook = function(script_name) + -- NOTE: share the log with the one used for watching events to test + -- relative order of events and script + _G.event_log = _G.event_log or {} + local rtp = vim.api.nvim_list_runtime_paths() + local data = { kind = script_name, cwd = vim.fs.normalize(vim.uv.cwd()), rtp = rtp } + table.insert(_G.event_log, { event = 'script', data = data }) + + -- Should handle errors during script exectuion + error('Error in ' .. script_name .. ' script') + end + } + ]] + repo_write_file('manifest', 'lua/manifest-hook.lua', hook_module_text) + + for name, script_path in pairs(manifest_tbl.scripts) do + local text = ("require('manifest-hook').hook('%s')"):format(name) + if vim.endswith(script_path, '.vim') then + text = 'lua << EOF\n' .. text .. '\nEOF' + end + repo_write_file('manifest', script_path, text) + end + git_add_commit('Initial commit', 'manifest') + + -- Add extra branch to test script execution in `vim.pack.update()` + git_cmd({ 'checkout', '-b', 'other-branch' }, 'manifest') + repo_write_file('manifest', 'lua/manifest.lua', 'return "manifest other-branch"') + local other_script = "_G.other_script = true\nrequire('manifest-hook').hook('update')" + repo_write_file('manifest', 'scripts/update.lua', other_script) + git_add_commit('Other commit', 'manifest') + + git_cmd({ 'checkout', 'main' }, 'manifest') end -- Utility -------------------------------------------------------------------- @@ -293,6 +335,17 @@ local function assert_packchanged(log_compact) eq(expected, log) end +local function assert_manifest_scripts(plug_path, ref_log_trimmed) + -- Should be sourced in particular order relative to `PackChanged` events + local log_trimmed = {} --- @type table[] + for i, tbl in ipairs(exec_lua('return _G.event_log')) do + local rtp_has_plug = tbl.data.rtp and vim.tbl_contains(tbl.data.rtp, plug_path) or nil + local kind, cwd = tbl.data.kind, tbl.data.cwd + log_trimmed[i] = { event = tbl.event, kind = kind, cwd = cwd, rtp_has_plug = rtp_has_plug } + end + eq(ref_log_trimmed, log_trimmed) +end + local function track_nvim_echo() exec_lua(function() _G.echo_log = {} @@ -305,6 +358,17 @@ local function track_nvim_echo() end) end +local function is_in_echo_log(chunk_substring, chunk_hl) + ---@diagnostic disable-next-line: no-unknown + for _, msg in ipairs(exec_lua('return _G.echo_log')) do + local chunk = msg[1][1] --- @type [string, string] + if chunk[1]:find(chunk_substring, 0, true) and chunk[2] == chunk_hl then + return true + end + end + return false +end + --- @param echo_log table[]? local function assert_progress_report(echo_log, action, step_names) echo_log = echo_log or exec_lua('return _G.echo_log') @@ -965,6 +1029,30 @@ describe('vim.pack', function() }) end) + it('sources relevant manifest scripts', function() + watch_events({ 'PackChangedPre', 'PackChanged' }) + track_nvim_echo() + local cwd = fn.getcwd() + exec_lua(function() + vim.pack.add({ repos_src.manifest }, { load = function() end }) + end) + + -- Should not have side effects after executing script in special context + eq(cwd, fn.getcwd()) + local plug_path = pack_get_plug_path('manifest') + eq(false, vim.tbl_contains(api.nvim_list_runtime_paths(), plug_path)) + + -- Should execute script in special context after triggering events + assert_manifest_scripts(plug_path, { + { event = 'PackChangedPre', kind = 'install' }, + { event = 'PackChanged', kind = 'install' }, + { event = 'script', kind = 'install', cwd = plug_path, rtp_has_plug = true }, + }) + + -- Should warn on errors during script execution + eq(true, is_in_echo_log('Error in install script', 'WarningMsg')) + end) + it('recognizes several `version` types', function() local prev_commit = git_get_hash('HEAD~', 'defbranch') exec_lua(function() @@ -2031,6 +2119,40 @@ describe('vim.pack', function() }) end) + it('sources relevant manifest scripts', function() + vim_pack_add({ repos_src.manifest }) + n.clear() + + exec_lua(function() + local specs = { { src = repos_src.manifest, version = 'other-branch' } } + vim.pack.add(specs, { load = function() end }) + end) + + watch_events({ 'PackChangedPre', 'PackChanged' }) + track_nvim_echo() + local cwd = fn.getcwd() + exec_lua("vim.pack.update({ 'manifest' }, { force = true })") + + -- Should not have side effects after executing script in special context + eq(cwd, fn.getcwd()) + local plug_path = pack_get_plug_path('manifest') + eq(false, vim.tbl_contains(api.nvim_list_runtime_paths(), plug_path)) + + -- Should execute script in special context after triggering events + assert_manifest_scripts(plug_path, { + { event = 'PackChangedPre', kind = 'update' }, + { event = 'script', kind = 'preupdate', cwd = plug_path, rtp_has_plug = true }, + { event = 'PackChanged', kind = 'update' }, + { event = 'script', kind = 'update', cwd = plug_path, rtp_has_plug = true }, + }) + -- - Should execute update script as it is *after the update* + eq(true, exec_lua('return _G.other_script')) + + -- Should warn on errors during script execution + eq(true, is_in_echo_log('Error in preupdate script', 'WarningMsg')) + eq(true, is_in_echo_log('Error in update script', 'WarningMsg')) + end) + it('stashes before applying changes', function() local fetch_lua_file = vim.fs.joinpath(pack_get_plug_path('fetch'), 'lua', 'fetch.lua') fn.writefile({ 'A text that will be stashed' }, fetch_lua_file) @@ -2249,6 +2371,12 @@ describe('vim.pack', function() name = 'plug', description = 'Nvim plugin', engine = { nvim = '>=0.12.0', vim = '>=9.1.0' }, + scripts = { + install = 'scripts/install.lua', + preupdate = 'scripts/preupdate.vim', + update = 'scripts/update.lua', + preuninstall = 'scirpts/preuninstall.vim', + }, } eq(manifest_tbl, exec_lua('return vim.pack.get({ "manifest" })[1].manifest')) end) @@ -2518,6 +2646,31 @@ describe('vim.pack', function() eq(false, vim.uv.fs_stat(get_lock_path()) ~= nil) end) + it('sources relevant manifest scripts', function() + vim_pack_add({ repos_src.manifest }) + n.clear() + + watch_events({ 'PackChangedPre', 'PackChanged' }) + track_nvim_echo() + local cwd = fn.getcwd() + exec_lua("vim.pack.del({ 'manifest' })") + + -- Should not have side effects after executing script in special context + eq(cwd, fn.getcwd()) + local plug_path = pack_get_plug_path('manifest') + eq(false, vim.tbl_contains(api.nvim_list_runtime_paths(), plug_path)) + + -- Should execute script in special context after triggering events + assert_manifest_scripts(plug_path, { + { event = 'PackChangedPre', kind = 'delete' }, + { event = 'script', kind = 'preuninstall', cwd = plug_path, rtp_has_plug = true }, + { event = 'PackChanged', kind = 'delete' }, + }) + + -- Should warn on errors during script execution + eq(true, is_in_echo_log('Error in preuninstall script', 'WarningMsg')) + end) + it('validates input', function() local function assert(err_pat, input) local function del_input()