mirror of
https://github.com/neovim/neovim.git
synced 2026-08-14 03:04:54 +00:00
Merge #41075 from echasnovski/pack-packspec-part1
This commit is contained in:
@@ -370,6 +370,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.
|
||||
|
||||
@@ -423,6 +423,56 @@ 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:
|
||||
• 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.
|
||||
|
||||
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"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "scripts/install.lua",
|
||||
"preupdate": "scripts/preupdate.vim",
|
||||
"update": "scripts/update.vim",
|
||||
"preuninstall": "scripts/preuninstall.lua"
|
||||
}
|
||||
}
|
||||
<
|
||||
|
||||
|
||||
*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
|
||||
• {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*
|
||||
|
||||
@@ -506,6 +556,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`.
|
||||
|
||||
@@ -232,6 +232,39 @@
|
||||
----- 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:
|
||||
---- 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.
|
||||
---
|
||||
---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"
|
||||
--- },
|
||||
--- "scripts": {
|
||||
--- "install": "scripts/install.lua",
|
||||
--- "preupdate": "scripts/preupdate.vim",
|
||||
--- "update": "scripts/update.vim",
|
||||
--- "preuninstall": "scripts/preuninstall.lua"
|
||||
--- }
|
||||
---}
|
||||
---```
|
||||
|
||||
local api = vim.api
|
||||
local uv = vim.uv
|
||||
@@ -516,14 +549,84 @@ 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.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?
|
||||
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 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
|
||||
|
||||
@@ -1468,6 +1571,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 +1599,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)
|
||||
|
||||
@@ -200,6 +200,44 @@ local function check_lockfile()
|
||||
end
|
||||
end
|
||||
|
||||
--- @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))
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
--- @return boolean Whether a check is successful
|
||||
local function check_installed_plugin(plug_name)
|
||||
local name_str = vim.inspect(plug_name)
|
||||
@@ -248,6 +286,11 @@ local function check_installed_plugin(plug_name)
|
||||
)
|
||||
end
|
||||
|
||||
-- Manifest
|
||||
if info[1].manifest then
|
||||
return check_manifest(info[1].manifest, plug_name, plug_path)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
@@ -244,6 +244,63 @@ 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' },
|
||||
-- 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 --------------------------------------------------------------------
|
||||
|
||||
--- Execute `vim.pack.add()` inside `testnvim` instance
|
||||
@@ -278,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 = {}
|
||||
@@ -290,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')
|
||||
@@ -950,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()
|
||||
@@ -2016,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)
|
||||
@@ -2227,6 +2364,21 @@ 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' },
|
||||
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)
|
||||
|
||||
it('reports potential revision after update', function()
|
||||
@@ -2494,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()
|
||||
|
||||
Reference in New Issue
Block a user