snacks.nvim/lua/snacks/picker/config/init.lua

355 lines
10 KiB
Lua

---@class snacks.picker.config
local M = {}
--- Source aliases
M.alias = {
live_grep = "grep",
find_files = "files",
git_commits = "git_log",
git_bcommits = "git_log_file",
oldfiles = "recent",
}
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)
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
local norm = Snacks.util.normkey(key)
if key ~= norm then
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 = 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 {}
---@type snacks.picker.Config[]
local todo = {
vim.deepcopy(defaults),
vim.deepcopy(user),
vim.deepcopy(source),
opts,
}
-- Merge the confirm action into the actions table
for _, t in ipairs(todo) do
if t.confirm then
t.actions = t.actions or {}
t.actions.confirm = t.confirm
end
end
-- Merge the configs
opts = Snacks.config.merge(unpack(todo))
if opts.cwd == true or opts.cwd == "" then
opts.cwd = nil
elseif opts.cwd then
opts.cwd = svim.fs.normalize(vim.fn.fnamemodify(opts.cwd, ":p"))
end
for _, t in ipairs(todo) do
if t.config then
opts = t.config(opts) or opts
end
end
-- add hl groups and actions for toggles
opts.actions = opts.actions or {}
for name in pairs(opts.toggles) do
local hl = table.concat(vim.tbl_map(function(a)
return a:sub(1, 1):upper() .. a:sub(2)
end, vim.split(name, "_")))
Snacks.util.set_hl({ [hl] = "SnacksPickerToggle" }, { default = true, prefix = "SnacksPickerToggle" })
opts.actions["toggle_" .. name] = function(picker)
picker.opts[name] = not picker.opts[name]
picker.list:set_target()
picker:find()
end
end
M.fix_old(opts)
M.multi(opts)
return opts
end
--- Fixes old config options
---@param opts snacks.picker.Config
function M.fix_old(opts)
if opts.previewers.diff.native ~= nil then
opts.previewers.diff.builtin = not opts.previewers.diff.native
end
if opts.previewers.git.native ~= nil then
opts.previewers.git.builtin = not opts.previewers.git.native
end
end
---@param opts snacks.picker.Config
function M.multi(opts)
if not opts.multi then
return opts
end
local Finder = require("snacks.picker.core.finder")
local finders = {} ---@type snacks.picker.finder[]
local formats = {} ---@type snacks.picker.format[]
local previews = {} ---@type snacks.picker.preview[]
local confirms = {} ---@type snacks.picker.Action.spec[]
local sources = {} ---@type snacks.picker.Config[]
for _, source in ipairs(opts.multi) do
if type(source) == "string" then
source = { source = source }
end
---@cast source snacks.picker.Config
source = Snacks.config.merge({}, opts.sources[source.source], source) --[[@as snacks.picker.Config]]
source.actions = source.actions or {}
if source.confirm then
source.actions.confirm = source.confirm
end
local finder = M.finder(source.finder)
finders[#finders + 1] = function(fopts, ctx)
fopts = Snacks.config.merge({}, vim.deepcopy(source), fopts)
-- Update source filter when needed
if not vim.tbl_isempty(fopts.filter or {}) then
ctx = setmetatable({}, { __index = ctx })
ctx.filter = ctx.filter:clone():init(fopts)
end
return finder(fopts, ctx)
end
confirms[#confirms + 1] = source.actions.confirm or "jump"
previews[#previews + 1] = M.preview(source)
formats[#formats + 1] = M.format(source)
sources[#sources + 1] = source
-- merge keys
for w, win in pairs(source.win or {}) do
if win.keys then
opts.win = opts.win or {}
opts.win[w] = opts.win[w] or {}
opts.win[w].keys = Snacks.config.merge(opts.win[w].keys or {}, win.keys)
end
end
end
opts.finder = opts.finder or Finder.multi(finders)
opts.format = opts.format or function(item, picker)
return formats[item.source_id](item, picker)
end
opts.preview = opts.preview or function(ctx)
return previews[ctx.item.source_id](ctx)
end
opts.confirm = opts.confirm
or function(picker, item, action)
return confirms[item.source_id](picker, item, action)
end
end
---@param opts snacks.picker.Config
function M.format(opts)
local ret = type(opts.format) == "string" and (Snacks.picker.format[opts.format] or M.field(opts.format))
or opts.format
or Snacks.picker.format.file
---@cast ret snacks.picker.format
return ret
end
---@param opts snacks.picker.Config
function M.transform(opts)
local ret = type(opts.transform) == "string" and require("snacks.picker.transform")[opts.transform]
or opts.transform
or nil
---@cast ret snacks.picker.transform?
return ret
end
---@param opts snacks.picker.Config
function M.preview(opts)
local preview = opts.preview or Snacks.picker.preview.file
preview = type(preview) == "string" and (Snacks.picker.preview[preview] or M.field(preview)) or preview
---@cast preview snacks.picker.preview
return preview
end
---@param opts snacks.picker.Config
function M.sort(opts)
local sort = opts.sort or require("snacks.picker.sort").default()
sort = type(sort) == "table" and require("snacks.picker.sort").default(sort) or sort
---@cast sort snacks.picker.sort
return sort
end
--- Resolve the layout configuration
---@param opts snacks.picker.Config|string
function M.layout(opts)
if type(opts) == "string" then
opts = M.get({ layout = { preset = opts } })
end
-- 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
-- only resolve presets when the layout has no layout
if not (layout.layout and layout.layout[1]) then
-- Resolve the preset
local layouts = opts.layouts or M.get().layouts or {}
local done = {} ---@type table<string, boolean>
local todo = { layout } ---@type snacks.picker.layout.Config[]
while true do
local preset = M.resolve(todo[1].preset or "custom", opts.source)
if not preset or done[preset] or not layouts[preset] then
break
end
done[preset] = true
table.insert(todo, 1, vim.deepcopy(layouts[preset]))
end
-- Merge and return the layout
layout = Snacks.config.merge(unpack(todo)) --[[@as snacks.picker.layout.Config]]
end
-- Fix deprecated layout options
layout.hidden = layout.hidden or {}
if layout.preview == false then
table.insert(layout.hidden, "preview")
layout.preview = nil
elseif type(layout.preview) == "table" then
---@cast layout snacks.picker.layout.Config|{preview: {enabled: boolean, main: boolean}}
if layout.preview.enabled == false then
table.insert(layout.hidden, "preview")
end
if layout.preview.main then
layout.preview = "main"
else
layout.preview = nil
end
end
return layout
end
---@generic T
---@generic A
---@param v (fun(...:A):T)|unknown
---@param ... A
---@return T
function M.resolve(v, ...)
return type(v) == "function" and v(...) or v
end
--- Get the finder
---@param finder string|snacks.picker.finder|snacks.picker.finder.multi
---@return snacks.picker.finder
function M.finder(finder)
local nop = function()
Snacks.notify.error("Finder not found:\n```lua\n" .. vim.inspect(finder) .. "\n```", { title = "Snacks Picker" })
end
if not finder or type(finder) == "function" then
return finder
end
if type(finder) == "table" then
---@cast finder snacks.picker.finder.multi
---@type snacks.picker.finder[]
local finders = vim.tbl_map(function(f)
return M.finder(f)
end, finder)
return require("snacks.picker.core.finder").multi(finders)
end
---@cast finder string
return M.field(finder) or nop
end
--- Resolves a module field
---@param spec string
function M.field(spec)
local parts = vim.split(spec, ".", { plain = true })
local name, field = parts[#parts]:match("^(.-)[_#](.+)$")
if name and field then
parts[#parts] = name
else
field = parts[#parts]
end
local ok, ret = pcall(function()
return require("snacks.picker.source." .. table.concat(parts, "."))[field]
end)
return ok and ret or nil
end
local did_setup = false
function M.setup()
if did_setup then
return
end
did_setup = true
require("snacks.picker.config.highlights")
for source in pairs(Snacks.picker.config.get().sources) do
M.wrap(source)
end
--- Automatically wrap new sources added after setup
setmetatable(require("snacks.picker.config.sources"), {
__newindex = function(t, k, v)
rawset(t, k, v)
M.wrap(k)
end,
})
end
---@param source string
---@param opts? {check?: boolean}
function M.wrap(source, opts)
if opts and opts.check then
local config = M.get()
if not config.sources[source] then
return
end
end
if rawget(Snacks.picker, source) then
return Snacks.picker[source]
end
---@type fun(opts: snacks.picker.Config): snacks.Picker
local ret = function(_opts)
return Snacks.picker.pick(source, _opts)
end
---@diagnostic disable-next-line: no-unknown
Snacks.picker[source] = ret
return ret
end
return M