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. -->
118 lines
3 KiB
Lua
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
|