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()