mirror of
https://github.com/folke/snacks.nvim
synced 2025-07-07 21:25:11 +00:00

## 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. -->
230 lines
6.2 KiB
Lua
230 lines
6.2 KiB
Lua
---@class snacks.picker.Preview
|
|
---@field item? snacks.picker.Item
|
|
---@field win snacks.win
|
|
---@field preview snacks.picker.preview
|
|
---@field state table<string, any>
|
|
---@field main? number
|
|
---@field win_opts {main: snacks.win.Config, layout: snacks.win.Config, win: snacks.win.Config}
|
|
---@field winhl string
|
|
local M = {}
|
|
M.__index = M
|
|
|
|
---@class snacks.picker.preview.ctx
|
|
---@field picker snacks.Picker
|
|
---@field item snacks.picker.Item
|
|
---@field prev? snacks.picker.Item
|
|
---@field preview snacks.picker.Preview
|
|
---@field buf number
|
|
---@field win number
|
|
|
|
local ns = vim.api.nvim_create_namespace("snacks.picker.preview")
|
|
local ns_loc = vim.api.nvim_create_namespace("snacks.picker.preview.loc")
|
|
|
|
---@param opts snacks.picker.Config
|
|
---@param main? number
|
|
function M.new(opts, main)
|
|
local self = setmetatable({}, M)
|
|
self.winhl = Snacks.picker.highlight.winhl("SnacksPickerPreview")
|
|
local win_opts = Snacks.win.resolve(
|
|
{
|
|
title_pos = "center",
|
|
},
|
|
opts.win.preview,
|
|
{
|
|
show = false,
|
|
enter = false,
|
|
width = 0,
|
|
height = 0,
|
|
fixbuf = false,
|
|
bo = { filetype = "snacks_picker_preview" },
|
|
on_win = function()
|
|
self.item = nil
|
|
self:reset()
|
|
end,
|
|
wo = {
|
|
winhighlight = self.winhl,
|
|
},
|
|
}
|
|
)
|
|
self.win_opts = {
|
|
main = {
|
|
relative = "win",
|
|
backdrop = false,
|
|
},
|
|
layout = {
|
|
backdrop = win_opts.backdrop == true,
|
|
relative = "win",
|
|
},
|
|
}
|
|
self.win = Snacks.win(win_opts)
|
|
self:update(main)
|
|
self.state = {}
|
|
|
|
self.win:on("WinClosed", function()
|
|
self:clear(self.win.buf)
|
|
end, { win = true })
|
|
|
|
local preview = opts.preview or Snacks.picker.preview.file
|
|
preview = type(preview) == "string" and Snacks.picker.preview[preview] or preview
|
|
---@cast preview snacks.picker.preview
|
|
self.preview = preview
|
|
return self
|
|
end
|
|
|
|
---@param main? number
|
|
function M:update(main)
|
|
self.main = main
|
|
self.win_opts.main.win = main
|
|
self.win.opts = vim.tbl_deep_extend("force", self.win.opts, main and self.win_opts.main or self.win_opts.layout)
|
|
self.win.opts.wo.winhighlight = main and vim.wo[main].winhighlight or self.winhl
|
|
if main then
|
|
self.win:update()
|
|
end
|
|
end
|
|
|
|
---@param picker snacks.Picker
|
|
function M:show(picker)
|
|
local item, prev = picker:current(), self.item
|
|
self.item = item
|
|
if item then
|
|
local buf = self.win.buf
|
|
local ok, err = pcall(
|
|
self.preview,
|
|
setmetatable({
|
|
preview = self,
|
|
item = item,
|
|
prev = prev,
|
|
picker = picker,
|
|
}, {
|
|
__index = function(_, k)
|
|
if k == "buf" then
|
|
return self.win.buf
|
|
elseif k == "win" then
|
|
return self.win.win
|
|
end
|
|
end,
|
|
})
|
|
)
|
|
if not ok then
|
|
self:notify(err, "error")
|
|
end
|
|
if self.win.buf ~= buf then
|
|
self:clear(buf)
|
|
end
|
|
else
|
|
self:reset()
|
|
end
|
|
end
|
|
|
|
---@param title string
|
|
function M:set_title(title)
|
|
self.win:set_title(title)
|
|
end
|
|
|
|
---@param buf? number
|
|
function M:clear(buf)
|
|
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
|
|
return
|
|
end
|
|
vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)
|
|
vim.api.nvim_buf_clear_namespace(buf, ns_loc, 0, -1)
|
|
end
|
|
|
|
function M:reset()
|
|
if vim.api.nvim_buf_is_valid(self.win.scratch_buf) then
|
|
vim.api.nvim_win_set_buf(self.win.win, self.win.scratch_buf)
|
|
else
|
|
self.win:scratch()
|
|
end
|
|
self:set_title("")
|
|
vim.treesitter.stop(self.win.buf)
|
|
vim.bo[self.win.buf].modifiable = true
|
|
vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, {})
|
|
self:clear(self.win.buf)
|
|
vim.bo[self.win.buf].filetype = "snacks_picker_preview"
|
|
vim.bo[self.win.buf].syntax = ""
|
|
vim.wo[self.win.win].cursorline = false
|
|
end
|
|
|
|
-- create a new scratch buffer
|
|
function M:scratch()
|
|
local buf = vim.api.nvim_create_buf(false, true)
|
|
vim.bo[buf].bufhidden = "wipe"
|
|
local ei = vim.o.eventignore
|
|
vim.o.eventignore = "all"
|
|
vim.bo[buf].filetype = "snacks_picker_preview"
|
|
vim.o.eventignore = ei
|
|
vim.api.nvim_win_set_buf(self.win.win, buf)
|
|
return buf
|
|
end
|
|
|
|
--- highlight the buffer
|
|
---@param opts? {file?:string, buf?:number, ft?:string, lang?:string}
|
|
function M:highlight(opts)
|
|
opts = opts or {}
|
|
local ft = opts.ft
|
|
if not ft and (opts.file or opts.buf) then
|
|
ft = vim.filetype.match({
|
|
buf = opts.buf or self.win.buf,
|
|
filename = opts.file,
|
|
})
|
|
end
|
|
local lang = opts.lang or ft and vim.treesitter.language.get_lang(ft)
|
|
if not (lang and pcall(vim.treesitter.start, self.win.buf, lang)) then
|
|
if ft then
|
|
vim.bo[self.win.buf].syntax = ft
|
|
end
|
|
end
|
|
end
|
|
|
|
-- show the item location
|
|
function M:loc()
|
|
vim.api.nvim_buf_clear_namespace(self.win.buf, ns_loc, 0, -1)
|
|
if not self.item then
|
|
return
|
|
end
|
|
local line_count = vim.api.nvim_buf_line_count(self.win.buf)
|
|
if self.item.pos and self.item.pos[1] > 0 and self.item.pos[1] <= line_count then
|
|
vim.api.nvim_win_set_cursor(self.win.win, { self.item.pos[1], 0 })
|
|
vim.api.nvim_win_call(self.win.win, function()
|
|
vim.cmd("norm! zz")
|
|
vim.wo[self.win.win].cursorline = true
|
|
end)
|
|
if self.item.end_pos then
|
|
vim.api.nvim_buf_set_extmark(self.win.buf, ns_loc, self.item.pos[1] - 1, self.item.pos[2] - 1, {
|
|
end_row = self.item.end_pos[1] - 1,
|
|
end_col = self.item.end_pos[2] - 1,
|
|
hl_group = "SnacksPickerSearch",
|
|
})
|
|
end
|
|
elseif self.item.search then
|
|
vim.api.nvim_win_call(self.win.win, function()
|
|
vim.cmd("keepjumps norm! gg")
|
|
if pcall(vim.cmd, self.item.search) then
|
|
vim.cmd("norm! zz")
|
|
vim.wo[self.win.win].cursorline = true
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
---@param msg string
|
|
---@param level? "info" | "warn" | "error"
|
|
---@param opts? {item?:boolean}
|
|
function M:notify(msg, level, opts)
|
|
level = level or "info"
|
|
local lines = vim.split(level .. ": " .. msg, "\n", { plain = true })
|
|
local msg_len = #lines
|
|
if not (opts and opts.item == false) then
|
|
lines[#lines + 1] = ""
|
|
vim.list_extend(lines, vim.split(vim.inspect(self.item), "\n", { plain = true }))
|
|
end
|
|
vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, lines)
|
|
vim.api.nvim_buf_set_extmark(self.win.buf, ns, 0, 0, {
|
|
hl_group = "Diagnostic" .. level:sub(1, 1):upper() .. level:sub(2),
|
|
end_row = msg_len,
|
|
})
|
|
self:highlight({ lang = "lua" })
|
|
end
|
|
|
|
return M
|