mirror of
https://github.com/folke/snacks.nvim
synced 2025-07-07 21:25:11 +00:00
198 lines
4.9 KiB
Lua
198 lines
4.9 KiB
Lua
local M = {}
|
|
|
|
local uv = vim.uv or vim.loop
|
|
|
|
---@type {cmd:string[], args:string[], enabled?:boolean, available?:boolean|string}[]
|
|
local commands = {
|
|
{
|
|
cmd = { "fd", "fdfind" },
|
|
args = { "--type", "f", "--type", "l", "--color", "never", "-E", ".git" },
|
|
},
|
|
{
|
|
cmd = { "rg" },
|
|
args = { "--files", "--no-messages", "--color", "never", "-g", "!.git" },
|
|
},
|
|
{
|
|
cmd = { "find" },
|
|
args = { ".", "-type", "f", "-not", "-path", "*/.git/*" },
|
|
enabled = vim.fn.has("win-32") == 0,
|
|
},
|
|
}
|
|
|
|
---@param cmd? string
|
|
---@return string? cmd, string[]? args
|
|
function M.get_cmd(cmd)
|
|
local checked = {} ---@type string[]
|
|
for _, command in ipairs(commands) do
|
|
if command.enabled ~= false and command.available ~= false and (not cmd or vim.tbl_contains(command.cmd, cmd)) then
|
|
if command.available then
|
|
assert(type(command.available) == "string", "available must be a string")
|
|
return command.available, vim.deepcopy(command.args)
|
|
end
|
|
for _, c in ipairs(command.cmd) do
|
|
table.insert(checked, c)
|
|
if vim.fn.executable(c) == 1 then
|
|
command.available = c
|
|
return c, vim.deepcopy(command.args)
|
|
end
|
|
end
|
|
command.available = false
|
|
end
|
|
end
|
|
checked = #checked == 0 and cmd and { cmd } or checked
|
|
checked = vim.tbl_map(function(c)
|
|
return "`" .. c .. "`"
|
|
end, checked)
|
|
Snacks.notify.error("No supported finder found:\n- " .. table.concat(checked, "\n-"))
|
|
end
|
|
|
|
function M.get_fd()
|
|
return M.get_cmd("fd")
|
|
end
|
|
|
|
---@param opts snacks.picker.files.Config
|
|
---@param filter snacks.picker.Filter
|
|
local function get_cmd(opts, filter)
|
|
local cmd, args = M.get_cmd(opts.cmd)
|
|
if not cmd or not args then
|
|
return
|
|
end
|
|
local is_fd, is_fd_rg, is_find, is_rg = cmd == "fd" or cmd == "fdfind", cmd ~= "find", cmd == "find", cmd == "rg"
|
|
|
|
-- exclude
|
|
for _, e in ipairs(opts.exclude or {}) do
|
|
if is_fd then
|
|
vim.list_extend(args, { "-E", e })
|
|
elseif is_rg then
|
|
vim.list_extend(args, { "-g", "!" .. e })
|
|
elseif is_find then
|
|
table.insert(args, "-not")
|
|
table.insert(args, "-path")
|
|
table.insert(args, e)
|
|
end
|
|
end
|
|
|
|
-- extensions
|
|
local ft = opts.ft or {}
|
|
ft = type(ft) == "string" and { ft } or ft
|
|
---@cast ft string[]
|
|
for _, e in ipairs(ft) do
|
|
if is_fd then
|
|
table.insert(args, "-e")
|
|
table.insert(args, e)
|
|
elseif is_rg then
|
|
table.insert(args, "-g")
|
|
table.insert(args, "*." .. e)
|
|
elseif is_find then
|
|
table.insert(args, "-name")
|
|
table.insert(args, "*." .. e)
|
|
end
|
|
end
|
|
|
|
-- hidden
|
|
if opts.hidden and is_fd_rg then
|
|
table.insert(args, "--hidden")
|
|
elseif not opts.hidden and is_find then
|
|
vim.list_extend(args, { "-not", "-path", "*/.*" })
|
|
end
|
|
|
|
-- ignored
|
|
if opts.ignored and is_fd_rg then
|
|
args[#args + 1] = "--no-ignore"
|
|
end
|
|
|
|
-- follow
|
|
if opts.follow then
|
|
args[#args + 1] = "-L"
|
|
end
|
|
|
|
-- extra args
|
|
vim.list_extend(args, opts.args or {})
|
|
|
|
-- file glob
|
|
---@type string?
|
|
local pattern, pargs = Snacks.picker.util.parse(filter.search)
|
|
vim.list_extend(args, pargs)
|
|
|
|
pattern = pattern ~= "" and pattern or nil
|
|
if pattern then
|
|
if is_fd then
|
|
table.insert(args, pattern)
|
|
elseif is_rg then
|
|
table.insert(args, "--glob")
|
|
table.insert(args, pattern)
|
|
elseif is_find then
|
|
table.insert(args, "-name")
|
|
table.insert(args, pattern)
|
|
end
|
|
end
|
|
|
|
-- dirs
|
|
local dirs = opts.dirs or {}
|
|
if opts.rtp then
|
|
vim.list_extend(dirs, Snacks.picker.util.rtp())
|
|
end
|
|
if #dirs > 0 then
|
|
dirs = vim.tbl_map(svim.fs.normalize, dirs) ---@type string[]
|
|
if is_fd and not pattern then
|
|
args[#args + 1] = "."
|
|
end
|
|
if is_find then
|
|
table.remove(args, 1)
|
|
for _, d in pairs(dirs) do
|
|
table.insert(args, 1, d)
|
|
end
|
|
else
|
|
vim.list_extend(args, dirs)
|
|
end
|
|
end
|
|
|
|
return cmd, args
|
|
end
|
|
|
|
---@param opts snacks.picker.files.Config
|
|
---@type snacks.picker.finder
|
|
function M.files(opts, ctx)
|
|
local cwd = not (opts.rtp or (opts.dirs and #opts.dirs > 0))
|
|
and svim.fs.normalize(opts and opts.cwd or uv.cwd() or ".")
|
|
or nil
|
|
local cmd, args = get_cmd(opts, ctx.filter)
|
|
if not cmd then
|
|
return function() end
|
|
end
|
|
if opts.debug.files then
|
|
Snacks.notify(cmd .. " " .. table.concat(args or {}, " "))
|
|
end
|
|
return require("snacks.picker.source.proc").proc({
|
|
opts,
|
|
{
|
|
cmd = cmd,
|
|
args = args,
|
|
notify = not opts.live,
|
|
---@param item snacks.picker.finder.Item
|
|
transform = function(item)
|
|
item.cwd = cwd
|
|
item.file = item.text
|
|
end,
|
|
},
|
|
}, ctx)
|
|
end
|
|
|
|
---@param opts snacks.picker.proc.Config
|
|
---@type snacks.picker.finder
|
|
function M.zoxide(opts, ctx)
|
|
return require("snacks.picker.source.proc").proc({
|
|
opts,
|
|
{
|
|
cmd = "zoxide",
|
|
args = { "query", "--list" },
|
|
---@param item snacks.picker.finder.Item
|
|
transform = function(item)
|
|
item.file = item.text
|
|
item.dir = true
|
|
end,
|
|
},
|
|
}, ctx)
|
|
end
|
|
|
|
return M
|