---@class snacks.picker.formatters ---@field [string] snacks.picker.format local M = {} local uv = vim.uv or vim.loop 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) if picker.opts.formatters.severity.pos == "right" then return { { col = 0, virt_text = { { picker.opts.icons.diagnostics[cap], "Diagnostic" .. cap } }, virt_text_pos = "right_align", hl_mode = "combine", }, } end if picker.opts.formatters.severity.icons then ret[#ret + 1] = { picker.opts.icons.diagnostics[cap], "Diagnostic" .. cap, virtual = true } ret[#ret + 1] = { " ", virtual = true } end if picker.opts.formatters.severity.level then ret[#ret + 1] = { lower:upper(), "Diagnostic" .. cap, virtual = true } ret[#ret + 1] = { " ", virtual = true } end 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 = Snacks.picker.util.path(item) or item.file path = Snacks.picker.util.truncpath(path, picker.opts.formatters.file.truncate or 40, { cwd = picker:cwd() }) 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, { fallback = picker.opts.icons.files, }) if item.dir and item.open then icon = picker.opts.icons.files.dir_open end icon = Snacks.picker.util.align(icon, picker.opts.formatters.file.icon_width or 2) ret[#ret + 1] = { icon, hl, virtual = true } end local base_hl = item.dir and "SnacksPickerDirectory" or "SnacksPickerFile" local function is(prop) local it = item while it do if it[prop] then return true end it = it.parent end end if is("ignored") then base_hl = "SnacksPickerPathIgnored" elseif is("hidden") then base_hl = "SnacksPickerPathHidden" elseif item.filename_hl then base_hl = item.filename_hl end local dir_hl = "SnacksPickerDir" if picker.opts.formatters.file.filename_only then path = vim.fn.fnamemodify(item.file, ":t") ret[#ret + 1] = { path, base_hl, field = "file" } else local dir, base = path:match("^(.*)/(.+)$") if base and dir then if picker.opts.formatters.file.filename_first then ret[#ret + 1] = { base, base_hl, field = "file" } ret[#ret + 1] = { " " } ret[#ret + 1] = { dir, dir_hl, field = "file" } else ret[#ret + 1] = { dir .. "/", dir_hl, field = "file" } ret[#ret + 1] = { base, base_hl, field = "file" } end else ret[#ret + 1] = { path, base_hl, field = "file" } end end if item.pos and item.pos[1] > 0 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] = { " " } if item.type == "link" then local real = uv.fs_realpath(item.file) local broken = not real real = real or uv.fs_readlink(item.file) if real then ret[#ret + 1] = { "-> ", "SnacksPickerDelim" } ret[#ret + 1] = { Snacks.picker.util.truncpath(real, 20), broken and "SnacksPickerLinkBroken" or "SnacksPickerLink" } ret[#ret + 1] = { " " } end end 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.parent then vim.list_extend(ret, M.tree(item, picker)) end if item.status then vim.list_extend(ret, M.file_git_status(item, picker)) 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" } local c = item.commit or item.branch or "HEAD" ret[#ret + 1] = { a(c, 8, { truncate = true }), "SnacksPickerGitCommit" } ret[#ret + 1] = { " " } if item.date then ret[#ret + 1] = { a(item.date, 16), "SnacksPickerGitDate" } end ret[#ret + 1] = { " " } local msg = item.msg ---@type string local type, scope, breaking, body = msg:match("^(%S+)%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.git_branch(item, picker) local a = Snacks.picker.util.align local ret = {} ---@type snacks.picker.Highlight[] if item.current then ret[#ret + 1] = { a("", 2), "SnacksPickerGitBranchCurrent" } else ret[#ret + 1] = { a("", 2) } end if item.detached then ret[#ret + 1] = { a("(detached HEAD)", 30, { truncate = true }), "SnacksPickerGitDetached" } else ret[#ret + 1] = { a(item.branch, 30, { truncate = true }), "SnacksPickerGitBranch" } end ret[#ret + 1] = { " " } local offset = Snacks.picker.highlight.offset(ret) local log = M.git_log(item, picker) Snacks.picker.highlight.fix_offset(log, offset) vim.list_extend(ret, log) return ret end function M.git_stash(item, picker) local a = Snacks.picker.util.align local ret = {} ---@type snacks.picker.Highlight[] ret[#ret + 1] = { a(item.stash, 10), "SnacksPickerIdx" } ret[#ret + 1] = { " " } ret[#ret + 1] = { a(item.branch, 10, { truncate = true }), "SnacksPickerGitBranch" } ret[#ret + 1] = { " " } local offset = Snacks.picker.highlight.offset(ret) local log = M.git_log(item, picker) Snacks.picker.highlight.fix_offset(log, offset) vim.list_extend(ret, log) return ret end function M.tree(item, picker) local ret = {} ---@type snacks.picker.Highlight[] local icons = picker.opts.icons.tree local indent = {} ---@type string[] local node = item while node and node.parent do local is_last, icon = node.last, "" if node ~= item then icon = is_last and " " or icons.vertical else icon = is_last and icons.last or icons.middle end table.insert(indent, 1, icon) node = node.parent end ret[#ret + 1] = { table.concat(indent), "SnacksPickerTree" } return ret end function M.undo(item, picker) local ret = {} ---@type snacks.picker.Highlight[] local entry = item.item ---@type vim.fn.undotree.entry local a = Snacks.picker.util.align if item.current then ret[#ret + 1] = { a("", 2), "SnacksPickerUndoCurrent" } else ret[#ret + 1] = { a("", 2) } end vim.list_extend(ret, M.tree(item, picker)) local w = vim.api.nvim_strwidth(ret[#ret][1]) ret[#ret + 1] = { tostring(entry.seq), "SnacksPickerIdx" } ret[#ret + 1] = { " " } ret[#ret + 1] = { a(" ", 8 - w - #tostring(entry.seq)) } ret[#ret + 1] = { a(Snacks.picker.util.reltime(entry.time), 15), "SnacksPickerTime" } ret[#ret + 1] = { " " } local function num(v, prefix) v = v or 0 return a((v and v > 0 and prefix .. v or ""), 4) end ret[#ret + 1] = { num(item.added, "+"), "SnacksPickerUndoAdded" } ret[#ret + 1] = { " " } ret[#ret + 1] = { num(item.removed, "-"), "SnacksPickerUndoRemoved" } if entry.save then ret[#ret + 1] = { " " } ret[#ret + 1] = { a(picker.opts.icons.undo.saved, 2), "SnacksPickerUndoSaved" } end return ret end function M.lsp_symbol(item, picker) local opts = picker.opts --[[@as snacks.picker.lsp.symbols.Config]] local ret = {} ---@type snacks.picker.Highlight[] if item.tree and not opts.workspace then vim.list_extend(ret, M.tree(item, picker)) 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] = { " " } local name = vim.trim(item.name:gsub("\r?\n", " ")) name = name == "" and item.detail or name Snacks.picker.highlight.format(item, name, ret) if opts.workspace then local offset = Snacks.picker.highlight.offset(ret, { char_idx = true }) ret[#ret + 1] = { Snacks.picker.util.align(" ", 40 - offset) } vim.list_extend(ret, M.filename(item, picker)) end 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, picker) local ret = {} ---@type snacks.picker.Highlight[] local ft = item.ft or picker.opts.formatters.text.ft if ft then Snacks.picker.highlight.format(item, item.text, ret, { lang = ft }) else ret[#ret + 1] = { item.text, item.text_hl } end return ret end function M.command(item) local ret = {} ---@type snacks.picker.Highlight[] ret[#ret + 1] = { item.cmd, "SnacksPickerCmd" .. (item.cmd:find("^[a-z]") and "Builtin" or "") } if item.desc then ret[#ret + 1] = { " " } ret[#ret + 1] = { item.desc, "SnacksPickerDesc" } end return ret 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 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 = Snacks.util.normkey(k.lhs) ret[#ret + 1] = { k.mode, "SnacksPickerKeymapMode" } ret[#ret + 1] = { " " } ret[#ret + 1] = { a(lhs, 15), "SnacksPickerKeymapLhs" } ret[#ret + 1] = { " " } local icon_nowait = picker.opts.icons.keymaps.nowait if k.nowait == 1 then ret[#ret + 1] = { icon_nowait, "SnacksPickerKeymapNowait" } else ret[#ret + 1] = { (" "):rep(vim.api.nvim_strwidth(icon_nowait)) } end ret[#ret + 1] = { " " } if k.buffer and k.buffer > 0 then ret[#ret + 1] = { a("buf:" .. k.buffer, 6), "SnacksPickerBufNr" } else ret[#ret + 1] = { a("", 6) } end 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("") if cmd then ret[#ret + 1] = { rhs:sub(1, cmd + 4), "NonText" } rhs = rhs:sub(cmd + 5) local cr = rhs:lower():find("$") 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] = { "", "NonText" } end elseif rhs:lower():find("^") then ret[#ret + 1] = { "", "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] = { " " } if item.rename then local file = item.file item.file = item.rename item._path = nil vim.list_extend(ret, M.filename(item, picker)) item.file = file item._path = nil ret[#ret + 1] = { "-> ", "SnacksPickerDelim" } ret[#ret + 1] = { " " } end vim.list_extend(ret, M.filename(item, picker)) return ret end function M.file_git_status(item, picker) local ret = {} ---@type snacks.picker.Highlight[] local status = require("snacks.picker.source.git").git_status(item.status) local hl = "SnacksPickerGitStatus" if status.unmerged then hl = "SnacksPickerGitStatusUnmerged" elseif status.staged then hl = "SnacksPickerGitStatusStaged" else hl = "SnacksPickerGitStatus" .. status.status:sub(1, 1):upper() .. status.status:sub(2) end if picker.opts.formatters.file.git_status_hl then item.filename_hl = hl end local icon = status.status:sub(1, 1):upper() icon = status.status == "untracked" and "?" or status.status == "ignored" and "!" or icon if picker.opts.icons.git.enabled then icon = picker.opts.icons.git[status.status] or icon --[[@as string]] if status.staged then icon = picker.opts.icons.git.staged end end ret[#ret + 1] = { col = 0, virt_text = { { icon, hl }, { " " } }, virt_text_pos = "right_align", hl_mode = "combine", } 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 a = Snacks.picker.util.align local selected = picker.opts.icons.ui.selected local unselected = picker.opts.icons.ui.unselected local width = math.max(vim.api.nvim_strwidth(selected), vim.api.nvim_strwidth(unselected)) local ret = {} ---@type snacks.picker.Highlight[] if picker.list:is_selected(item) then ret[#ret + 1] = { a(selected, width), "SnacksPickerSelected", virtual = true } elseif picker.opts.formatters.selected.unselected then ret[#ret + 1] = { a(unselected, width), "SnacksPickerUnselected", virtual = true } else ret[#ret + 1] = { a("", width) } end 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 function M.icon(item, picker) local a = Snacks.picker.util.align ---@cast item snacks.picker.Icon local ret = {} ---@type snacks.picker.Highlight[] ret[#ret + 1] = { a(item.icon, 2), "SnacksPickerIcon" } ret[#ret + 1] = { " " } ret[#ret + 1] = { a(item.source, 10), "SnacksPickerIconSource" } ret[#ret + 1] = { " " } ret[#ret + 1] = { a(item.name, 30), "SnacksPickerIconName" } ret[#ret + 1] = { " " } ret[#ret + 1] = { a(item.category, 8), "SnacksPickerIconCategory" } return ret end function M.notification(item, picker) local a = Snacks.picker.util.align local ret = {} ---@type snacks.picker.Highlight[] local notif = item.item ---@type snacks.notifier.Notif ret[#ret + 1] = { a(os.date("%R", notif.added), 5), "SnacksPickerTime" } ret[#ret + 1] = { " " } if item.severity then vim.list_extend(ret, M.severity(item, picker)) end ret[#ret + 1] = { " " } ret[#ret + 1] = { a(notif.title or "", 15), "SnacksNotifierHistoryTitle" } ret[#ret + 1] = { " " } ret[#ret + 1] = { notif.msg, "SnacksPickerNotificationMessage" } Snacks.picker.highlight.markdown(ret) -- ret[#ret + 1] = { " " } return ret end return M