From 9986b47707bbe76cf3b901c3048e55b2ba2bb4a8 Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Fri, 17 Jan 2025 20:36:33 +0100 Subject: [PATCH] fix(picker.config): better config merging and tests --- lua/snacks/init.lua | 8 +++- lua/snacks/picker/config/defaults.lua | 2 +- lua/snacks/picker/config/init.lua | 63 ++++++++++++++++----------- lua/snacks/picker/select.lua | 15 ++++--- tests/config_spec.lua | 46 +++++++++++++++++++ 5 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 tests/config_spec.lua diff --git a/lua/snacks/init.lua b/lua/snacks/init.lua index e9ffeed2..3391cb2d 100644 --- a/lua/snacks/init.lua +++ b/lua/snacks/init.lua @@ -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 diff --git a/lua/snacks/picker/config/defaults.lua b/lua/snacks/picker/config/defaults.lua index c4b6318e..103ae466 100644 --- a/lua/snacks/picker/config/defaults.lua +++ b/lua/snacks/picker/config/defaults.lua @@ -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 ---@field layouts? table --- Actions ---@field actions? table actions used by keymaps diff --git a/lua/snacks/picker/config/init.lua b/lua/snacks/picker/config/init.lua index 8ed6f079..b6a509f8 100644 --- a/lua/snacks/picker/config/init.lua +++ b/lua/snacks/picker/config/init.lua @@ -11,45 +11,57 @@ M.alias = { } local key_cache = {} ---@type table +local defaults ---@type snacks.picker.Config? --- Fixes keys before merging configs for correctly resolving keymaps. --- For example: -> ---@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 diff --git a/lua/snacks/picker/select.lua b/lua/snacks/picker/select.lua index 5292b58c..ff1efd02 100644 --- a/lua/snacks/picker/select.lua +++ b/lua/snacks/picker/select.lua @@ -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 diff --git a/tests/config_spec.lua b/tests/config_spec.lua new file mode 100644 index 00000000..23f13970 --- /dev/null +++ b/tests/config_spec.lua @@ -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)