diff options
| author | Justin M. Keyes <justinkz@gmail.com> | 2026-08-13 08:53:49 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-13 08:53:49 -0400 |
| commit | a1ea2f35be951673bd5ef100f96b756e9eec00f6 (patch) | |
| tree | f281eaf8dd7c52ff8295c4aecb9776befcdc37e7 /runtime/lua/vim | |
| parent | 3934dabfbb9f4a185750b50a38efc56eac0138fc (diff) | |
| parent | 9d12df0398499d22bb509ad5c209dd608095596d (diff) | |
| download | rneovim-a1ea2f35be951673bd5ef100f96b756e9eec00f6.tar.gz rneovim-a1ea2f35be951673bd5ef100f96b756e9eec00f6.tar.bz2 rneovim-a1ea2f35be951673bd5ef100f96b756e9eec00f6.zip | |
Merge #41075 from echasnovski/pack-packspec-part1
Diffstat (limited to 'runtime/lua/vim')
| -rw-r--r-- | runtime/lua/vim/pack.lua | 106 | ||||
| -rw-r--r-- | runtime/lua/vim/pack/health.lua | 43 |
2 files changed, 149 insertions, 0 deletions
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index 2b456a6dbb..0e986d3196 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -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) diff --git a/runtime/lua/vim/pack/health.lua b/runtime/lua/vim/pack/health.lua index 5045c58c8f..6e4749976a 100644 --- a/runtime/lua/vim/pack/health.lua +++ b/runtime/lua/vim/pack/health.lua @@ -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 |