mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-03 18:28:38 +00:00
415 lines
13 KiB
Lua
415 lines
13 KiB
Lua
---@class snacks.picker.formatters
|
|
---@field [string] snacks.picker.format
|
|
local M = {}
|
|
|
|
function M.severity(item, picker)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
local severity = item.severity
|
|
severity = type(severity) == "number" and vim.diagnostic.severity[severity] or severity
|
|
if not severity or type(severity) == "number" then
|
|
return ret
|
|
end
|
|
---@cast severity string
|
|
local lower = severity:lower()
|
|
local cap = severity:sub(1, 1):upper() .. lower:sub(2)
|
|
|
|
ret[#ret + 1] = { picker.opts.icons.diagnostics[cap], "Diagnostic" .. cap, virtual = true }
|
|
ret[#ret + 1] = { " ", virtual = true }
|
|
return ret
|
|
end
|
|
|
|
---@param item snacks.picker.Item
|
|
function M.filename(item, picker)
|
|
---@type snacks.picker.Highlight[]
|
|
local ret = {}
|
|
if not item.file then
|
|
return ret
|
|
end
|
|
local path = vim.fn.fnamemodify(item.file, ":~:.")
|
|
path = vim.fs.normalize(path)
|
|
local name, cat = path, "file"
|
|
if item.buf and vim.api.nvim_buf_is_loaded(item.buf) then
|
|
name = vim.bo[item.buf].filetype
|
|
cat = "filetype"
|
|
elseif item.dir then
|
|
cat = "directory"
|
|
end
|
|
|
|
if picker.opts.icons.files.enabled ~= false then
|
|
local icon, hl = Snacks.util.icon(name, cat)
|
|
ret[#ret + 1] = { icon .. " ", hl, virtual = true }
|
|
end
|
|
|
|
local dir, file = path:match("^(.*)/(.+)$")
|
|
if file and dir then
|
|
if picker.opts.formatters.file.filename_first then
|
|
ret[#ret + 1] = { file, "SnacksPickerFile", field = "file" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { dir, "SnacksPickerDir", field = "file" }
|
|
else
|
|
ret[#ret + 1] = { dir .. "/", "SnacksPickerDir", field = "file" }
|
|
ret[#ret + 1] = { file, "SnacksPickerFile", field = "file" }
|
|
end
|
|
else
|
|
ret[#ret + 1] = { path, "SnacksPickerFile", field = "file" }
|
|
end
|
|
if item.pos then
|
|
ret[#ret + 1] = { ":", "SnacksPickerDelim" }
|
|
ret[#ret + 1] = { tostring(item.pos[1]), "SnacksPickerRow" }
|
|
if item.pos[2] > 0 then
|
|
ret[#ret + 1] = { ":", "SnacksPickerDelim" }
|
|
ret[#ret + 1] = { tostring(item.pos[2]), "SnacksPickerCol" }
|
|
end
|
|
end
|
|
ret[#ret + 1] = { " " }
|
|
return ret
|
|
end
|
|
|
|
function M.file(item, picker)
|
|
---@type snacks.picker.Highlight[]
|
|
local ret = {}
|
|
|
|
if item.label then
|
|
ret[#ret + 1] = { item.label, "SnacksPickerLabel" }
|
|
ret[#ret + 1] = { " ", virtual = true }
|
|
end
|
|
|
|
if item.severity then
|
|
vim.list_extend(ret, M.severity(item, picker))
|
|
end
|
|
|
|
vim.list_extend(ret, M.filename(item, picker))
|
|
|
|
if item.comment then
|
|
table.insert(ret, { item.comment, "SnacksPickerComment" })
|
|
table.insert(ret, { " " })
|
|
end
|
|
|
|
if item.line then
|
|
Snacks.picker.highlight.format(item, item.line, ret)
|
|
table.insert(ret, { " " })
|
|
end
|
|
return ret
|
|
end
|
|
|
|
function M.git_log(item, picker)
|
|
local a = Snacks.picker.util.align
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
ret[#ret + 1] = { picker.opts.icons.git.commit, "SnacksPickerGitCommit" }
|
|
ret[#ret + 1] = { item.commit, "SnacksPickerGitCommit" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { a(item.date, 16), "SnacksPickerGitDate" }
|
|
|
|
local msg = item.msg ---@type string
|
|
local type, scope, breaking, body = msg:match("^(%S+)(%(.-%))(!?):%s*(.*)$")
|
|
if not type then
|
|
type, breaking, body = msg:match("^(%S+)(!?):%s*(.*)$")
|
|
end
|
|
local msg_hl = "SnacksPickerGitMsg"
|
|
if type and body then
|
|
local dimmed = vim.tbl_contains({ "chore", "bot", "build", "ci", "style", "test" }, type)
|
|
msg_hl = dimmed and "SnacksPickerDimmed" or "SnacksPickerGitMsg"
|
|
ret[#ret + 1] =
|
|
{ type, breaking ~= "" and "SnacksPickerGitBreaking" or dimmed and "SnacksPickerBold" or "SnacksPickerGitType" }
|
|
if scope and scope ~= "" then
|
|
ret[#ret + 1] = { scope, "SnacksPickerGitScope" }
|
|
end
|
|
if breaking ~= "" then
|
|
ret[#ret + 1] = { "!", "SnacksPickerGitBreaking" }
|
|
end
|
|
ret[#ret + 1] = { ":", "SnacksPickerDelim" }
|
|
ret[#ret + 1] = { " " }
|
|
msg = body
|
|
end
|
|
ret[#ret + 1] = { msg, msg_hl }
|
|
Snacks.picker.highlight.markdown(ret)
|
|
Snacks.picker.highlight.highlight(ret, {
|
|
["#%d+"] = "SnacksPickerGitIssue",
|
|
})
|
|
return ret
|
|
end
|
|
|
|
function M.lsp_symbol(item, picker)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
if item.hierarchy then
|
|
local indents = picker.opts.icons.indent
|
|
local indent = {} ---@type string[]
|
|
local node = item
|
|
while node and node.depth > 0 do
|
|
local is_last, icon = node.last, ""
|
|
if node ~= item then
|
|
icon = is_last and " " or indents.vertical
|
|
else
|
|
icon = is_last and indents.last or indents.middle
|
|
end
|
|
table.insert(indent, 1, icon)
|
|
node = node.parent
|
|
end
|
|
ret[#ret + 1] = { table.concat(indent), "SnacksPickerIndent" }
|
|
end
|
|
local kind = item.kind or "Unknown" ---@type string
|
|
local kind_hl = "SnacksPickerIcon" .. kind
|
|
ret[#ret + 1] = { picker.opts.icons.kinds[kind], kind_hl }
|
|
ret[#ret + 1] = { " " }
|
|
-- ret[#ret + 1] = { kind:lower() .. string.rep(" ", 10 - #kind), kind_hl }
|
|
-- ret[#ret + 1] = { " " }
|
|
local name = vim.trim(item.name:gsub("\r?\n", " "))
|
|
name = name == "" and item.detail or name
|
|
Snacks.picker.highlight.format(item, name, ret)
|
|
-- ret[#ret + 1] = { name }
|
|
return ret
|
|
end
|
|
|
|
---@param kind? string
|
|
---@param count number
|
|
---@return snacks.picker.format
|
|
function M.ui_select(kind, count)
|
|
return function(item)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
local idx = tostring(item.idx)
|
|
idx = (" "):rep(#tostring(count) - #idx) .. idx
|
|
ret[#ret + 1] = { idx .. ".", "SnacksPickerIdx" }
|
|
ret[#ret + 1] = { " " }
|
|
|
|
if kind == "codeaction" then
|
|
---@type lsp.Command|lsp.CodeAction, lsp.HandlerContext
|
|
local action, ctx = item.item.action, item.item.ctx
|
|
local client = vim.lsp.get_client_by_id(ctx.client_id)
|
|
ret[#ret + 1] = { action.title }
|
|
if client then
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { ("[%s]"):format(client.name), "SnacksPickerSpecial" }
|
|
end
|
|
else
|
|
ret[#ret + 1] = { item.formatted }
|
|
end
|
|
return ret
|
|
end
|
|
end
|
|
|
|
function M.lines(item)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
local line_count = vim.api.nvim_buf_line_count(item.buf)
|
|
local idx = Snacks.picker.util.align(tostring(item.idx), #tostring(line_count), { align = "right" })
|
|
ret[#ret + 1] = { idx, "LineNr", virtual = true }
|
|
ret[#ret + 1] = { " ", virtual = true }
|
|
ret[#ret + 1] = { item.text }
|
|
|
|
local offset = #idx + 2
|
|
|
|
for _, extmark in ipairs(item.highlights or {}) do
|
|
extmark = vim.deepcopy(extmark)
|
|
if type(extmark[1]) ~= "string" then
|
|
---@cast extmark snacks.picker.Extmark
|
|
extmark.col = extmark.col + offset
|
|
if extmark.end_col then
|
|
extmark.end_col = extmark.end_col + offset
|
|
end
|
|
end
|
|
ret[#ret + 1] = extmark
|
|
end
|
|
return ret
|
|
end
|
|
|
|
function M.text(item)
|
|
return {
|
|
{ item.text },
|
|
}
|
|
end
|
|
|
|
function M.diagnostic(item, picker)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
local diag = item.item ---@type vim.Diagnostic
|
|
if item.severity then
|
|
vim.list_extend(ret, M.severity(item, picker))
|
|
end
|
|
|
|
local message = diag.message:gsub("\n", " ")
|
|
ret[#ret + 1] = { message }
|
|
Snacks.picker.highlight.markdown(ret)
|
|
ret[#ret + 1] = { " " }
|
|
|
|
if diag.source then
|
|
ret[#ret + 1] = { diag.source, "SnacksPickerDiagnosticSource" }
|
|
ret[#ret + 1] = { " " }
|
|
end
|
|
|
|
if diag.code then
|
|
ret[#ret + 1] = { ("(%s)"):format(diag.code), "SnacksPickerDiagnosticCode" }
|
|
ret[#ret + 1] = { " " }
|
|
end
|
|
vim.list_extend(ret, M.filename(item, picker))
|
|
return ret
|
|
end
|
|
|
|
function M.autocmd(item)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
---@type vim.api.keyset.get_autocmds.ret
|
|
local au = item.item
|
|
local a = Snacks.picker.util.align
|
|
ret[#ret + 1] = { a(au.event, 15), "SnacksPickerAuEvent" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { a(au.pattern, 10), "SnacksPickerAuPattern" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { a(tostring(au.group_name or ""), 15), "SnacksPickerAuGroup" }
|
|
ret[#ret + 1] = { " " }
|
|
if au.command ~= "" then
|
|
Snacks.picker.highlight.format(item, au.command, ret, { lang = "vim" })
|
|
else
|
|
ret[#ret + 1] = { "callback", "Function" }
|
|
end
|
|
return ret
|
|
end
|
|
|
|
function M.hl(item)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
ret[#ret + 1] = { item.hl_group, item.hl_group }
|
|
return ret
|
|
end
|
|
|
|
function M.man(item)
|
|
local a = Snacks.picker.util.align
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
ret[#ret + 1] = { a(item.page, 20), "SnacksPickerManPage" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { ("(%s)"):format(item.section), "SnacksPickerManSection" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { item.desc, "SnacksPickerManDesc" }
|
|
return ret
|
|
end
|
|
|
|
-- Pretty keymaps using which-key icons when available
|
|
function M.keymap(item, picker)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
---@type vim.api.keyset.get_keymap
|
|
local k = item.item
|
|
local a = Snacks.picker.util.align
|
|
|
|
if package.loaded["which-key"] then
|
|
local Icons = require("which-key.icons")
|
|
local icon, hl = Icons.get({ keymap = k, desc = k.desc })
|
|
if icon then
|
|
ret[#ret + 1] = { a(icon, 3), hl }
|
|
else
|
|
ret[#ret + 1] = { " " }
|
|
end
|
|
end
|
|
local lhs = vim.fn.keytrans(Snacks.util.keycode(k.lhs))
|
|
ret[#ret + 1] = { k.mode, "SnacksPickerKeymapMode" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { a(lhs, 15), "SnacksPickerKeymapLhs" }
|
|
ret[#ret + 1] = { " " }
|
|
local rhs_len = 0
|
|
if k.rhs and k.rhs ~= "" then
|
|
local rhs = k.rhs or ""
|
|
rhs_len = #rhs
|
|
local cmd = rhs:lower():find("<cmd>")
|
|
if cmd then
|
|
ret[#ret + 1] = { rhs:sub(1, cmd + 4), "NonText" }
|
|
rhs = rhs:sub(cmd + 5)
|
|
local cr = rhs:lower():find("<cr>$")
|
|
if cr then
|
|
rhs = rhs:sub(1, cr - 1)
|
|
end
|
|
Snacks.picker.highlight.format(item, rhs, ret, { lang = "vim" })
|
|
if cr then
|
|
ret[#ret + 1] = { "<CR>", "NonText" }
|
|
end
|
|
elseif rhs:lower():find("^<plug>") then
|
|
ret[#ret + 1] = { "<Plug>", "NonText" }
|
|
local plug = rhs:sub(7):gsub("^%(", ""):gsub("%)$", "")
|
|
ret[#ret + 1] = { "(", "SnacksPickerDelim" }
|
|
Snacks.picker.highlight.format(item, plug, ret, { lang = "vim" })
|
|
ret[#ret + 1] = { ")", "SnacksPickerDelim" }
|
|
elseif rhs:find("v:lua%.") then
|
|
ret[#ret + 1] = { "v:lua", "NonText" }
|
|
ret[#ret + 1] = { ".", "SnacksPickerDelim" }
|
|
Snacks.picker.highlight.format(item, rhs:sub(7), ret, { lang = "lua" })
|
|
else
|
|
ret[#ret + 1] = { k.rhs, "SnacksPickerKeymapRhs" }
|
|
end
|
|
else
|
|
ret[#ret + 1] = { "callback", "Function" }
|
|
rhs_len = 8
|
|
end
|
|
|
|
if rhs_len < 15 then
|
|
ret[#ret + 1] = { (" "):rep(15 - rhs_len) }
|
|
end
|
|
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { a(k.desc or "", 20) }
|
|
|
|
if item.file then
|
|
ret[#ret + 1] = { " " }
|
|
vim.list_extend(ret, M.filename(item, picker))
|
|
end
|
|
return ret
|
|
end
|
|
|
|
function M.git_status(item, picker)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
local a = Snacks.picker.util.align
|
|
local s = vim.trim(item.status):sub(1, 1)
|
|
local hls = {
|
|
["A"] = "SnacksPickerGitStatusAdded",
|
|
["M"] = "SnacksPickerGitStatusModified",
|
|
["D"] = "SnacksPickerGitStatusDeleted",
|
|
["R"] = "SnacksPickerGitStatusRenamed",
|
|
["C"] = "SnacksPickerGitStatusCopied",
|
|
["?"] = "SnacksPickerGitStatusUntracked",
|
|
}
|
|
local hl = hls[s] or "SnacksPickerGitStatus"
|
|
ret[#ret + 1] = { a(item.status, 2, { align = "right" }), hl }
|
|
ret[#ret + 1] = { " " }
|
|
vim.list_extend(ret, M.filename(item, picker))
|
|
return ret
|
|
end
|
|
|
|
function M.register(item)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { "[", "SnacksPickerDelim" }
|
|
ret[#ret + 1] = { item.reg, "SnacksPickerRegister" }
|
|
ret[#ret + 1] = { "]", "SnacksPickerDelim" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { item.value }
|
|
return ret
|
|
end
|
|
|
|
function M.buffer(item, picker)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
ret[#ret + 1] = { Snacks.picker.util.align(tostring(item.buf), 3), "SnacksPickerBufNr" }
|
|
ret[#ret + 1] = { " " }
|
|
ret[#ret + 1] = { Snacks.picker.util.align(item.flags, 2, { align = "right" }), "SnacksPickerBufFlags" }
|
|
ret[#ret + 1] = { " " }
|
|
vim.list_extend(ret, M.filename(item, picker))
|
|
return ret
|
|
end
|
|
|
|
function M.selected(item, picker)
|
|
local selected = picker.list:is_selected(item)
|
|
local icon = picker.opts.icons.ui.selected
|
|
local icon_width = vim.api.nvim_strwidth(icon)
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
ret[#ret + 1] = { selected and icon or string.rep(" ", icon_width), "SnacksPickerSelected", virtual = true }
|
|
return ret
|
|
end
|
|
|
|
function M.debug(item, picker)
|
|
local score = item.score
|
|
if not picker.matcher.sorting then
|
|
score = picker.matcher.DEFAULT_SCORE
|
|
if item.score_add then
|
|
score = score + item.score_add
|
|
end
|
|
if item.score_mul then
|
|
score = score * item.score_mul
|
|
end
|
|
end
|
|
local ret = {} ---@type snacks.picker.Highlight[]
|
|
ret[#ret + 1] = { ("%.2f "):format(score), "Number" }
|
|
return ret
|
|
end
|
|
|
|
return M
|