8000 fix(merge): clear opts keys set to vim.NIL by zuqini · Pull Request #47 · zuqini/zpack.nvim · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/zpack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,17 @@ opts (table|function(plugin, opts), optional)
plugin are merged - each opts function receives the result from
previous specs.

Set a key to `vim.NIL` to remove it, as in lazy.nvim. It is
removed only where specs merge: at the top level of opts
(unless these or the earlier opts are a non-empty list), and
in a table merged into the one an earlier spec set at the
same place. A table that replaces the earlier value keeps the
`vim.NIL` inside it (nothing, a scalar or a non-empty list was
there, or it is a non-empty list itself), and so do opts a
function returns or edits. To hand a plugin a JSON `null`
(e.g. in LSP settings), nest it in a table no earlier spec
sets, or set it from function-form opts.

When a custom `config` function is provided, the resolved
`opts` is passed as the second argument, but you are
responsible for calling `setup()` yourself.
Expand Down
1 change: 1 addition & 0 deletions docs/tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Most of your lazy.nvim plugin specs will work as-is with zpack. However, zpack f
Known gotchas when using zpack:
- **install/update feedback**: `vim.pack` surfaces install/update progress via `:messages` (e.g. `vim.pack: Downloading updates (0/83)`). These messages are hidden if you have `vim.opt.cmdheight = 0` — raise it, check `:messages`, or route them through a notifier like [snacks.notifier](https://github.com/folke/snacks.nvim), [nvim-notify](https://github.com/rcarriga/nvim-notify), or [noice.nvim](https://github.com/folke/noice.nvim). Also see [noice.nvim with vim.pack](#noicenvim-with-vimpack) for compatibility notes
- **same plugin, two owners**: specs deriving the same name (e.g. `williamboman/mason.nvim` in a `dependencies` list and `mason-org/mason.nvim` at top level) are folded into one plugin; a `dev = true` fragment wins, then an explicit `src`/`url`/`dir`, then a top-level spec over a dependency, and zpack warns unless the specs name a repo in common (a shared `[1]`, or a `src`/`url` equal to the other's `[1]`). Fields merge as usual, so `enabled = false` on either fragment disables the plugin. Point both at the current owner, or set `name` on one to keep them apart
- **`vim.NIL` in opts**: as in lazy.nvim, a key set to `vim.NIL` is removed rather than passed to `setup()` wherever specs merge, which includes every top-level key of table opts. A table that replaces the earlier value (nothing, a scalar or a list was there) keeps its `vim.NIL`, and so do opts a function returns or edits. To hand a plugin a JSON `null` (e.g. in LSP settings), nest it in a table no earlier spec sets, or set it from function-form opts

## Compatibility Notes

Expand Down
33 changes: 30 additions & 3 deletions lua/zpack/merge.lua
Original file line number Diff line number Diff line change
Expand Up @@ -349,16 +349,43 @@ local function concat(tables)
return list
end

---vim.tbl_deep_extend's test for a table it merges into rather than replaces.
local function can_merge(v)
return type(v) == "table" and (vim.tbl_isempty(v) or not vim.islist(v))
end

---Remove the keys `value` sets to vim.NIL wherever vim.tbl_deep_extend
---merged `value` into `base`, as lazy.nvim's merge does. `result` is that
---merge, so every table written to is one vim.tbl_deep_extend made, never
---a spec's own.
---@param result table
---@param base any
---@param value any
local function clear_nil(result, base, value)
if not (can_merge(base) and can_merge(value)) then
return
end
for k, v in pairs(value) do
if rawequal(v, vim.NIL) then
result[k] = nil
else
clear_nil(result[k], rawget(base, k), v)
end
end
end

---vim.tbl_deep_extend("force", base, value), then concatenate the lists at
---each `opts_extend` path (lazy.nvim's `opts_extend`). Empty tables count
---as unset, two dicts stay deep-merged, and a path with a scalar or object
---on the incoming side keeps what vim.tbl_deep_extend made of it.
---each `opts_extend` path (lazy.nvim's `opts_extend`). A key set to
---vim.NIL is removed where the merge reaches it. Empty tables count as
---unset, two dicts stay deep-merged, and a path with a scalar or object on
---the incoming side keeps what vim.tbl_deep_extend made of it.
---@param base table
---@param value table
---@param paths string[][]
---@return table
local function merge_opts(base, value, paths)
local result = vim.tbl_deep_extend("force", base, value)
clear_nil(result, base, value)
for _, path in ipairs(paths) do
local v = vim.tbl_get(value, unpack(path))
if is_plain(v) then
Expand Down
65 changes: 65 additions & 0 deletions tests/merge_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,71 @@ describe("Merge Module Unit Tests", function()
assert.are.same({ list = { 'a' } }, resolve(specs))
end)

it("resolve_opts clears a key set to vim.NIL", function()
local specs = {
{ opts_extend = { 'list' }, opts = { a = { x = 1 }, nested = { b = 1, c = 2 }, list = { 1 }, keep = 1 } },
{ opts = { a = vim.NIL, nested = { b = vim.NIL }, list = vim.NIL, gone = vim.NIL } },
}

assert.are.same({ nested = { c = 2 }, keep = 1 }, resolve(specs))
end)

it("resolve_opts clears a top-level vim.NIL key when the plugin has one spec", function()
assert.are.same({ b = 1 }, resolve({ { opts = { a = vim.NIL, b = 1 } } }))
end)

it("resolve_opts lets a later spec set a key an earlier one cleared with vim.NIL", function()
local result = resolve({ { opts = { a = { x = 1 } } }, { opts = { a = vim.NIL } }, { opts = { a = { y = 2 } } } })

assert.are.same({ y = 2 }, result.a)
end)

it("resolve_opts keeps vim.NIL where lazy.nvim's merge does", function()
local specs = {
{ opts = { new = { x = vim.NIL }, list = { vim.NIL }, replaced = { 1, 2 }, scalar = 's' } },
{ opts = { replaced = { x = vim.NIL }, scalar = { x = vim.NIL } } },
{ opts = function(_, opts) opts.fn = { y = vim.NIL } end },
}
local result = resolve(specs)

assert.are.equal(vim.NIL, result.new.x)
assert.are.equal(vim.NIL, result.list[1])
assert.are.equal(vim.NIL, result.replaced.x)
assert.are.equal(vim.NIL, result.scalar.x)
assert.are.equal(vim.NIL, result.fn.y)
assert.are.same({ 'a', vim.NIL, 'b' }, resolve({ { opts = { 'a', vim.NIL, 'b' } } }))
assert.are.equal(vim.NIL, resolve({ { opts = function() return { 1 } end }, { opts = { x = vim.NIL } } }).x)
end)

it("resolve_opts clears vim.NIL without editing a spec's opts", function()
local first = { a = { x = 1, y = 2 } }
local second = { a = { x = vim.NIL } }
local specs = { { opts = first }, { opts = second } }

assert.are.same({ a = { y = 2 } }, resolve(specs))
assert.are.same({ a = { y = 2 } }, resolve(specs))
assert.are.same({ a = { x = 1, y = 2 } }, first)
assert.are.equal(vim.NIL, second.a.x)
end)

it("resolve_opts clears vim.NIL in a dict at an opts_extend key", function()
local specs = {
{ opts_extend = { 'cfg' }, opts = { cfg = { a = 1, b = 2 } } },
{ opts = { cfg = { a = vim.NIL } } },
}

assert.are.same({ b = 2 }, resolve(specs).cfg)
end)

local has_ffi, ffi = pcall(require, 'ffi')
;(has_ffi and it or pending)("resolve_opts does not call an opts value's __eq when checking for vim.NIL", function()
local T = ffi.metatype(ffi.typeof('struct { int v; }'), { __eq = function() error('__eq called') end })
local c = T(1)
local result = resolve({ { opts = { a = { c = 1 } } }, { opts = { a = { c = c }, d = c } } })

assert.is_true(rawequal(c, result.a.c) and rawequal(c, result.d))
end)

it("merge_spec_array unions opts_extend across specs", function()
local merge = require('zpack.merge')

Expand Down
Loading
0