fix(picker.config): better config merging and tests

This commit is contained in:
Folke Lemaitre 2025-01-17 20:36:33 +01:00
parent a194bbc374
commit 9986b47707
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040
5 changed files with 99 additions and 35 deletions

View file

@ -31,7 +31,11 @@ M.config = setmetatable({}, {
end,
})
local function can_merge(v)
local islist = vim.islist or vim.tbl_islist
local is_dict_like = function(v) -- has string and number keys
return type(v) == "table" and (vim.tbl_isempty(v) or not islist(v))
end
local is_dict = function(v) -- has only string keys
return type(v) == "table" and (vim.tbl_isempty(v) or not v[1])
end
@ -44,7 +48,7 @@ function M.config.merge(...)
local ret = select(1, ...)
for i = 2, select("#", ...) do
local value = select(i, ...)
if can_merge(ret) and can_merge(value) then
if is_dict_like(ret) and is_dict(value) then
for k, v in pairs(value) do
ret[k] = M.config.merge(ret[k], v)
end

View file

@ -75,7 +75,7 @@ local M = {}
--- Preset options
---@field previewers? snacks.picker.previewers.Config|{}
---@field formatters? snacks.picker.formatters.Config|{}
---@field sources? snacks.picker.sources.Config|{}
---@field sources? snacks.picker.sources.Config|{}|table<string, snacks.picker.Config|{}>
---@field layouts? table<string, snacks.picker.layout.Config>
--- Actions
---@field actions? table<string, snacks.picker.Action.spec> actions used by keymaps

View file

@ -11,45 +11,57 @@ M.alias = {
}
local key_cache = {} ---@type table<string, string>
local defaults ---@type snacks.picker.Config?
--- Fixes keys before merging configs for correctly resolving keymaps.
--- For example: <c-s> -> <C-S>
---@param opts? snacks.picker.Config
function M.fix_keys(opts)
if not (opts and opts.win) then
return
opts = opts or {}
-- fix keys in sources
for _, source in pairs(opts.sources or {}) do
M.fix_keys(source)
end
if not opts.win then
return opts
end
-- fix keys in wins
for _, win in pairs(opts.win) do
---@cast win snacks.win.Config
if win.keys then
local keys = vim.tbl_keys(win.keys) ---@type string[]
for _, key in ipairs(keys) do
key_cache[key] = key_cache[key] or vim.fn.keytrans(Snacks.util.keycode(key))
if key ~= key_cache[key] then
win.keys[key_cache[key]], win.keys[key] = win.keys[key], nil
local norm = key_cache[key]
if key ~= norm then
key_cache[norm] = norm
win.keys[norm], win.keys[key] = win.keys[key], nil
end
end
end
end
return opts
end
---@param opts? snacks.picker.Config
function M.get(opts)
M.setup()
opts = opts or {}
opts = M.fix_keys(opts)
local sources = require("snacks.picker.config.sources")
local defaults = require("snacks.picker.config.defaults").defaults
defaults.sources = sources
local user = Snacks.config.picker or {}
M.fix_keys(user)
M.fix_keys(defaults)
M.fix_keys(opts)
-- Setup defaults
if not defaults then
defaults = require("snacks.picker.config.defaults").defaults
defaults.sources = require("snacks.picker.config.sources")
defaults.layouts = require("snacks.picker.config.layouts")
M.fix_keys(defaults)
end
local user = M.fix_keys(Snacks.config.picker or {})
opts.source = M.alias[opts.source] or opts.source
-- Prepare config
local global = Snacks.config.get("picker", defaults, opts) -- defaults + global user config
local source = opts.source and global.sources[opts.source] or {}
M.fix_keys(source)
---@type snacks.picker.Config[]
local todo = {
vim.deepcopy(defaults),
@ -58,6 +70,7 @@ function M.get(opts)
opts,
}
-- Merge the confirm action into the actions table
for _, t in ipairs(todo) do
if t.confirm then
t.actions = t.actions or {}
@ -65,13 +78,8 @@ function M.get(opts)
end
end
local ret = Snacks.config.merge(unpack(todo))
ret.layouts = ret.layouts or {}
local layouts = require("snacks.picker.config.layouts")
for k, v in pairs(layouts or {}) do
ret.layouts[k] = ret.layouts[k] or v
end
return ret
-- Merge the configs
return Snacks.config.merge(unpack(todo))
end
--- Resolve the layout configuration
@ -80,19 +88,22 @@ function M.layout(opts)
if type(opts) == "string" then
opts = M.get({ layout = { preset = opts } })
end
local layouts = require("snacks.picker.config.layouts")
-- Resolve the layout configuration
local layout = M.resolve(opts.layout or {}, opts.source)
layout = type(layout) == "string" and { preset = layout } or layout
---@cast layout snacks.picker.layout.Config
if layout.layout and layout.layout[1] then
return layout
end
-- Resolve the preset
local preset = M.resolve(layout.preset or "custom", opts.source)
local ret = vim.deepcopy(opts.layouts and opts.layouts[preset] or layouts[preset] or {})
-- NOTE: use deep extend instead of merge to allow merging list-like tables
ret = vim.tbl_deep_extend("force", ret, layout)
ret.preset = nil
return ret
---@type snacks.picker.layout.Config
local ret = vim.deepcopy(opts.layouts and opts.layouts[preset] or {})
-- Merge and return the layout
return Snacks.config.merge(ret, layout)
end
---@generic T

View file

@ -24,11 +24,6 @@ function M.select(items, opts, on_choice)
local title = opts.prompt or "Select"
title = title:gsub("^%s*", ""):gsub("[%s:]*$", "")
local layout = Snacks.picker.config.layout("select")
layout.preview = false
layout.layout.height = math.floor(math.min(vim.o.lines * 0.8 - 10, #items + 2) + 0.5) + 10
layout.layout.title = " " .. title .. " "
layout.layout.title_pos = "center"
---@type snacks.picker.finder.Item[]
return Snacks.picker.pick({
@ -36,6 +31,15 @@ function M.select(items, opts, on_choice)
items = finder_items,
main = { current = true },
format = Snacks.picker.format.ui_select(opts.kind, #items),
layout = {
preset = "select",
preview = false,
layout = {
height = math.floor(math.min(vim.o.lines * 0.8 - 10, #items + 2) + 0.5) + 10,
title = " " .. title .. " ",
title_pos = "center",
},
},
actions = {
confirm = function(picker, item)
picker:close()
@ -44,7 +48,6 @@ function M.select(items, opts, on_choice)
end)
end,
},
layout = layout,
})
end

46
tests/config_spec.lua Normal file
View file

@ -0,0 +1,46 @@
---@module 'luassert'
local function d(v)
return vim.inspect(v):gsub("%s+", " ")
end
describe("config", function()
local tests = {
{
{ 1, 2 },
{ 3, 4 },
{ 3, 4 },
},
{
{ 1, 2 },
nil,
{ 1, 2 },
},
{
{ a = 1, b = 2 },
{ c = 3 },
{ a = 1, b = 2, c = 3 },
},
{
{ 1, 2, a = 1 },
{ 3, 4, b = 2 },
{ 3, 4, b = 2 },
},
{
{ 3, 4, b = 2 },
{ 1, 2 },
{ 1, 2 },
},
{
{ 1, 2, a = 1 },
{ b = 2 },
{ 1, 2, b = 2, a = 1 },
},
}
for _, t in ipairs(tests) do
it("merges correctly " .. d(t), function()
local ret = Snacks.config.merge(t[1], t[2])
assert.are.same(ret, t[3])
end)
end
end)