Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lua/snacks/explorer/git.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
---@diagnostic disable: missing-fields
local Spawn = require("snacks.util.spawn")

local M = {}

---@class snacks.explorer.git.Status
Expand Down Expand Up @@ -55,7 +57,7 @@ function M.update(cwd, opts)
local output = ""
local stdout = assert(uv.new_pipe())
local handle ---@type uv.uv_process_t
handle = uv.spawn("git", {
handle = uv.spawn(Spawn.resolve_cmd("git"), {
stdio = { nil, stdout, nil },
cwd = root,
hide = true,
Expand Down
3 changes: 2 additions & 1 deletion lua/snacks/picker/source/proc.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---@diagnostic disable: await-in-sync
local Async = require("snacks.picker.util.async")
local Spawn = require("snacks.util.spawn")

local M = {}

Expand Down Expand Up @@ -58,7 +59,7 @@ function M.proc(opts, ctx)

local handle ---@type uv.uv_process_t
---@diagnostic disable-next-line: missing-fields
handle = uv.spawn(opts.cmd, spawn_opts, function(code, _signal)
handle = uv.spawn(Spawn.resolve_cmd(opts.cmd), spawn_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 {})
Expand Down
30 changes: 29 additions & 1 deletion lua/snacks/util/spawn.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ local M = {}

local uv = vim.uv or vim.loop

local is_windows = (uv.os_uname().sysname or ""):lower():find("windows") ~= nil

--- Resolve a bare executable name to an absolute path on Windows.
--- libuv's `spawn` does not search `PATH` for bare names on Windows the way
--- it does on POSIX, so a command like `rg` may return ENOENT even when
--- `vim.fn.exepath("rg")` resolves it (e.g. through a scoop shim).
--- Returns the input unchanged on non-Windows systems, when given a path
--- (anything containing `/` or `\`), or when resolution fails.
---@param cmd string
---@return string
function M.resolve_cmd(cmd)
if not is_windows or type(cmd) ~= "string" or cmd == "" then
return cmd
end
if cmd:find("[/\\]") then
return cmd
end
local resolved = vim.fn.exepath(cmd .. ".exe")
if resolved ~= "" then
return resolved
end
resolved = vim.fn.exepath(cmd)
if resolved ~= "" then
return resolved
end
return cmd
end

---@class snacks.spawn.Config: uv.spawn.options,{}
---@field cmd string
---@field args? (string|number)[]
Expand Down Expand Up @@ -149,7 +177,7 @@ function Proc:run()
hide = true,
args = vim.tbl_map(tostring, self.opts.args or {}),
})
self.handle = uv.spawn(self.opts.cmd, opts, function(code, signal)
self.handle = uv.spawn(M.resolve_cmd(self.opts.cmd), opts, function(code, signal)
self.code = code
self.signal = signal
self:on_exit()
Expand Down
32 changes: 32 additions & 0 deletions tests/spawn_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---@module 'luassert'

describe("snacks.spawn.resolve_cmd", function()
local Spawn = require("snacks.util.spawn")

local is_windows = (vim.uv or vim.loop).os_uname().sysname:lower():find("windows") ~= nil

it("returns non-string input unchanged", function()
assert.are.same(nil, Spawn.resolve_cmd(nil))
end)

it("returns empty string unchanged", function()
assert.are.same("", Spawn.resolve_cmd(""))
end)

it("returns an absolute POSIX path unchanged", function()
assert.are.same("/usr/bin/rg", Spawn.resolve_cmd("/usr/bin/rg"))
end)

it("returns a Windows-style backslash path unchanged", function()
assert.are.same([[C:\Tools\rg.exe]], Spawn.resolve_cmd([[C:\Tools\rg.exe]]))
end)

if not is_windows then
it("returns a bare command unchanged on non-Windows", function()
-- On POSIX, libuv's spawn searches PATH for bare names, so resolve_cmd
-- is a no-op for bare commands like "rg" or "git".
assert.are.same("rg", Spawn.resolve_cmd("rg"))
assert.are.same("git", Spawn.resolve_cmd("git"))
end)
end
end)