---@diagnostic disable-next-line: deprecated
local uv = vim.uv or vim.loop
local _is_win = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
local M = {}
local base64 = require("fzf-lua.lib.base64")
local serpent = require("fzf-lua.lib.serpent")
---@param pid integer
---@param signal integer|string?
---@return boolean
M.process_kill = function(pid, signal)
if not pid or not tonumber(pid) then return false end
if type(uv.os_getpriority(pid)) == "number" then
uv.kill(pid, signal or 9)
return true
end
return false
end
local function coroutine_callback(fn)
local co = coroutine.running()
---@cast co thread
local callback = function(...)
if coroutine.status(co) == "suspended" then
coroutine.resume(co, ...)
else
local pid = unpack({ ... }) ---@cast pid integer
M.process_kill(pid)
end
end
fn(callback)
return coroutine.yield()
end
local function coroutinify(fn)
return function(...)
local args = { ... }
return coroutine.wrap(function()
return coroutine_callback(function(cb)
table.insert(args, cb)
fn(unpack(args))
end)
end)()
end
end
-- fix environ for uv.spawn
---@param cmd string
---@param opts uv.spawn.options
---@param on_exit fun(code: integer, signal: integer)
---@return uv.uv_process_t handle
---@return integer pid
M.uv_spawn = function(cmd, opts, on_exit)
opts.env = (function()
-- uv.spawn will override all env when table provided?
-- steal from $VIMRUNTIME/lua/vim/_system.lua
local env = vim.fn.environ() --- @type table<string,string>
env["NVIM"] = vim.v.servername
env["NVIM_LISTEN_ADDRESS"] = nil
env = vim.tbl_extend("keep", opts.env or {}, env or {})
local renv = {} --- @type string[]
for k, v in pairs(env) do
renv[#renv + 1] = string.format("%s=%s", k, tostring(v))
end
return renv
end)() ---@diagnostic disable-next-line: unnecessary-assert, return-type-mismatch, redundant-return-value
return assert(uv.spawn(cmd, opts, on_exit))
end
-- The FFI table must be obtained via `require("ffi")`: it is cached in
-- `package.loaded`, so all code shares a single CType state. Calling the
-- library constructor directly (`package.preload.ffi()`) re-runs
-- `luaopen_ffi`, which re-initializes the CType state: every previous
-- `cdef` is dropped and already-resolved symbols start resolving to
-- unrelated types (calling them fails with e.g. `'short' is not callable`).
-- All Windows APIs used by this module are declared in a single `cdef`.
local win_ffi = nil ---@type table|false|nil
---@return { ffi: table, k32: table }?
local function win_ffi_init()
if win_ffi ~= nil then return win_ffi or nil end
local ok, res = pcall(function()
local ffi = require("ffi")
ffi.cdef([[
typedef struct { short x; short y; } FzfCOORD;
typedef struct { short left; short top; short right; short bottom; } FzfSMALL;
typedef struct {
FzfCOORD dwSize;
FzfCOORD dwCursorPosition;
unsigned short wAttributes;
FzfSMALL srWindow;
FzfCOORD dwMaximumWindowSize;
} FzfCSBI;
void* CreateFileW(const unsigned short*, unsigned long,
unsigned long, void*, unsigned long, unsigned long, void*);
int CloseHandle(void* hObject);
int GetConsoleScreenBufferInfo(void* hConsoleOutput, FzfCSBI* info);
int _open_osfhandle(long long, int);
void* GetStdHandle(unsigned long nStdHandle);
int GetHandleInformation(void* hObject, unsigned long* lpdwFlags);
int SetHandleInformation(void* hObject, unsigned long dwMask,
unsigned long dwFlags);
typedef struct {
unsigned long nLength;
void* lpSecurityDescriptor;
int bInheritHandle;
} FzfSA;
]])
return { ffi = ffi, k32 = ffi.load("kernel32") }
end)
win_ffi = (ok and res) or false
return win_ffi or nil
end
-- Reopen the Windows console (`CONIN$`/`CONOUT$`) as inheritable CRT fds.
-- When the shell widgets capture both output streams (e.g. a PSReadLine key
-- handler piping stdout and stderr), spawned children have no console handle
-- left to inherit: nvim's TUI hangs without one and fzf silently ignores
-- `--height` (the light renderer writes the UI to a console stderr and falls
-- back to fullscreen without one). Returns `{ stdin, stdout, stderr }` fds,
-- or nil when the console cannot be reopened (or on other platforms).
M.win_console_stdio = function()
if not _is_win then return end
if M._win_console_fds ~= nil then return M._win_console_fds or nil end
local ok = pcall(function()
local win = win_ffi_init()
local ffi = assert(win and win.ffi)
local function wsz(s)
local b = ffi.new("unsigned short[?]", #s + 1)
for i = 1, #s do b[i - 1] = s:byte(i) end
return b
end
local sa = ffi.new("FzfSA")
sa.nLength = ffi.sizeof(sa)
sa.bInheritHandle = 1
-- The handles must allow *write* access, not just read: nvim's TUI
-- calls `SetConsoleMode` on stdin (needs `GENERIC_WRITE`) and hangs
-- before rendering anything if that fails.
local GENERIC_RW, SHARE_RW, OPEN_EXISTING = 0xC0000000, 3, 3
local hi = ffi.C.CreateFileW(wsz("CONIN$"), GENERIC_RW, SHARE_RW, sa, OPEN_EXISTING, 0, nil)
local ho = ffi.C.CreateFileW(wsz("CONOUT$"), GENERIC_RW, SHARE_RW, sa, OPEN_EXISTING, 0, nil)
local vi = tonumber(ffi.cast("intptr_t", hi))
local vo = tonumber(ffi.cast("intptr_t", ho))
if vi == -1 or vo == -1 then error("CreateFileW failed") end
local conin = ffi.C._open_osfhandle(ffi.cast("long long", hi), 0)
local conout = ffi.C._open_osfhandle(ffi.cast("long long", ho), 0)
if conin < 0 or conout < 0 then error("_open_osfhandle failed") end
M._win_console_fds = { conin, conout, conout } ---@type integer[]|false
end)
if not ok then M._win_console_fds = false end
return M._win_console_fds or nil
end
-- Returns the stderr fd to pass to `uv.spawn` so the child sees a console:
-- fd 2 is reused when it already is a tty, otherwise `CONOUT$` is reopened
-- (see `win_console_stdio`); falls back to fd 2 when neither is possible.
-- Always 2 on non-Windows platforms.
M.win_stderr_fd = function()
if not _is_win then return 2 end
local ok, tty = pcall(uv.guess_handle, 2)
if ok and tty == "tty" then return 2 end
local console = M.win_console_stdio()
return console and console[3] or 2
end
--- Width of the Windows console screen buffer (`srWindow`), `nil` when not
--- attached to a console or on other platforms. The std handles cannot be
--- trusted for this: the pwsh PSReadLine key handlers pipe both stdout and
--- stderr (`uv.new_tty` fails on the pipes) and a ConPTY stdin tty reports
--- no size, so the console device is opened directly (`CONOUT$`/`CONIN$`,
--- same rationale as `win_console_stdio`).
---@return integer?
M.win_console_width = function()
if not _is_win then return nil end
local width
pcall(function()
local win = win_ffi_init()
local ffi = assert(win and win.ffi)
local function wsz(s)
local b = ffi.new("unsigned short[?]", #s + 1)
for i = 1, #s do b[i - 1] = s:byte(i) end
return b
end
local GENERIC_RW, SHARE_RW, OPEN_EXISTING = 0xC0000000, 3, 3
local info = ffi.new("FzfCSBI")
for _, dev in ipairs({ "CONOUT$", "CONIN$" }) do
local h = ffi.C.CreateFileW(wsz(dev), GENERIC_RW, SHARE_RW, nil, OPEN_EXISTING, 0, nil)
local hv = tonumber(ffi.cast("intptr_t", h))
if hv ~= -1 then
if ffi.C.GetConsoleScreenBufferInfo(h, info) ~= 0 then
local w = info.srWindow.right - info.srWindow.left + 1
if w > 0 then width = w end
end
ffi.C.CloseHandle(h)
if width then break end
end
end
end)
return width
end
--- libuv as bundled with Neovim (<= 1.52) spawns children with
--- `CreateProcess(bInheritHandles=TRUE)` and no
--- `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`, so *every* inheritable handle of
--- this process leaks into the child even when it isn't part of `stdio`.
--- When a shell integration captures the picker output (stdout/stderr are
--- pipes), those pipes would end up in the spawned process tree; an nvim
--- `--embed` server left running by `:detach` would then keep the pipes'
--- write end open forever and the shell's pipeline reader would never see
--- EOF (stuck shell/key handler).
---
--- Temporarily clears `HANDLE_FLAG_INHERIT` on the stdio fds (`0`, `1`,
--- `2`) that are not referenced by `stdio`. Returns a function restoring
--- the original flags; call it right after `uv.spawn` returns (the
--- child's handle table is fixed at `CreateProcess` time).
---@param stdio table stdio table as passed to `uv.spawn`
---@return fun()|nil restore
M.win_stdio_no_inherit = function(stdio)
if not _is_win then return end
local win = win_ffi_init()
if not win then return end
local ffi, k32 = win.ffi, win.k32
local in_use = {} ---@type table<integer, boolean>
for _, std in ipairs(stdio) do
if type(std) == "number" then
---@cast std integer
in_use[std] = true
end
end
local HANDLE_FLAG_INHERIT = 1
-- STD_INPUT_HANDLE/STD_OUTPUT_HANDLE/STD_ERROR_HANDLE (fds 0/1/2)
local STD_HANDLES = { 0xFFFFFFF6, 0xFFFFFFF5, 0xFFFFFFF4 }
local saved = {} ---@type { h: any, flags: integer }[]
for fd = 0, 2 do
if not in_use[fd] then
local h = k32.GetStdHandle(STD_HANDLES[fd + 1])
local flags = ffi.new("unsigned long[1]")
if k32.GetHandleInformation(h, flags) ~= 0
and k32.SetHandleInformation(h, HANDLE_FLAG_INHERIT, 0) ~= 0 then
saved[#saved + 1] = { h = h, flags = flags[0] }
end
end
end
if #saved == 0 then return end
return function()
for _, s in ipairs(saved) do
k32.SetHandleInformation(s.h, HANDLE_FLAG_INHERIT, s.flags)
end
end
end
---@class fzf-lua.WinSpawnSpec
---@field bin string fzf executable (`fzf`/`sk` or full path)
---@field args string[] raw fzf argv (`uv.spawn` prepends `bin` as argv[0])
---@field item? string producer command (cmd syntax), piped into fzf stdin
---@field outfile string fzf stdout is redirected to this file
---@field env? table<string,any>
---@field cwd? string
---@field stdin? integer fzf stdin fd when there is no `item` (default 0)
---@field stderr? integer fzf stderr fd (default `win_stderr_fd()`)
-- Windows ("one route") fzf spawn for `fzf.raw_fzf`: no shell, no escaping.
-- `uv.spawn` runs fzf with the raw argv (`_cli_segments`), stderr stays on
-- the console/pty (fzf's light renderer requires a console, see
-- `IsLightRendererSupported` in fzf's `src/tui/light_windows.go`), stdout is
-- redirected to `spec.outfile` (replaces the `> outfile` shell redirect) and
-- the optional `spec.item` producer is spawned via `cmd.exe /c` with its
-- stdout pumped into fzf's stdin, every chunk forwarded as-is, immediately.
-- This replaces the caret-escaped cmd.exe launcher, the `(item) | fzf` cmd
-- pipeline and the >8191-char PowerShell fallback. Callers: `scripts/cli.lua`
-- (CLI nvim) and `win_spawn.lua` (mediator for TUI terminal buffers).
---@param spec fzf-lua.WinSpawnSpec
---@param opts { on_exit: fun(rc: integer) } called once fzf and the producer
--- tree exited (the `outfile` is flushed before that)
---@return boolean ok false when fzf failed to spawn (on_exit(1) still called)
M.win_spawn = function(spec, opts)
assert(_is_win, "win_spawn is Windows only")
-- pause the producer read when this many bytes are queued for fzf
local HIGH, LOW = 4 * 1024 * 1024, 1024 * 1024
local item = spec.item and #spec.item > 0 and spec.item or nil
local stderr_fd = spec.stderr or M.win_stderr_fd()
---@type integer?
local out_fd = assert(uv.fs_open(spec.outfile, "w", 420))
local prod_out = item and assert(uv.new_pipe(false)) or nil
local fzf_in = item and assert(uv.new_pipe(false)) or nil
local fzf_handle, prod_handle, kill_handle, kill_null
local prod_pid
local fzf_done, prod_done, kill_done = false, item == nil, true
local fzf_rc, finished
local writes, pending, paused, eof = 0, 0, false, false
---@param h uv.uv_pipe_t|uv.uv_process_t|nil
local function close_handle(h)
if h and not h:is_closing() then h:close() end
end
local function finish(rc)
if finished then return end
finished = true
close_handle(fzf_in)
close_handle(prod_out)
close_handle(kill_handle)
close_handle(kill_null)
close_handle(fzf_handle)
close_handle(prod_handle)
if out_fd then
uv.fs_close(out_fd)
out_fd = nil
end
opts.on_exit(rc)
end
local function maybe_finish()
if fzf_done and prod_done and kill_done then
finish(fzf_rc == nil and 1 or fzf_rc)
end
end
-- producer EOF: flush the pending writes, then close our end of fzf's
-- stdin so fzf sees EOF (`--select-1` and `reload` actions rely on it)
local function close_write_end()
if eof and writes == 0 and fzf_in and not fzf_in:is_closing() then
fzf_in:close()
end
end
-- byte pump: forward each chunk as-is, immediately, no processing
local function pump(err, data)
if err or data == nil then
eof = true
close_handle(prod_out)
close_write_end()
return
end
if not fzf_in or fzf_in:is_closing() then return end
local len = #data
writes, pending = writes + 1, pending + len
fzf_in:write(data, function(werr)
writes, pending = writes - 1, pending - len
-- after fzf exits the queued writes error (EPIPE/ECANCELED)
if not werr and fzf_in and not fzf_in:is_closing() then
if paused and pending < LOW and prod_out and not prod_out:is_closing() then
paused = false
prod_out:read_start(pump)
end
close_write_end()
end
end)
if not paused and pending > HIGH and prod_out and not prod_out:is_closing() then
paused = true
prod_out:read_stop()
end
end
-- fzf first: a spawn failure aborts before the producer is started
local ok, spawn_err = pcall(function()
fzf_handle = M.uv_spawn(spec.bin, {
cwd = spec.cwd,
args = spec.args,
stdio = { fzf_in or spec.stdin or 0, out_fd, stderr_fd },
env = spec.env,
}, function(rc, _)
fzf_done, fzf_rc = true, rc
close_handle(fzf_handle)
if prod_out and not prod_out:is_closing() then
prod_out:read_stop()
end
-- fzf exited while the producer may still be running (e.g. the user
-- aborted a `ping`-like producer): luv's `kill` is not a tree kill
-- on Windows, `taskkill /T /F` reaps the launcher and its children
if not prod_done and prod_pid then
kill_done = false
local kok = pcall(function()
if not kill_null then kill_null = assert(uv.new_pipe(false)) end
kill_handle = M.uv_spawn("taskkill.exe", {
args = { "/PID", tostring(prod_pid), "/T", "/F" },
stdio = { nil, kill_null, kill_null },
}, function()
kill_done = true
close_handle(kill_handle)
maybe_finish()
end)
close_handle(kill_null)
kill_null = nil
end)
if not kok then kill_done = true end
end
maybe_finish()
end)
end)
if not ok or not fzf_handle then
io.stderr:write(
("[fzf-lua] failed to spawn '%s': %s\n"):format(spec.bin, tostring(spawn_err)))
finish(1)
return false
end
if item then
local pok = pcall(function()
prod_handle, prod_pid = M.uv_spawn("cmd.exe", {
-- pass the producer command verbatim: it is a raw `cmd /c` line,
-- libuv's quoting would corrupt it (same as the old launcher)
args = { "/d", "/e:off", "/f:off", "/v:off", "/c", item },
verbatim = true,
cwd = spec.cwd,
env = spec.env,
stdio = { nil, prod_out, stderr_fd },
}, function(_, _)
prod_done = true
close_handle(prod_handle)
maybe_finish()
end)
end)
if pok and prod_handle and prod_out then
prod_out:read_start(pump)
else
-- producer failed to spawn (e.g. malformed command): close the write
-- end so fzf still terminates (empty list, `shell_nop` semantics)
prod_done, eof = true, true
close_write_end()
maybe_finish()
end
end
return true
end
---@class fzf-lua.SpawnOpts
---@field cwd? string
---@field cmd string|table
---@field env? table
---@field cb_finish fun(code: integer, sig: integer, from: string, pid: integer)
---@field cb_write fun(data: string, cb: fun(err: any): nil): nil
---@field cb_write_lines? fun(lines: string[])
---@field cb_err fun(data: string)
---@field cb_pid? fun(pid: integer)
---@field fn_transform? fun()
---@field EOL? string
---@field EOL_data? string
---@field process1? boolean
---@field profiler? boolean
---@field use_queue? boolean
---@param opts fzf-lua.SpawnOpts
---@param fn_transform function?
---@param fn_done function?
---@return uv.uv_process_t proc
---@return integer pid
M.spawn = function(opts, fn_transform, fn_done)
local EOL = opts.EOL or "\n"
local EOL_data = type(opts.cmd) == "string"
-- fd -0|--print0
-- rg -0|--null
-- grep -Z|--null
-- find . -print0
and (opts.cmd:match("%s%-0")
or opts.cmd:match("%s%-?%-print0") -- -print0|--print0
or opts.cmd:match("%s%-%-null")
or opts.cmd:match("%s%-Z"))
and "\0" or "\n"
local output_pipe = assert(uv.new_pipe(false))
local error_pipe = assert(uv.new_pipe(false))
local write_cb_count, read_cb_count = 0, 0
local prev_line_content ---@type string?
local handle, pid ---@type uv.uv_process_t, integer
local co = coroutine.running()
---@cast co thread
local queue = require("fzf-lua.lib.queue").new()
local work_ctx
-- Disable queue if running headless due to
-- "Attempt to yield across a C-call boundary"
opts.use_queue = not _G._fzf_lua_is_headless and opts.use_queue
-- cb_write_lines trumps cb_write
---@diagnostic disable-next-line: assign-type-mismatch
if opts.cb_write_lines then opts.cb_write = opts.cb_write_lines end
local can_finish = function()
if not output_pipe:is_active() -- EOF signalled or process is aborting
and read_cb_count == 0 -- no outstanding read_cb data processing
and write_cb_count == 0 -- no outstanding write callbacks
then
return true
end
end
---@diagnostic disable-next-line: redefined-local
local finish = function(code, sig, from, pid)
-- Uncomment to debug pipe closure timing issues (#1521)
-- output_pipe:close(function() print("closed o") end)
-- error_pipe:close(function() print("closed e") end)
if not output_pipe:is_closing() then output_pipe:close() end
if not error_pipe:is_closing() then error_pipe:close() end
if opts.cb_finish then
opts.cb_finish(code, sig, from, pid)
end
queue:clear()
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
-- NO LONGER USED, was coroutinify callback
if fn_done then fn_done(pid) end
end
-- https://github.com/luvit/luv/blob/master/docs.md
-- uv.spawn returns tuple: handle, pid
local shell = _is_win and "cmd.exe" or "sh"
local args = _is_win and { "/d", "/e:off", "/f:off", "/v:on", "/c" } or { "-c" }
if type(opts.cmd) == "table" then
if _is_win then
vim.list_extend(args, opts.cmd)
else
table.insert(args, table.concat(opts.cmd, " "))
end
else
table.insert(args, tostring(opts.cmd))
end
handle, pid = M.uv_spawn(shell, {
args = args,
stdio = { nil, output_pipe, error_pipe },
cwd = opts.cwd,
env = opts.env,
verbatim = _is_win,
}, function(code, signal)
if can_finish() or code ~= 0 then
-- Do not call `:read_stop` or `:close` here as we may have data
-- reads outstanding on slower Windows machines (#1521), only call
-- `finish` if all our `uv.write` calls are completed and the pipe
-- is no longer active (i.e. no more read cb's expected)
finish(code, signal, "[on_exit]", pid)
end
handle:close()
end)
-- save current process pid
if opts.cb_pid then opts.cb_pid(pid) end
local function write_cb(data)
write_cb_count = write_cb_count + 1
---@diagnostic disable-next-line: need-check-nil
opts.cb_write(data, function(err)
write_cb_count = write_cb_count - 1
if err then
-- can fail with premature process kill
-- assert(not err)
finish(130, 0, "[write_cb: err]", pid)
elseif can_finish() then
-- on_exit callback already called and did not close the
-- pipe due to write_cb_count>0, since this is the last
-- call we can close the fzf pipe
finish(0, 0, "[write_cb: finish]", pid)
end
end)
end
---@param data string data stream
---@param prev string? rest of line from previous call
---@param trans function? line transformation function
---@return table, string? line array, partial last line (no EOL)
local function split_lines(data, prev, trans)
local ret = {}
local start_idx = 1
repeat
local nl_idx = data:find(EOL_data, start_idx, true)
if nl_idx then
local cr = data:byte(nl_idx - 1, nl_idx - 1) == 13 -- \r
local line = data:sub(start_idx, nl_idx - (cr and 2 or 1))
if prev then
line = prev .. line
prev = nil
end
if trans then line = trans(line) end
if line then table.insert(ret, line) end
start_idx = nl_idx + 1
else
-- assert(start_idx <= #data)
if prev and #prev > 4096 then
-- chunk size is 64K, limit previous line length to 4K
-- max line length is therefor 4K + 64K (leftover + full chunk)
-- without this we can memory fault on extremely long lines (#185)
-- or have UI freezes (#211)
prev = prev:sub(1, 4096)
end
prev = (prev or "") .. data:sub(start_idx)
end
until not nl_idx or start_idx > #data
return ret, prev
end
--- Called with nil to process the leftover data
---@param data string?
local process_data = function(data)
if not data and prev_line_content then
data = prev_line_content .. EOL
prev_line_content = nil
end
if not data then
-- NOTE: this isn't called when prev_line_content is not nil but that's
-- not a problem as the write_cb will call finish once the callback is done
-- since the output_pipe is already in "closing" state
if can_finish() then
finish(0, 0, "[EOF]", pid)
end
return
end
if not fn_transform then
write_cb(data)
else
-- NOTE: cannot use due to "yield across a C-call boundary"
-- if co and not work_ctx then
-- work_ctx = uv.new_work(split_lines, function(lines, prev)
-- coroutine.resume(co, lines, prev)
-- end)
-- end
local nlines, lines = 0, nil
local t_st = opts.profiler and uv.hrtime()
if t_st then write_cb(string.format("[DEBUG] start: %.0f (ns)" .. EOL, t_st)) end
if work_ctx then
-- should never get here, work_ctx is never initialized
-- code remains as a solemn reminder to my efforts of making
-- multiprocess=false a lag free experience
if prev_line_content then uv.queue_work(work_ctx, data, prev_line_content) end
lines, prev_line_content = coroutine.yield()
else
lines, prev_line_content = split_lines(data, prev_line_content,
-- NOTE `fn_transform=true` is used to force line split without transformation
type(fn_transform) == "function" and fn_transform or nil)
end
nlines = nlines + #lines
if #lines > 0 then
if opts.cb_write_lines then
write_cb(lines)
else
-- Testing shows better performance writing the entire table at once as opposed to
-- calling 'write_cb' for every line after 'fn_transform', we therefore only use
-- `process1` when using "mini.icons" as `vim.filetype.match` causes a signigicant
-- delay and having to wait for all lines to be processed has an apparent lag
if opts.process1 then
vim.tbl_map(function(l) write_cb(l .. EOL) end, lines)
else
write_cb(table.concat(lines, EOL) .. EOL)
end
end
end
if t_st then
local t_e = vim.uv.hrtime()
write_cb(string.format("[DEBUG] finish:%.0f (ns) %d lines took %.0f (ms)" .. EOL,
t_e, nlines, (t_e - t_st) / 1e6))
end
end
end
local read_cb = function(err, data)
if err then
finish(130, 0, "[read_cb: err]", pid)
return
end
if not data then
-- EOF signalled, we can close the pipe
output_pipe:close()
end
if opts.use_queue then
if data then queue:push(data) end
-- Either we have outstanding data enqueued or the pipe is closing
-- due to the above `output_pipe:close`, in both cases we need to
-- resume the dequeue loop
coroutine.resume(co)
else
read_cb_count = read_cb_count + 1
local process = function()
read_cb_count = read_cb_count - 1
process_data(data)
end
-- Schedule data processing if we're in fast event
-- avoids "attempt to yield across C-call boundary" by using vim.schedule
if vim.in_fast_event() then vim.schedule(process) else process() end
end
end
local err_cb = function(err, data)
if err then
finish(130, 0, "[err_cb]", pid)
end
if not data then
return
end
if opts.cb_err then
opts.cb_err(data)
else
write_cb(data)
end
end
if not handle then
-- uv.spawn failed, error will be in 'pid'
-- call once to output the error message
-- and second time to signal EOF (data=nil)
err_cb(nil, pid .. EOL)
err_cb(pid, nil)
else
output_pipe:read_start(read_cb)
error_pipe:read_start(err_cb)
end
if opts.use_queue then
while not (output_pipe:is_closing() and queue:empty()) do
if queue:empty() then
coroutine.yield()
else
process_data(queue:pop())
end
end
-- process the leftover line from `processs_data`
-- will call `finish` immediately if there's no last line
-- otherwise, finish is called in the write callback
process_data(nil)
end
return handle, pid
end
-- Coroutine version of spawn so we can use queue
M.async_spawn = coroutinify(M.spawn)
---@param obj table
---@param b64? boolean
---@return string, boolean -- boolean used for ./scripts/headless_fd.sh
M.serialize = function(obj, b64)
local str = serpent.line(obj, { name = "_", comment = false, sortkeys = false })
str = b64 ~= false and base64.encode(str) or str
return "return [==[" .. str .. "]==]", (b64 ~= false and true or false)
end
---@param str string
---@param b64? boolean
---@return table
M.deserialize = function(str, b64)
local res = assert(loadstring(str))()
if type(res) == "table" then return res --[[@as table]] end -- ./scripts/headless_fd.sh
res = b64 ~= false and base64.decode(res) or res
-- safe=false enable call function
local _, obj = serpent.load(res, { safe = false })
assert(type(obj) == "table")
return obj
end
---@param fn_str any
---@return function?
M.load_fn = function(fn_str)
if type(fn_str) ~= "string" then return end
local fn_loaded = nil
local fn = loadstring(fn_str)
if fn then fn_loaded = fn() end
if type(fn_loaded) ~= "function" then
fn_loaded = nil
end
return fn_loaded
end
local posix_exec = function(cmd)
if type(cmd) ~= "string" or _is_win or not pcall(require, "ffi") then return end
require("ffi").cdef([[int execl(const char *, const char *, ...);]])
require("ffi").C.execl("/bin/sh", "sh", "-c", cmd, nil)
end
---@param opts fzf-lua.SpawnStdioOpts
---@return uv.uv_process_t?, integer?
M.spawn_stdio = function(opts)
-- stdin/stdout are already buffered, not stderr. This means
-- that every character is flushed immediately which caused
-- rendering issues on Mac (#316, #287) and Linux (#414)
-- switch 'stderr' stream to 'line' buffering
-- https://www.lua.org/manual/5.2/manual.html#pdf-file%3asetvbuf
io.stderr:setvbuf "line"
-- redirect 'stderr' to 'stdout' on Macs by default
-- only takes effect if 'opts.stderr' was not set
if opts.stderr_to_stdout == nil and
uv.os_uname().sysname == "Darwin" then
opts.stderr_to_stdout = true
end
-- setup global vars
for k, v in pairs(opts.g or {}) do _G[k] = v end
---@diagnostic disable-next-line: undefined-field
local EOL = _G._EOL or opts.multiline and "\0" or "\n"
-- Requiring make_entry will create the pseudo `_G.FzfLua` global
-- Must be called after global vars are created or devicons will
-- err with "fzf-lua fatal: '_G._fzf_lua_server', '_G._devicons_path' both nil"
pcall(require, "fzf-lua.make_entry")
-- still need load_fn from str val? now deserialize do all the thing automatically
-- or because we want to debugprint them, so we still make a string?
local fn_transform = M.load_fn(opts.fn_transform) or opts.fn_transform
local fn_preprocess = M.load_fn(opts.fn_preprocess) or opts.fn_preprocess
local fn_postprocess = M.load_fn(opts.fn_postprocess) or opts.fn_postprocess
local argv = function(i)
local idx = tonumber(i) or #_G.arg
local arg = _G.arg[idx]
if opts.debug == "v" or opts.debug == 2 then
io.stdout:write(("[DEBUG] raw_argv(%d) = %s" .. EOL):format(idx, arg))
end
-- TODO: maybe not needed anymore? since we're not using v:argv
if FzfLua.utils.__IS_WINDOWS then
arg = M.unescape_fzf(arg, FzfLua.utils.has(opts, "fzf", { 0, 52 }) and 0.52 or 0)
end
if opts.debug == "v" or opts.debug == 2 then
io.stdout:write(("[DEBUG] esc_argv(%d) = %s" .. EOL):format(idx, M.shellescape(arg)))
end
return arg
end
-- Since the `rg` command will be wrapped inside the shell escaped
-- 'nvim -l ...', we won't be able to search single quotes
-- NOTE: since we cannot guarantee the positional index
-- of arguments (#291), we use the last argument instead
if opts.is_live and type(opts.contents) == "string" then
opts.contents = FzfLua.make_entry.expand_query(opts, assert(argv()), opts.contents)
end
-- run the preprocessing fn
---@diagnostic disable-next-line: call-non-callable
if fn_preprocess then fn_preprocess(opts) end
---@type fzf-lua.content|fzf-lua.shell.data2?
local content = opts.contents
if type(content) == "string" and content:match("%-%-color[=%s]+never") then
-- perf: skip stripping ansi coloring in `make_file.entry`
opts.no_ansi_colors = true
end
if opts.debug == "v" or opts.debug == 2 then
for k, v in vim.spairs(opts) do
io.stdout:write(string.format("[DEBUG] %s=%s" .. EOL, k, vim.inspect(v)))
end
elseif opts.debug then
io.stdout:write("[DEBUG] [mt] " .. tostring(content) .. EOL)
end
local stderr, stdout = nil, nil
local function stderr_write(msg)
-- prioritize writing errors to stderr
if stderr then
stderr:write(msg)
else
io.stderr:write(msg)
end
end
-- Exit the fzf-lua spawn process; `vim.cmd.cquit` is typed `never` in
-- emmylua's stubs which makes `exit` itself uncallable from a `local
-- function` declaration, so wrap it as an annotated module-level alias.
---@type fun(exit_code?: integer, msg?: string)
local exit = function(exit_code, msg)
if msg then stderr_write(msg) end
vim.cmd.cquit({ count = exit_code })
end
local function pipe_open(pipename)
if not pipename then return end
local fd = assert(uv.fs_open(pipename, "w", -1))
if type(fd) ~= "number" then
exit(1, ("error opening '%s': %s" .. EOL):format(pipename, fd))
end
local pipe = assert(uv.new_pipe(false))
pipe:open(fd)
return pipe
end
local function pipe_close(pipe)
if pipe and not pipe:is_closing() then
pipe:close()
end
end
local function pipe_write(pipe, data, cb)
if not pipe or pipe:is_closing() then return end
pipe:write(data,
function(err)
-- if the user cancels the call prematurely with
-- <C-c>, err will be either EPIPE or ECANCELED
-- don't really need to do anything since the
-- processes will be killed anyways with os.exit()
if err then
stderr_write(("pipe:write error: %s" .. EOL):format(err))
end
if cb then cb(err) end
end)
end
if type(opts.stderr) == "string" then
stderr = pipe_open(opts.stderr)
end
if type(opts.stdout) == "string" then
stdout = pipe_open(opts.stdout)
end
local on_finish = function(code)
pipe_close(stdout)
pipe_close(stderr)
if vim.in_fast_event() then
vim.schedule(function()
---@diagnostic disable-next-line: call-non-callable
if fn_postprocess then fn_postprocess(opts) end
---@diagnostic disable-next-line: call-non-callable
exit(code)
end)
else
---@diagnostic disable-next-line: call-non-callable
if fn_postprocess then fn_postprocess(opts) end
exit(code)
end
end
local on_write = function(data, cb)
if stdout then
pipe_write(stdout, data, cb)
else
-- on success: rc=true, err=nil
-- on failure: rc=nil, err="Broken pipe"
-- cb with an err ends the process
local rc, err = io.stdout:write(data)
if not rc then
stderr_write(("io.stdout:write error: %s" .. EOL):format(err))
cb(err or true)
else
cb(nil)
end
end
end
local on_err = function(data)
if stderr then
pipe_write(stderr, data)
elseif opts.stderr ~= false then
if opts.stderr_to_stdout then
io.stdout:write(data)
else
io.stderr:write(data)
end
end
end
local args = function()
local items = vim.deepcopy(_G.arg)
items[0] = nil
table.remove(items, 1)
return items
end
local cmd ---@type string
if type(content) == "string" then
cmd = content
else
local f = fn_transform or function(x) return x end
---@diagnostic disable-next-line: call-non-callable, missing-parameter
local w = function(s) if s then io.stdout:write(f(s) .. EOL) else on_finish(0) end end
---@diagnostic disable-next-line: call-non-callable, missing-parameter, param-type-mismatch
local wn = function(s) if s then return io.stdout:write(f(s)) else on_finish(0) end end
if opts.is_live then ---@cast content fzf-lua.shell.data2
---@diagnostic disable-next-line: param-type-mismatch, assign-type-mismatch
local res = content(args(), opts)
if not res then return on_finish(0), nil end
---@cast res fzf-lua.shell.data2
content = res
end
if type(content) == "function" then content(w, wn) end
if type(content) == "table" then for _, v in ipairs(content) do w(v) end end
-- Table/function content was already written to stdout above, the only
-- content type that requires spawning a child process is a string command.
-- Without this guard, when `fn_postprocess` is set, `on_finish` defers
-- `os.exit` via `vim.schedule` and the code falls through to `M.spawn`
-- which would attempt to execute the table content as a shell command,
-- resulting in errors like "sh: 1: 1:: not found" appearing as fzf items.
if type(content) ~= "string" then return on_finish(0), nil end ---@cast content string
cmd = content
if opts.debug then
io.stdout:write(("[DEBUG] [mt] %s" .. EOL):format(content))
end
end
if not fn_transform and not fn_postprocess then posix_exec(content) end
return M.spawn({
cwd = opts.cwd,
cmd = cmd,
cb_finish = on_finish,
cb_write = on_write,
cb_err = on_err,
process1 = opts.process1,
profiler = opts.profiler,
EOL = EOL,
},
fn_transform and function(x)
---@diagnostic disable-next-line: call-non-callable
return fn_transform(x, opts)
end)
end
M.is_escaped = function(s, is_win)
local m
-- test spec override
if is_win == nil then is_win = _is_win end
if is_win then
m = s:match([[^".*"$]]) or s:match([[^%^".*%^"$]])
else
m = s:match([[^'.*'$]]) or s:match([[^".*"$]])
end
return m ~= nil
end
-- our own version of vim.fn.shellescape compatible with fish shells
-- * don't double-escape '\' (#340)
-- * if possible, replace surrounding single quote with double
-- from ':help shellescape':
-- If 'shell' contains "fish" in the tail, the "\" character will
-- be escaped because in fish it is used as an escape character
-- inside single quotes.
--
-- for windows, we assume we want to keep all quotes as literals
-- to avoid the quotes being stripped when run from fzf actions
-- we therefore have to escape the quotes with backslashes and
-- for nested quotes we double the backslashes due to windows
-- quirks, further reading:
-- https://stackoverflow.com/questions/6714165/powershell-stripping-double-quotes-from-command-line-arguments
-- https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
--
-- this function is a better fit for utils but we're
-- trying to avoid having any 'require' in this file
---@param s string
---@param win_style integer|string|boolean? force Windows escaping on posix:
--- 1 = classic CommandLineToArgvW backslash escape, 2 = caret escape with
--- delayed expansion double-escape, `true`/nil (on Windows) = caret escape
---@return string
M.shellescape = function(s, win_style)
if _is_win or win_style then
if tonumber(win_style) == 1 then
--
-- "classic" CommandLineToArgvW backslash escape
--
s = s:gsub([[\-"]], function(x)
-- Quotes found in string. From the above stackoverflow link:
--
-- (2n) + 1 backslashes followed by a quotation mark again produce n backslashes
-- followed by a quotation mark literal ("). This does not toggle the "in quotes"
-- mode.
--
-- to produce (2n)+1 backslashes we use the following `string.rep` calc:
-- (#x-1) * 2 + 1 - (#x-1) == #x
-- which translates to prepending the string with number of escape chars
-- (\) equal to its own length, this in turn is an **always odd** number
--
-- " -> \" (0->1)
-- \" -> \\\" (1->3)
-- \\" -> \\\\\" (2->5)
-- \\\" -> \\\\\\\" (3->7)
-- \\\\" -> \\\\\\\\\" (4->9)
--
x = string.rep([[\]], #x) .. x
return x
end)
s = s:gsub([[\+$]], function(x)
-- String ends with backslashes. From the above stackoverflow link:
--
-- 2n backslashes followed by a quotation mark again produce n backslashes
-- followed by a begin/end quote. This does not become part of the parsed
-- argument but toggles the "in quotes" mode.
--
-- c:\foo\ -> "c:\foo\" // WRONG
-- c:\foo\ -> "c:\foo\\" // RIGHT
-- c:\foo\\ -> "c:\foo\\" // WRONG
-- c:\foo\\ -> "c:\foo\\\\" // RIGHT
--
-- To produce equal number of backslashes without converting the ending quote
-- to a quote literal, double the backslashes (2n), **always even** number
x = string.rep([[\]], #x * 2)
return x
end)
return [["]] .. s .. [["]]
else
--
-- CMD.exe caret+backslash escape, after lot of trial and error
-- this seems to be the winning logic, a combination of v1 above
-- and caret escaping special chars
--
-- The logic is as follows
-- (1) all escaped quotes end up the same \^"
-- (1) if quote was prepended with backslash or backslash+caret
-- the resulting number of backslashes will be 2n + 1
-- (2) if caret exists between the backslash/quote combo, move it
-- before the backslash(s)
-- (4) all cmd special chars are escaped with ^
--
-- NOTE: explore "tests/libuv_spec.lua" to see examples of quoted
-- combinations and their expecetd results
--
local escape_inner = function(inner)
inner = inner:gsub([[\-%^?"]], function(x)
-- although we currently only transfer 1 caret, the below
-- can handle any number of carets with the regex [[\-%^-"]]
local carets = x:match("%^+") or ""
x = carets .. string.rep([[\]], #x - #(carets)) .. x:gsub("%^+", "")
return x
end)
-- escape all windows metacharacters but quotes
-- ( ) % ! ^ < > & | ; "
-- TODO: should % be escaped with ^ or %?
inner = inner:gsub('[%(%)%%!%^<>&|;%s"]', function(x)
return "^" .. x
end)
-- escape backslashes at the end of the string
inner = inner:gsub([[\+$]], function(x)
x = string.rep([[\]], #x * 2)
return x
end)
return inner
end
s = escape_inner(s)
if s:match("!") and tonumber(win_style) == 2 then
--
-- https://ss64.com/nt/syntax-esc.html
-- This changes slightly if you are running with DelayedExpansion of variables:
-- if any part of the command line includes an '!' then CMD will escape a second
-- time, so ^^^^ will become ^
--
-- NOTE: we only do this on demand (currently only used in "libuv_spec.lua")
--
s = escape_inner(s)
end
s = [[^"]] .. s .. [[^"]]
return s
end
end
local shell = vim.o.shell
if not shell or not shell:match("fish$") then
return vim.fn.shellescape(s)
else
local ret = nil
vim.o.shell = "sh"
if s and not s:match([["]]) and not s:match([[\]]) then
-- if the original string does not contain double quotes,
-- replace surrounding single quote with double quotes,
-- temporarily replace all single quotes with double
-- quotes and restore after the call to shellescape.
-- NOTE: we use '({s:gsub(...)})[1]' to extract the
-- modified string without the multival # of changes,
-- otherwise the number will be sent to shellescape
-- as {special}, triggering an escape for ! % and #
ret = vim.fn.shellescape(({ s:gsub([[']], [["]]) })[1])
ret = [["]] .. ret:gsub([["]], [[']]):sub(2, #ret - 1) .. [["]]
else
ret = vim.fn.shellescape(s)
end
vim.o.shell = shell
return ret
end
end
-- Parse a `cmd.exe` command line fragment into argv values, exactly as
-- `cmd.exe` + the Win32 argv parser (Go runtime; fzf is a Go binary) would
-- deliver them. User provided argument strings (`fzf_args`, `fzf_cli_args`,
-- etc.) are written for the platform shell: on Windows they are cmd.exe
-- fragments, typically pre-escaped with `shellescape` (caret style). The
-- Windows direct spawn route (`libuv.win_spawn`) passes the raw argv to
-- `uv.spawn` with no shell, so this reverses the parsing passes:
-- 1. `%NAME%` expansion (cmd.exe semantics; differs from batch scripts):
-- a defined name (case-insensitive) expands to its value, which is
-- not rescanned; an undefined name is left as text and the scan
-- resumes one char later, so a later `%` may still pair (`%%TEMP%%`
-- -> `%TEMP%`). A `shellescape`-escaped value carries a caret inside
-- the scanned name (`^%NAME^%`), never matches and survives for the
-- caret pass below. Not modelled: `%VAR:~x,y%` substitutions and
-- cmd's pseudo vars (`%CD%` etc.); verified against `cmd.exe`
-- 2. cmd.exe caret consumption: `^X` -> `X` (and thus `^^` -> `^`); a
-- caret is only special outside of double quotes, inside quotes it is
-- literal and an unescaped `"` toggles the quoted state (this is what
-- makes the `^"...^"` shellescape form safe: the escaped surrounding
-- quotes are emitted literally and never enter the quoted state, so
-- the inner caret escapes still apply)
-- 3. Win32 argv parsing as implemented by the Go runtime
-- (`os/exec_windows.go`; fzf is a Go binary): quotes toggle and are
-- dropped, a doubled `""` inside a quoted section is one literal quote
-- ("Prior to 2008" rule; the pair is consumed and the quote state
-- toggles once), backslash runs before quotes collapse (`2n` backslashes
-- followed by a quote -> `n` backslashes + toggle quote mode, `2n+1` ->
-- `n` backslashes + literal quote), unquoted whitespace splits args
---@param s string cmd.exe command line fragment
---@param env table<string,string>? variable environment (defaults to
--- `vim.fn.environ()`, the environment `cmd.exe` would be spawned with)
---@return string[] argv
M.cmd_argv = function(s, env)
if not s or s == "" then return {} end
-- (1) expand defined `%NAME%` variables (`cmd.exe` performs this before
-- everything else; env lookups are case-insensitive on Windows)
env = env or vim.fn.environ()
local lookup = function(name)
if name == "" then return nil end
if env[name] ~= nil then return env[name] end
name = name:lower()
for k, v in pairs(env) do
if k:lower() == name then return v end
end
return nil
end
do
local out, i, slen = {}, 1, #s
while i <= slen do
if s:sub(i, i) == "%" then
local j = s:find("%", i + 1, true)
local val = j and lookup(s:sub(i + 1, j - 1)) or nil
if j and val then
out[#out + 1] = val
i = j + 1
elseif j then
-- undefined: leave this `%` for the next scan step (the closing
-- `%` can pair with a later one, as `cmd.exe` does with `%%X%%`)
out[#out + 1] = "%"
i = i + 1
else
out[#out + 1] = s:sub(i)
i = slen + 1
end
else
out[#out + 1] = s:sub(i, i)
i = i + 1
end
end
s = table.concat(out)
end
-- (2) consume cmd.exe caret escapes; a caret is only special outside of
-- double quotes. An unescaped `"` toggles the quoted state, while a caret
-- escaped `^"` is emitted literally and does not toggle it (the emitted
-- quote is re-interpreted by the argv pass below)
local parts = {}
local i, len = 1, #s
local quoted = false
while i <= len do
local c = s:sub(i, i)
if c == "^" and not quoted then
if i == len then break end -- dangling caret is dropped by cmd.exe
parts[#parts + 1] = s:sub(i + 1, i + 1)
i = i + 2
else
if c == [["]] then quoted = not quoted end
parts[#parts + 1] = c
i = i + 1
end
end
s = table.concat(parts)
-- (3) parse the unescaped command line into argv values
local argv = {}
local j = 1
len = #s
while j <= len do
local sp = s:sub(j, j)
if sp == " " or sp == "\t" then
j = j + 1
else
local buf, in_quotes = {}, false
while j <= len do
local c = s:sub(j, j)
if c == "\\" then
local k = j
while k <= len and s:sub(k, k) == "\\" do k = k + 1 end
local nback = k - j
if k <= len and s:sub(k, k) == '"' then
buf[#buf + 1] = string.rep("\\", math.floor(nback / 2))
if nback % 2 == 1 then
buf[#buf + 1] = '"'
j = k + 1
else
-- even run: emit backslashes, let the quote branch below apply
-- the doubling rule / toggle for the quote itself
j = k
end
else
buf[#buf + 1] = string.rep("\\", nback)
j = k
end
elseif c == '"' then
-- Go runtime `os/exec_windows.go`: a `""` inside a quoted section
-- is one literal quote ("Prior to 2008" rule); the pair is consumed
-- and the quote state toggles once (matches `fzf.exe` via `cmd.exe`)
if in_quotes and s:sub(j + 1, j + 1) == '"' then
buf[#buf + 1] = '"'
j = j + 2
else
j = j + 1
end
in_quotes = not in_quotes
elseif not in_quotes and (c == " " or c == "\t") then
break
else
buf[#buf + 1] = c
j = j + 1
end
end
argv[#argv + 1] = table.concat(buf)
end
end
return argv
end
---@class fzf-lua.CliSegment
---@field text string -- raw (unescaped) text
---@field escape boolean -- true = escape this text for the target shell
-- One argv element is a list of segments (usually 1; 2 for a fused "flag=value"
-- or "--bind=<value>" string where the flag prefix is literal and the value is
-- escaped). This lets a single final pass escape only the value segments,
-- per target shell, without double-escaping literal flag text.
---@alias fzf-lua.CliElement fzf-lua.CliSegment[]
-- Escape a list of CLI elements for the given shell, re-fusing each element's
-- segments into a single string. Both shells reuse `shellescape` (`cmd` uses
-- the caret/win_style variant). Literal segments (`escape=false`) are emitted
-- verbatim.
---@param elements fzf-lua.CliElement[]
---@param shell "posix"|"cmd"
---@param win_style integer|string|boolean? forwarded to shellescape for "cmd"
---@return string[]
M.escape_cli = function(elements, shell, win_style)
local escapefn = function(s)
return M.shellescape(s, shell == "cmd" and win_style or nil)
end
local argv = {}
for _, elem in ipairs(elements) do
local buf = {}
for _, seg in ipairs(elem) do
table.insert(buf, seg.escape and escapefn(seg.text) or seg.text)
end
table.insert(argv, table.concat(buf))
end
return argv
end
-- Marker for `_fzf_cli_args` entries that carry a raw (unescaped) value
-- alongside the escaped string form. `build_fzf_cli` consumes these to emit
-- BOTH the legacy escaped string (for cmd/posix, byte-identical) and a raw
-- CliElement segment list -- `fzf.raw_fzf` rebuilds the raw argv from the
-- segments for the Windows direct spawn (`libuv.win_spawn`, no shell and no
-- escaping; shares `fzf-tmux` and the CLI launcher equally). `prefix` is the
-- literal, never-escaped flag text (e.g. "--bind=" or "--interactive --cmd ").
---@param prefix string literal flag prefix (e.g. "--bind=")
---@param value string raw (unescaped) value
---@return table marker
M.cli_raw_arg = function(prefix, value)
return { __raw_cli = true, prefix = prefix, value = value }
end
-- Windows fzf oddities, fzf's {q} will send escaped blackslahes,
-- but only when the backslash prefixes another character which
-- isn't a backslash, test with:
-- fzf --disabled --height 30% --preview-window up --preview "echo {q}"
M.unescape_fzf = function(s, fzf_version, is_win)
if is_win == nil then is_win = _is_win end
if not is_win then return s end
if tonumber(fzf_version) and tonumber(fzf_version) >= 0.52 then return s end
local ret = s:gsub("\\+[^\\]", function(x)
local bslash_num = #x:match([[\+]])
return string.rep([[\]],
bslash_num == 1 and bslash_num or math.floor(bslash_num / 2)) .. x:sub(-1)
end)
return ret
end
-- with live_grep, we use a modified "reload" command as our
-- FZF_DEFAULT_COMMAND and due to the above oddity with fzf
-- doing weird extra escaping with {q}, we use this to simulate
-- {q} being sent via the reload action as the initial command
-- TODO: better solution for these stupid hacks (upstream issues?)
M.escape_fzf = function(s, fzf_version, is_win)
if is_win == nil then is_win = _is_win end
if not is_win then return s end
if tonumber(fzf_version) and tonumber(fzf_version) >= 0.52 then return s end
local ret = s:gsub("\\+[^\\]", function(x)
local bslash_num = #x:match([[\+]])
return string.rep([[\]], bslash_num * 2) .. x:sub(-1)
end)
return ret
end
-- `vim.fn.escape`
-- (1) On *NIX: double the backslashes as they will be reduced by expand
-- (2) ... other issues we will surely find with special chars
M.expand = function(s)
if not _is_win then
s = s:gsub([[\]], [[\\]])
end
return vim.fn.expand(s)
end
return M