local M = {} ---@class snacks.picker.history.Config: snacks.picker.Config ---@field name string function M.commands() local commands = vim.api.nvim_get_commands({}) for k, v in pairs(vim.api.nvim_buf_get_commands(0, {})) do if type(k) == "string" then -- fixes vim.empty_dict() bug commands[k] = v end end for _, c in ipairs(vim.fn.getcompletion("", "command")) do if not commands[c] and c:find("^[a-z]") then commands[c] = { definition = "completion" } end end ---@async ---@param cb async fun(item: snacks.picker.finder.Item) return function(cb) ---@type string[] local names = vim.tbl_keys(commands) table.sort(names) for _, name in pairs(names) do local def = commands[name] cb({ text = name, desc = def.script_id and def.script_id < 0 and def.definition or nil, command = def, cmd = name, preview = { text = vim.inspect(def), ft = "lua", }, }) end end end ---@param opts snacks.picker.history.Config function M.history(opts) local count = vim.fn.histnr(opts.name) local items = {} for i = count, 1, -1 do local line = vim.fn.histget(opts.name, i) if not line:find("^%s*$") then table.insert(items, { text = line, cmd = line, preview = { text = line, ft = "text", }, }) end end return items end ---@param opts snacks.picker.marks.Config function M.marks(opts) local marks = {} ---@type vim.fn.getmarklist.ret.item[] if opts.global then vim.list_extend(marks, vim.fn.getmarklist()) end if opts["local"] then vim.list_extend(marks, vim.fn.getmarklist(vim.api.nvim_get_current_buf())) end ---@type snacks.picker.finder.Item[] local items = {} local bufname = vim.api.nvim_buf_get_name(0) for _, mark in ipairs(marks) do local file = mark.file or bufname local buf = mark.pos[1] and mark.pos[1] > 0 and mark.pos[1] or nil local line ---@type string? if buf and mark.pos[2] > 0 and vim.api.nvim_buf_is_valid(mark.pos[2]) then line = vim.api.nvim_buf_get_lines(buf, mark.pos[2] - 1, mark.pos[2], false)[1] end local label = mark.mark:sub(2, 2) items[#items + 1] = { text = table.concat({ label, file, line }, " "), label = label, line = line, buf = buf, file = file, pos = mark.pos[2] > 0 and { mark.pos[2], mark.pos[3] }, } end table.sort(items, function(a, b) return a.label < b.label end) return items end function M.jumps() local jumps = vim.fn.getjumplist()[1] local items = {} ---@type snacks.picker.finder.Item[] for _, jump in ipairs(jumps) do local buf = jump.bufnr and vim.api.nvim_buf_is_valid(jump.bufnr) and jump.bufnr or 0 local file = jump.filename or buf and vim.api.nvim_buf_get_name(buf) or nil if buf or file then local line ---@type string? if buf then line = vim.api.nvim_buf_get_lines(buf, jump.lnum - 1, jump.lnum, false)[1] end local label = tostring(#jumps - #items) table.insert(items, 1, { label = Snacks.picker.util.align(label, #tostring(#jumps), { align = "right" }), buf = buf, line = line, text = table.concat({ file, line }, " "), file = file, pos = jump.lnum and jump.lnum > 0 and { jump.lnum, jump.col } or nil, }) end end return items end function M.autocmds() local autocmds = vim.api.nvim_get_autocmds({}) local items = {} ---@type snacks.picker.finder.Item[] for _, au in ipairs(autocmds) do local item = au --[[@as snacks.picker.finder.Item]] item.text = Snacks.picker.util.text(item, { "event", "group_name", "pattern", "command" }) item.preview = { text = vim.inspect(au), ft = "lua", } item.item = au if au.callback then local info = debug.getinfo(au.callback, "S") if info.what == "Lua" then item.file = info.source:sub(2) item.pos = { info.linedefined, 0 } item.preview = "file" end end items[#items + 1] = item end return items end function M.highlights() local hls = vim.api.nvim_get_hl(0, {}) --[[@as table ]] local items = {} ---@type snacks.picker.finder.Item[] for group, hl in pairs(hls) do local defs = {} ---@type {group:string, hl:vim.api.keyset.get_hl_info}[] defs[#defs + 1] = { group = group, hl = hl } local link = hl.link local done = { [group] = true } ---@type table while link and not done[link] do done[link] = true local hl_link = hls[link] if hl_link then defs[#defs + 1] = { group = link, hl = hl_link } link = hl_link.link else break end end local code = {} ---@type string[] local extmarks = {} ---@type snacks.picker.Extmark[] local row = 1 for _, def in ipairs(defs) do for _, prop in ipairs({ "fg", "bg", "sp" }) do local v = def.hl[prop] if type(v) == "number" then def.hl[prop] = ("#%06X"):format(v) end end code[#code + 1] = ("%s = %s"):format(def.group, vim.inspect(def.hl)) extmarks[#extmarks + 1] = { row = row, col = 0, hl_group = def.group, end_col = #def.group } row = row + #vim.split(code[#code], "\n") + 1 end items[#items + 1] = { text = vim.inspect(defs):gsub("\n", " "), hl_group = group, preview = { text = table.concat(code, "\n\n"), ft = "lua", extmarks = extmarks, }, } end table.sort(items, function(a, b) return a.hl_group < b.hl_group end) return items end function M.colorschemes() local items = {} ---@type snacks.picker.finder.Item[] local rtp = vim.o.runtimepath if package.loaded.lazy then rtp = rtp .. "," .. table.concat(require("lazy.core.util").get_unloaded_rtp(""), ",") end local files = vim.fn.globpath(rtp, "colors/*", false, true) ---@type string[] for _, file in ipairs(files) do local name = vim.fn.fnamemodify(file, ":t:r") local ext = vim.fn.fnamemodify(file, ":e") if ext == "vim" or ext == "lua" then items[#items + 1] = { text = name, file = file, } end end return items end ---@param opts snacks.picker.keymaps.Config function M.keymaps(opts) local items = {} ---@type snacks.picker.finder.Item[] local maps = {} ---@type vim.api.keyset.get_keymap[] for _, mode in ipairs(opts.modes) do if opts.global then vim.list_extend(maps, vim.api.nvim_get_keymap(mode)) end if opts["local"] then vim.list_extend(maps, vim.api.nvim_buf_get_keymap(0, mode)) end end local done = {} ---@type table for _, km in ipairs(maps) do local key = Snacks.picker.util.text(km, { "mode", "lhs", "buffer" }) local keep = true if opts.plugs == false and km.lhs:match("^") then keep = false end if keep and not done[key] then done[key] = true local item = { mode = km.mode, item = km, key = km.lhs, preview = { text = vim.inspect(km), ft = "lua", }, } if km.callback then local info = debug.getinfo(km.callback, "S") if info.what == "Lua" then item.file = info.source:sub(2) item.pos = { info.linedefined, 0 } item.preview = "file" end end item.text = Snacks.util.normkey(km.lhs) .. " " .. Snacks.picker.util.text(km, { "mode", "lhs", "rhs", "desc" }) .. (item.file or "") items[#items + 1] = item end end return items end function M.registers() local registers = '*+"-:.%/#=_abcdefghijklmnopqrstuvwxyz0123456789' local items = {} ---@type snacks.picker.finder.Item[] local is_osc52 = vim.g.clipboard and vim.g.clipboard.name == "OSC 52" local has_clipboard = vim.g.loaded_clipboard_provider == 2 for i = 1, #registers, 1 do local reg = registers:sub(i, i) local value = "" if is_osc52 and reg:match("[%+%*]") then value = "OSC 52 detected, register not checked to maintain compatibility" elseif has_clipboard or not reg:match("[%+%*]") then local ok, reg_value = pcall(vim.fn.getreg, reg, 1) value = (ok and reg_value or "") --[[@as string]] end if value ~= "" then table.insert(items, { text = ("%s: %s"):format(reg, value:gsub("\n", "\\n"):gsub("\r", "\\r")), reg = reg, label = reg, data = value, value = value:gsub("\n", "\\n"):gsub("\r", "\\r"), preview = { text = value, ft = "text", }, }) end end return items end function M.spelling() local buf = vim.api.nvim_get_current_buf() local win = vim.api.nvim_get_current_win() local cursor = vim.api.nvim_win_get_cursor(0) local line = vim.api.nvim_buf_get_lines(buf, cursor[1] - 1, cursor[1], false)[1] -- get a misspelled word from under the cursor, if not found, then use the cursor_word instead local bad = vim.fn.spellbadword() ---@type string[] local word = bad[1] == "" and vim.fn.expand("") or bad[1] local suggestions = vim.fn.spellsuggest(word, 25, bad[2] == "caps") local items = {} ---@type snacks.picker.finder.Item[] for _, label in ipairs(suggestions) do table.insert(items, { text = label, action = function() -- skip whitespace local col = cursor[2] + 1 while line:sub(col, col):match("%s") and col < #line do col = col + 1 vim.api.nvim_win_set_cursor(win, { cursor[1], col - 1 }) end vim.cmd('normal! "_ciw' .. label) end, }) end return items end ---@param opts snacks.picker.undo.Config ---@type snacks.picker.finder function M.undo(opts, ctx) local tree = vim.fn.undotree() local buf = vim.api.nvim_get_current_buf() local file = vim.api.nvim_buf_get_name(buf) local items = {} ---@type snacks.picker.finder.Item[] -- Copy the current buffer to a temporary file and load the undo history. -- This is done to prevent the current buffer from being modified, -- and is way better for performance, since LSP change tracking won't be triggered local tmp_file = vim.fn.stdpath("cache") .. "/snacks-undo" local tmp_undo = tmp_file .. ".undo" local tmpbuf = vim.fn.bufadd(tmp_file) vim.bo[tmpbuf].swapfile = false vim.fn.writefile(vim.api.nvim_buf_get_lines(buf, 0, -1, false), tmp_file) vim.fn.bufload(tmpbuf) vim.api.nvim_buf_call(buf, function() vim.cmd("silent wundo! " .. tmp_undo) end) vim.api.nvim_buf_call(tmpbuf, function() pcall(vim.cmd, "silent rundo " .. tmp_undo) end) ---@param item snacks.picker.finder.Item local function resolve(item) local entry = item.item ---@type vim.fn.undotree.entry ---@type string[], string[] local before, after = {}, {} local ei = vim.o.eventignore vim.o.eventignore = "all" vim.api.nvim_buf_call(tmpbuf, function() -- state after the undo vim.cmd("noautocmd silent undo " .. entry.seq) after = vim.api.nvim_buf_get_lines(tmpbuf, 0, -1, false) -- state before the undo vim.cmd("noautocmd silent undo") before = vim.api.nvim_buf_get_lines(tmpbuf, 0, -1, false) end) vim.o.eventignore = ei local diff = vim.diff(table.concat(before, "\n") .. "\n", table.concat(after, "\n") .. "\n", opts.diff) --[[@as string]] local changes = {} ---@type string[] local added_lines = {} ---@type string[] local removed_lines = {} ---@type string[] for _, line in ipairs(vim.split(diff, "\n")) do if line:sub(1, 1) == "+" then changes[#changes + 1] = line:sub(2) added_lines[#added_lines + 1] = line:sub(2) elseif line:sub(1, 1) == "-" then changes[#changes + 1] = line:sub(2) removed_lines[#removed_lines + 1] = line:sub(2) end end diff = Snacks.picker.util.tpl( "diff --git a/{file} b/{file}\n--- {file}\n+++ {file}\n{diff}", { file = vim.fn.fnamemodify(file, ":."), diff = diff } ) item.text = table.concat(changes, " ") item.data = table.concat(added_lines, "\n") item.added_lines = table.concat(added_lines, "\n") item.removed_lines = table.concat(removed_lines, "\n") item.added = #added_lines item.removed = #removed_lines item.diff = diff end ---@param entries? vim.fn.undotree.entry[] ---@param parent? snacks.picker.finder.Item local function add(entries, parent) entries = entries or {} table.sort(entries, function(a, b) return a.seq < b.seq end) local last ---@type snacks.picker.finder.Item? for e, entry in ipairs(entries) do add(entry.alt, last or parent) local item = { seq = entry.seq, buf = buf, resolve = resolve, file = file, item = entry, current = entry.seq == tree.seq_cur, parent = parent, last = e == #entries, action = function() vim.api.nvim_buf_call(buf, function() vim.cmd("undo " .. entry.seq) end) end, } items[#items + 1] = item last = item end end add(tree.entries) -- Resolve the items in batches to prevent blocking the UI ---@param cb async fun(item: snacks.picker.finder.Item) ---@async return function(cb) for i = #items, 1, -1 do cb(items[i]) end while #items > 0 do vim.schedule(function() local count = 0 while #items > 0 and count < 5 do count = count + 1 local item = table.remove(items, 1) Snacks.picker.util.resolve(item) end ctx.async:resume() end) ctx.async:suspend() end vim.schedule(function() vim.api.nvim_buf_delete(tmpbuf, { force = true }) end) end end return M