mirror of
https://github.com/folke/snacks.nvim
synced 2025-07-07 21:25:11 +00:00
355 lines
10 KiB
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
|