snacks.nvim/lua/snacks/picker/source/vim.lua
Folke Lemaitre 559d6c6bf2
feat(snacks): added snacks.picker (#445)
## Description

More info coming tomorrow.

In short:
- very fast. pretty much realtime filtering/sorting in huge repos (like
1.7 million files)
- extensible
- easy to customize the layout (and lots of presets) with
`snacks.layout`
- simple to create custom pickers
- `vim.ui.select`
- lots of builtin pickers
- uses treesitter highlighting wherever it makes sense
- fast lua fuzzy matcher which supports the [fzf
syntax](https://junegunn.github.io/fzf/search-syntax/) and additionally
supports field filters, like `file:lua$ 'function`

There's no snacks picker command, just use lua.

```lua
-- all pickers
Snacks.picker()

-- run files picker
Snacks.picker.files(opts)
Snacks.picker.pick("files", opts)
Snacks.picker.pick({source = "files", ...})
```

<!-- Describe the big picture of your changes to communicate to the
maintainers
  why we should accept this pull request. -->

## Todo
- [x] issue with preview loc not always correct when scrolling fast in
list (probably due to `snacks.scroll`)
- [x] `grep` (`live_grep`) is sometimes too fast in large repos and can
impact ui rendering. Not very noticeable, but something I want to look
at.
- [x] docs
- [x] treesitter highlights are broken. Messed something up somewhere

## Related Issue(s)

<!--
  If this PR fixes any issues, please link to the issue here.
  - Fixes #<issue_number>
-->

## Screenshots

<!-- Add screenshots of the changes if applicable. -->
2025-01-14 22:53:59 +01:00

290 lines
8.8 KiB
Lua

local M = {}
---@class snacks.picker
---@field commands fun(opts?: snacks.picker.Config): snacks.Picker
---@field marks fun(opts?: snacks.picker.marks.Config): snacks.Picker
---@field jumps fun(opts?: snacks.picker.Config): snacks.Picker
---@field autocmds fun(opts?: snacks.picker.Config): snacks.Picker
---@field highlights fun(opts?: snacks.picker.Config): snacks.Picker
---@field colorschemes fun(opts?: snacks.picker.Config): snacks.Picker
---@field keymaps fun(opts?: snacks.picker.Config): snacks.Picker
---@field registers fun(opts?: snacks.picker.Config): snacks.Picker
---@field command_history fun(opts?: snacks.picker.history.Config): snacks.Picker
---@field search_history fun(opts?: snacks.picker.history.Config): snacks.Picker
---@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
---@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,
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<string,vim.api.keyset.get_hl_info> ]]
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<string, boolean>
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/*", true, 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<string, boolean>
for _, km in ipairs(maps) do
local key = Snacks.picker.util.text(km, { "mode", "lhs", "buffer" })
if not done[key] then
done[key] = true
local item = {
mode = km.mode,
item = km,
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.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,
preview = {
text = value,
ft = "text",
},
})
end
end
return items
end
return M