snacks.nvim/lua/snacks/picker/source/proc.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

118 lines
3 KiB
Lua

local Async = require("snacks.picker.util.async")
local M = {}
local uv = vim.uv or vim.loop
M.USE_QUEUE = true
---@class snacks.picker.proc.Config: snacks.picker.Config
---@field cmd string
---@field args? string[]
---@field env? table<string, string>
---@field cwd? string
---@field notify? boolean Notify on failure
---@field transform? fun(item: snacks.picker.finder.Item): boolean?
---@param opts snacks.picker.proc.Config
---@return fun(cb:async fun(item:snacks.picker.finder.Item))
function M.proc(opts)
assert(opts.cmd, "`opts.cmd` is required")
---@async
return function(cb)
if opts.transform then
local _cb = cb
cb = function(item)
if opts.transform(item) ~= false then
_cb(item)
end
end
end
local aborted = false
local stdout = assert(uv.new_pipe())
opts = vim.tbl_deep_extend("force", {}, opts or {}, {
stdio = { nil, stdout, nil },
cwd = opts.cwd and vim.fs.normalize(opts.cwd) or nil,
}) --[[@as snacks.picker.proc.Config]]
local self = Async.running()
local handle ---@type uv.uv_process_t
handle = uv.spawn(opts.cmd, opts, function(code, _signal)
if not aborted and code ~= 0 and opts.notify ~= false then
local full = { opts.cmd or "" }
vim.list_extend(full, opts.args or {})
return Snacks.notify.error(("Command failed:\n- cmd: `%s`"):format(table.concat(full, " ")))
end
stdout:close()
handle:close()
self:resume()
end)
if not handle then
return Snacks.notify.error("Failed to spawn " .. opts.cmd)
end
local prev ---@type string?
self:on("abort", function()
aborted = true
if not handle:is_closing() then
handle:kill("sigterm")
vim.defer_fn(function()
if not handle:is_closing() then
handle:kill("sigkill")
end
end, 200)
end
end)
---@param data? string
local function process(data)
if aborted then
return
end
if not data then
return prev and cb({ text = prev })
end
local from = 1
while from <= #data do
local nl = data:find("\n", from, true)
if nl then
local cr = data:byte(nl - 2, nl - 2) == 13 -- \r
local line = data:sub(from, nl - (cr and 2 or 1))
if prev then
line, prev = prev .. line, nil
end
cb({ text = line })
from = nl + 1
elseif prev then
prev = prev .. data:sub(from)
break
else
prev = data:sub(from)
break
end
end
end
local queue = require("snacks.picker.util.queue").new()
stdout:read_start(function(err, data)
assert(not err, err)
if M.USE_QUEUE then
queue:push(data)
self:resume()
else
process(data)
end
end)
while not (handle:is_closing() and queue:empty()) do
if queue:empty() then
self:suspend()
else
process(queue:pop())
end
end
end
end
return M