snacks.nvim/lua/snacks/picker/core/input.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

147 lines
4.1 KiB
Lua

---@class snacks.picker.input
---@field win snacks.win
---@field totals string
---@field picker snacks.Picker
---@field _statuscolumn string
---@field filter snacks.picker.Filter
local M = {}
M.__index = M
local uv = vim.uv or vim.loop
local ns = vim.api.nvim_create_namespace("snacks.picker.input")
---@param picker snacks.Picker
function M.new(picker)
local self = setmetatable({}, M)
self.totals = ""
self.picker = picker
self.filter = require("snacks.picker.core.filter").new(picker)
self._statuscolumn = self:statuscolumn()
self.win = Snacks.win(Snacks.win.resolve(picker.opts.win.input, {
show = false,
enter = true,
height = 1,
text = picker.opts.live and self.filter.search or self.filter.pattern,
ft = "regex",
on_win = function()
vim.fn.prompt_setprompt(self.win.buf, "")
self.win:focus()
vim.cmd.startinsert()
vim.api.nvim_win_set_cursor(self.win.win, { 1, #self:get() + 1 })
vim.fn.prompt_setcallback(self.win.buf, function()
self.win:execute("confirm")
end)
vim.fn.prompt_setinterrupt(self.win.buf, function()
self.win:close()
end)
end,
bo = {
filetype = "snacks_picker_input",
buftype = "prompt",
},
wo = {
statuscolumn = self._statuscolumn,
cursorline = false,
winhighlight = Snacks.picker.highlight.winhl("SnacksPickerInput"),
},
}))
self.win:on(
{ "TextChangedI", "TextChanged" },
Snacks.util.throttle(function()
if not self.win:valid() then
return
end
vim.bo[self.win.buf].modified = false
local pattern = self:get()
if self.picker.opts.live then
self.filter.search = pattern
else
self.filter.pattern = pattern
end
picker:match()
end, { ms = picker.opts.live and 100 or 30 }),
{ buf = true }
)
return self
end
function M:statuscolumn()
local parts = {} ---@type string[]
local function add(str, hl)
if str then
parts[#parts + 1] = ("%%#%s#%s%%*"):format(hl, str:gsub("%%", "%%"))
end
end
local pattern = self.picker.opts.live and self.filter.pattern or self.filter.search
if pattern ~= "" then
if #pattern > 20 then
pattern = Snacks.picker.util.truncate(pattern, 20)
end
add(pattern, "SnacksPickerInputSearch")
end
add(self.picker.opts.prompt or "", "SnacksPickerPrompt")
return table.concat(parts, " ")
end
function M:update()
if not self.win:valid() then
return
end
local sc = self:statuscolumn()
if self._statuscolumn ~= sc then
self._statuscolumn = sc
vim.wo[self.win.win].statuscolumn = sc
end
local line = {} ---@type snacks.picker.Highlight[]
if self.picker:is_active() then
line[#line + 1] = { M.spinner(), "SnacksPickerSpinner" }
line[#line + 1] = { " " }
end
local selected = #self.picker.list.selected
if selected > 0 then
line[#line + 1] = { ("(%d)"):format(selected), "SnacksPickerTotals" }
line[#line + 1] = { " " }
end
line[#line + 1] = { ("%d/%d"):format(self.picker.list:count(), #self.picker.finder.items), "SnacksPickerTotals" }
line[#line + 1] = { " " }
local totals = table.concat(vim.tbl_map(function(v)
return v[1]
end, line))
if self.totals == totals then
return
end
self.totals = totals
vim.api.nvim_buf_set_extmark(self.win.buf, ns, 0, 0, {
id = 999,
virt_text = line,
virt_text_pos = "right_align",
})
end
function M:get()
return self.win:line()
end
---@param pattern? string
---@param search? string
function M:set(pattern, search)
self.filter.pattern = pattern or self.filter.pattern
self.filter.search = search or self.filter.search
vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, {
self.picker.opts.live and self.filter.search or self.filter.pattern,
})
vim.api.nvim_win_set_cursor(self.win.win, { 1, #self:get() + 1 })
self.totals = ""
self._statuscolumn = ""
self:update()
self.picker:update_titles()
end
function M.spinner()
local spinner = { "", "", "", "", "", "", "", "", "", "" }
return spinner[math.floor(uv.hrtime() / (1e6 * 80)) % #spinner + 1]
end
return M