From e8777aace40f918bdd0141706c1d55fe5c667197 Mon Sep 17 00:00:00 2001 From: TheLeoP Date: Wed, 20 Dec 2023 11:35:03 -0500 Subject: [PATCH] explore: add windows support --- .luarc.json | 75 ++++++++------- lua/fzf-lua/actions.lua | 4 +- lua/fzf-lua/config.lua | 8 ++ lua/fzf-lua/core.lua | 59 +++++++++--- lua/fzf-lua/fzf.lua | 154 ++++++++++++++++++++++++------ lua/fzf-lua/init.lua | 1 + lua/fzf-lua/libuv.lua | 42 ++++++-- lua/fzf-lua/make_entry.lua | 17 +++- lua/fzf-lua/path.lua | 10 +- lua/fzf-lua/previewer/builtin.lua | 1 + lua/fzf-lua/providers/grep.lua | 9 +- lua/fzf-lua/providers/module.lua | 2 + lua/fzf-lua/shell.lua | 6 +- lua/fzf-lua/shell_helper.lua | 20 ++-- lua/fzf-lua/utils.lua | 34 ++++++- lua/fzf-lua/win.lua | 4 + 16 files changed, 346 insertions(+), 100 deletions(-) diff --git a/.luarc.json b/.luarc.json index 124e64e5..902595e5 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,39 +1,42 @@ { - "runtime.version": "LuaJIT", - "diagnostics": { - "enable": true, - "globals": [ - "vim", - "describe", - "pending", - "it", - "before_each", - "after_each", - ], - "neededFileStatus": { - "codestyle-check": "Any" - }, - "disable": [ - "need-check-nil", - "missing-parameter", - "cast-local-type", - ], + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "runtime.version": "LuaJIT", + "diagnostics": { + "enable": true, + "globals": [ + "vim", + "describe", + "pending", + "it", + "before_each", + "after_each" + ], + "neededFileStatus": { + "codestyle-check": "Any" }, - "workspace": { - "library": [ - "$VIMRUNTIME/lua", - ], - "checkThirdParty": false, - "maxPreload": 2000, - "preloadFileSize": 1000, - "ignoreDir": [ - "tests/", - ], - }, - "type": { - "weakNilCheck": true, - "weakUnionCheck": true, - "castNumberToInteger": true, - }, - "telemetry.enable": false + "disable": [ + "need-check-nil", + "missing-parameter", + "cast-local-type" + ] + }, + "workspace": { + "library": [ + "$VIMRUNTIME/lua", + "lua", + "${3rd}/luv/library" + ], + "checkThirdParty": false, + "maxPreload": 2000, + "preloadFileSize": 1000, + "ignoreDir": [ + "tests/" + ] + }, + "type": { + "weakNilCheck": true, + "weakUnionCheck": true, + "castNumberToInteger": true + }, + "telemetry.enable": false } diff --git a/lua/fzf-lua/actions.lua b/lua/fzf-lua/actions.lua index 0d8d12d0..22403370 100644 --- a/lua/fzf-lua/actions.lua +++ b/lua/fzf-lua/actions.lua @@ -816,8 +816,10 @@ M.set_qflist = function(selected, opts) vim.cmd(opts._is_loclist and "lopen" or "copen") end +---@param selected string[] +---@param opts table M.apply_profile = function(selected, opts) - local fname = selected[1]:match("[^:]+") + local fname = utils.__IS_WINDOWS and selected[1]:match("%u?:?[^:]+") or selected[1]:match("[^:]+") local profile = selected[1]:match(":([^%s]+)") local ok = utils.load_profile(fname, profile, opts.silent) if ok then diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua index e3dc8e8a..ac0285b3 100644 --- a/lua/fzf-lua/config.lua +++ b/lua/fzf-lua/config.lua @@ -10,6 +10,7 @@ if utils.__HAS_DEVICONS then -- get the devicons module path M._devicons_path = M._has_devicons and M._devicons and M._devicons.setup and debug.getinfo(M._devicons.setup, "S").source:gsub("^@", "") + if utils.__IS_WINDOWS then M._devicons_path = vim.fs.normalize(M._devicons_path) end end M._diricon_escseq = function() @@ -120,6 +121,8 @@ M.resume_set = function(what, val, opts) -- _G.dump("resume_set", key1, utils.map_get(M, key1)) end +---@param opts {resume: boolean, __call_opts: table} +---@return table function M.resume_opts(opts) assert(opts.resume and opts.__call_opts) local __call_opts = M.resume_get(nil, opts) @@ -191,6 +194,9 @@ do m.globals = M.globals end +---@param opts table|fun():table? +---@param globals string|table? +---@param __resume_key string? function M.normalize_opts(opts, globals, __resume_key) if not opts then opts = {} end @@ -236,6 +242,8 @@ function M.normalize_opts(opts, globals, __resume_key) end -- normalize all binds as lowercase or we can have duplicate keys (#654) + ---@param m {fzf: table, builtin: table} + ---@return {fzf: table, builtin: table}? local keymap_tolower = function(m) return m and { fzf = utils.map_tolower(m.fzf), diff --git a/lua/fzf-lua/core.lua b/lua/fzf-lua/core.lua index 3f8a86fa..d467e248 100644 --- a/lua/fzf-lua/core.lua +++ b/lua/fzf-lua/core.lua @@ -101,8 +101,12 @@ local contents_from_arr = function(cont_arr) return contents end +---@alias content table|function|string + -- Main API, see: -- https://github.com/ibhagwan/fzf-lua/wiki/Advanced +---@param contents content +---@param opts {fn_reload: string|function, fn_transform: function, __fzf_init_cmd: string, _normalized: boolean} M.fzf_exec = function(contents, opts) if type(contents) == "table" and type(contents[1]) == "table" then contents = contents_from_arr(contents) @@ -142,7 +146,7 @@ M.fzf_exec = function(contents, opts) -- the caller requested to transform, we need to convert -- to a function that returns string so that libuv.spawn -- is called - local cmd = opts.fn_reload + local cmd = opts.fn_reload --[[@as string]] opts.fn_reload = function(q) if cmd:match(M.fzf_query_placeholder) then return cmd:gsub(M.fzf_query_placeholder, q or "") @@ -181,6 +185,10 @@ M.fzf_resume = function(opts) M.fzf_exec(config.__resume_data.contents, opts) end +---@param opts table +---@param contents content +---@param fn_selected function? +---@return function M.fzf_wrap = function(opts, contents, fn_selected) opts = opts or {} return coroutine.wrap(function() @@ -241,6 +249,9 @@ M.CTX = function(includeBuflist) return M.__CTX end +---@param contents content +---@param opts table? +---@return string[]? M.fzf = function(contents, opts) -- Disable opening from the command-line window `:q` -- creates all kinds of issues, will fail on `nvim_win_close` @@ -404,7 +415,8 @@ M.fzf = function(contents, opts) return selected end - +---@param o table +---@return string M.preview_window = function(o) local preview_args = ("%s:%s:%s:"):format( o.winopts.preview.hidden, o.winopts.preview.border, o.winopts.preview.wrap) @@ -456,11 +468,7 @@ M.create_fzf_binds = function(binds) if type(v) == "table" then v = v[1] end - -- backward compatibility to when binds - -- where defined as one string ':' if v then - local key, action = v:match("(.*):(.*)") - if action then k, v = key, action end dedup[k] = v end end @@ -470,6 +478,8 @@ M.create_fzf_binds = function(binds) return vim.fn.shellescape(table.concat(tbl, ",")) end +---@param opts table +---@return string M.build_fzf_cli = function(opts) opts.fzf_opts = vim.tbl_extend("force", config.globals.fzf_opts, opts.fzf_opts or {}) -- copy from globals @@ -569,6 +579,9 @@ M.build_fzf_cli = function(opts) elseif type(v) == "number" then -- convert to string v = string.format("%d", v) + elseif utils.__IS_WINDOWS and + type(v) == "string" and k == "--delimiter" and v:sub(1, 1) == "'" and v:sub(#v, #v) == "'" then + v = '"' .. v:sub(2, #v - 1) .. '"' end if v then v = v:gsub(k .. "=", "") @@ -579,15 +592,21 @@ M.build_fzf_cli = function(opts) return cli_args .. extra_args end +---@param opts table +---@return string|function M.mt_cmd_wrapper = function(opts) assert(opts and opts.cmd) + ---@param s string + ---@return string local str_to_str = function(s) -- use long format of bracket escape so we can include "]" (#925) -- https://www.lua.org/manual/5.4/manual.html#3.1 return "[==[" .. s .. "]==]" end + ---@param o table + ---@return string local opts_to_str = function(o) local names = { "debug", @@ -648,7 +667,9 @@ M.mt_cmd_wrapper = function(opts) -- due to fzf replacing ' with \ (no idea why) if not opts.no_remote_config then fn_transform = ([[_G._fzf_lua_server=%s; %s]]):format( - libuv.shellescape(vim.g.fzf_lua_server), + -- since the server adress is passed inside of `[[]]`, single `\` + -- gives an error when trying to eval the string as lua code + libuv.shellescape(utils.__IS_WINDOWS and vim.g.fzf_lua_server:gsub("\\", "\\\\") or vim.g.fzf_lua_server), fn_transform) end if config._devicons_setup then @@ -705,10 +726,15 @@ end M.set_header = function(opts, hdr_tbl) local function normalize_cwd(cwd) - if path.starts_with_separator(cwd) and cwd ~= vim.loop.cwd() then + local _cwd = vim.loop.cwd() + if utils.__IS_WINDOWS then + cwd = vim.fs.normalize(cwd) + _cwd = vim.fs.normalize(_cwd) + end + if path.starts_with_separator(cwd) and cwd ~= _cwd then -- since we're always converting cwd to full path -- try to convert it back to relative for display - cwd = path.relative(cwd, vim.loop.cwd()) + cwd = path.relative(cwd, _cwd) end -- make our home dir path look pretty return path.HOME_to_tilde(cwd) @@ -838,9 +864,12 @@ end -- converts actions defined with "reload=true" to use fzf's `reload` bind -- provides a better UI experience without a visible interface refresh +---@param reload_cmd content +---@param opts table +---@return table M.convert_reload_actions = function(reload_cmd, opts) - local fallback - local has_reload + local fallback ---@type boolean? + local has_reload ---@type boolean? if opts._is_skim or type(reload_cmd) ~= "string" then fallback = true end @@ -923,6 +952,8 @@ end -- converts actions defined inside 'silent_actions' to use fzf's 'execute-silent' -- bind, these actions will not close the UI, e.g. commits|bcommits yank commit sha +---@param opts table +---@return table M.convert_exec_silent_actions = function(opts) if opts._is_skim then return opts @@ -947,6 +978,10 @@ M.convert_exec_silent_actions = function(opts) return opts end +---@param command string +---@param fzf_field_expression string +---@param opts table +---@return table M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts) -- query cannot be 'nil' opts.query = opts.query or "" @@ -1018,6 +1053,8 @@ end -- query placeholder for "live" queries M.fzf_query_placeholder = "" +---@param opts {_is_skim: boolean} +---@return string M.fzf_field_expression = function(opts) -- fzf already adds single quotes around the placeholder when expanding. -- for skim we surround it with double quotes or single quote searches fail diff --git a/lua/fzf-lua/fzf.lua b/lua/fzf-lua/fzf.lua index f6f71fa6..641bd42f 100644 --- a/lua/fzf-lua/fzf.lua +++ b/lua/fzf-lua/fzf.lua @@ -5,6 +5,8 @@ -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf.lua local uv = vim.loop +local utils = require "fzf-lua.utils" + local M = {} -- workaround to a potential 'tempname' bug? (#222) @@ -27,9 +29,72 @@ local function tempname() return tmpname end +---Escaping `"` inside a pair of `"` by converting it to `""` as vim.fn.shellescape does, doesn't work. +--- +---This function escapes `"` inside a pair of `"` doing the following: +--- - If there is two quotes (`""`), it's interpreted as a quote that should be sended to the nested +--- cmd intance spawned by fzf, so it becomes `\"`. E.g. `--cwd=""C:/users/some path/with spaces""` +--- -> `--cwd=\"C:/users/some path/with spaces\"` +--- - If there is at least three quotes (`"""`), it's interpreted as a lua string (should be ignored +--- by cmd), so it becomes `"""""""""`. E.g. `require(""""make_entry"""")` -> `require("""""""""make_entry""""""""")` +---@param str string +---@return string +local function windows_cmd_escape(str) + ---@type string[] + local out = {} + + local inside_quotes = false + + local quote = string.byte('"') + + ---@type integer? + local last_quote + + local n = 1 + while n <= #str do + local previous = str:byte(n - 1) + local current = str:byte(n) + local next = str:byte(n + 1) + local next_next = str:byte(n + 2) + + if inside_quotes and current == quote and next ~= quote and (previous ~= quote or last_quote == n - 1) then + -- current is closing quote + inside_quotes = false + last_quote = nil + table.insert(out, string.char(current)) + elseif not inside_quotes and current == quote then + -- current is opening quote + inside_quotes = true + last_quote = n + table.insert(out, string.char(current)) + elseif inside_quotes and current == quote and next == quote and next_next == quote then + -- current is lua string + + while next == quote do + n = n + 1 + next = str:byte(n + 1) + end + + table.insert(out, '"""""""""') -- needed because the quotes have to go through 2 cmd.exe + elseif inside_quotes and current == quote and next == quote then + -- current is nested quote + table.insert(out, "\\") + else + table.insert(out, string.char(current)) + end + n = n + 1 + end + return table.concat(out) +end + -- contents can be either a table with tostring()able items, or a function that -- can be called repeatedly for values. The latter can use coroutines for async -- behavior. +---@param contents string[]|table|function? +---@param fzf_cli_args string? +---@param opts table +---@return table selected +---@return integer exit_code function M.raw_fzf(contents, fzf_cli_args, opts) if not coroutine.running() then error("[Fzf-lua] function must be called inside a coroutine.") @@ -38,7 +103,7 @@ function M.raw_fzf(contents, fzf_cli_args, opts) if not opts then opts = {} end local cwd = opts.fzf_cwd or opts.cwd local cmd = opts.fzf_bin or "fzf" - local fifotmpname = tempname() + local fifotmpname = utils.__IS_WINDOWS and utils.windows_pipename() or tempname() local outputtmpname = tempname() -- we use a temporary env $FZF_DEFAULT_COMMAND instead of piping @@ -81,12 +146,25 @@ function M.raw_fzf(contents, fzf_cli_args, opts) local fd, output_pipe = nil, nil local finish_called = false local write_cb_count = 0 + local windows_pipe_server = nil + ---@type function|nil + local handle_contents - -- Create the output pipe - -- We use tbl for perf reasons, from ':help system': - -- If {cmd} is a List it runs directly (no 'shell') - -- If {cmd} is a String it runs in the 'shell' - vim.fn.system({ "mkfifo", fifotmpname }) + if utils.__IS_WINDOWS then + windows_pipe_server = uv.new_pipe(false) + windows_pipe_server:bind(fifotmpname) + windows_pipe_server:listen(16, function() + output_pipe = uv.new_pipe(false) + windows_pipe_server:accept(output_pipe) + handle_contents() + end) + else + -- Create the output pipe + -- We use tbl for perf reasons, from ':help system': + -- If {cmd} is a List it runs directly (no 'shell') + -- If {cmd} is a String it runs in the 'shell' + vim.fn.system({ "mkfifo", fifotmpname }) + end local function finish(_) -- mark finish once called @@ -147,6 +225,23 @@ function M.raw_fzf(contents, fzf_cli_args, opts) end end + handle_contents = vim.schedule_wrap(function() + -- this part runs in the background. When the user has selected, it will + -- error out, but that doesn't matter so we just break out of the loop. + if contents then + if type(contents) == "table" then + if not vim.tbl_isempty(contents) then + write_cb(vim.tbl_map(function(x) + return x .. "\n" + end, contents)) + end + finish(4) + else + contents(usr_write_cb(true), usr_write_cb(false), output_pipe) + end + end + end) + -- I'm not sure why this happens (probably a neovim bug) but when pressing -- in quick successsion immediately after opening the window neovim -- hangs the CPU at 100% at the last `coroutine.yield` before returning from @@ -202,11 +297,21 @@ function M.raw_fzf(contents, fzf_cli_args, opts) local co = coroutine.running() local jobstart = opts.is_fzf_tmux and vim.fn.jobstart or vim.fn.termopen - jobstart({ "sh", "-c", cmd }, { + local shell = utils.__IS_WINDOWS and "cmd" or "sh" + ---@type string + local shell_cmd + if utils.__IS_WINDOWS then + cmd = windows_cmd_escape(cmd) + shell_cmd = { shell, "/d", "/e:off", "/f:off", "/v:off", "/c", cmd } + else + shell_cmd = { shell, "-c", cmd } + end + + jobstart(shell_cmd, { cwd = cwd, pty = true, env = { - ["SHELL"] = "sh", + ["SHELL"] = shell, ["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND, ["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND, }, @@ -220,7 +325,11 @@ function M.raw_fzf(contents, fzf_cli_args, opts) f:close() end finish(1) - vim.fn.delete(fifotmpname) + if windows_pipe_server then + windows_pipe_server:close() + end + -- in windows, pipes that are not used are automatically cleaned up + if not utils.__IS_WINDOWS then vim.fn.delete(fifotmpname) end vim.fn.delete(outputtmpname) if #output == 0 then output = nil end coroutine.resume(co, output, rc) @@ -260,26 +369,17 @@ function M.raw_fzf(contents, fzf_cli_args, opts) goto wait_for_fzf end - -- have to open this after there is a reader (termopen) - -- otherwise this will block - fd = uv.fs_open(fifotmpname, "w", -1) - output_pipe = uv.new_pipe(false) - output_pipe:open(fd) - -- print(output_pipe:getpeername()) - - -- this part runs in the background. When the user has selected, it will - -- error out, but that doesn't matter so we just break out of the loop. - if contents then - if type(contents) == "table" then - if not vim.tbl_isempty(contents) then - write_cb(vim.tbl_map(function(x) return x .. "\n" end, contents)) - end - finish(4) - else - contents(usr_write_cb(true), usr_write_cb(false), output_pipe) - end + if not utils.__IS_WINDOWS then + -- have to open this after there is a reader (termopen) + -- otherwise this will block + fd = uv.fs_open(fifotmpname, "w", -1) + output_pipe = uv.new_pipe(false) + output_pipe:open(fd) + -- print(output_pipe:getpeername()) + handle_contents() end + ::wait_for_fzf:: return coroutine.yield() end diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua index 7f9765c2..4ed78ea6 100644 --- a/lua/fzf-lua/init.lua +++ b/lua/fzf-lua/init.lua @@ -7,6 +7,7 @@ do -- plugin '.vim' initialization sometimes doesn't get called local currFile = debug.getinfo(1, "S").source:gsub("^@", "") vim.g.fzf_lua_directory = path.parent(currFile) + if utils.__IS_WINDOWS then vim.g.fzf_lua_directory = vim.fs.normalize(vim.g.fzf_lua_directory) end -- Manually source the vimL script containing ':FzfLua' cmd if not vim.g.loaded_fzf_lua then diff --git a/lua/fzf-lua/libuv.lua b/lua/fzf-lua/libuv.lua index 7b0f675a..957bd4a3 100644 --- a/lua/fzf-lua/libuv.lua +++ b/lua/fzf-lua/libuv.lua @@ -1,5 +1,7 @@ local uv = vim.loop +local is_windows = vim.fn.has("win32") == 1 + local M = {} -- path to current file @@ -97,6 +99,9 @@ local function coroutinify(fn) end end +---@param opts {cwd: string, cmd: string, cb_finish: function, cb_write: function, cb_pid: function, fn_transform: function?} +---@param fn_transform function? +---@param fn_done function? M.spawn = function(opts, fn_transform, fn_done) local output_pipe = uv.new_pipe(false) local error_pipe = uv.new_pipe(false) @@ -119,10 +124,14 @@ M.spawn = function(opts, fn_transform, fn_done) -- https://github.com/luvit/luv/blob/master/docs.md -- uv.spawn returns tuple: handle, pid - local handle, pid = uv.spawn("sh", { - args = { "-c", opts.cmd }, + local shell = is_windows and "cmd" or "sh" + local args = is_windows and { "/d", "/e:off", "/f:off", "/v:off", "/c", opts.cmd } + or { "-c", opts.cmd } + local handle, pid = uv.spawn(shell, { + args = args, stdio = { nil, output_pipe, error_pipe }, - cwd = opts.cwd + cwd = opts.cwd, + verbatim = is_windows, }, function(code, signal) output_pipe:read_stop() error_pipe:read_stop() @@ -256,7 +265,9 @@ end M.async_spawn = coroutinify(M.spawn) - +---@param opts {cmd: string, cwd: string, cb_pid: function, cb_finish: function, cb_write: function} +---@param fn_transform function? +---@param fn_preprocess function? M.spawn_nvim_fzf_cmd = function(opts, fn_transform, fn_preprocess) assert(not fn_transform or type(fn_transform) == "function") @@ -292,7 +303,12 @@ M.spawn_nvim_fzf_cmd = function(opts, fn_transform, fn_preprocess) end end +---@param opts table +---@param fn_transform string +---@param fn_preprocess string M.spawn_stdio = function(opts, fn_transform, fn_preprocess) + ---@param fn_str string + ---@return function? local function load_fn(fn_str) if type(fn_str) ~= "string" then return end local fn_loaded = nil @@ -324,7 +340,6 @@ M.spawn_stdio = function(opts, fn_transform, fn_preprocess) -- run the preprocessing fn if fn_preprocess then fn_preprocess(opts) end - if opts.debug then io.stdout:write("[DEBUG]: " .. opts.cmd .. "\n") end @@ -469,23 +484,34 @@ M.shellescape = function(s) end end +---@param opts string +---@param fn_transform string? +---@param fn_preprocess string? +---@return string M.wrap_spawn_stdio = function(opts, fn_transform, fn_preprocess) assert(opts and type(opts) == "string") assert(not fn_transform or type(fn_transform) == "string") local nvim_bin = os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath local nvim_runtime = os.getenv("FZF_LUA_NVIM_BIN") and "" - or string.format("VIMRUNTIME=%s ", M.shellescape(vim.env.VIMRUNTIME)) + or string.format( + is_windows and 'set "VIMRUNTIME=%s" & ' or "VIMRUNTIME=%s ", + is_windows and vim.fs.normalize(vim.env.VIMRUNTIME) or M.shellescape(vim.env.VIMRUNTIME) + ) local call_args = opts for _, fn in ipairs({ fn_transform, fn_preprocess }) do if type(fn) == "string" then call_args = ("%s,[[%s]]"):format(call_args, fn) end end + local cmd = ("lua loadfile([[%s]])().spawn_stdio(%s)"):format( + is_windows and vim.fs.normalize(__FILE__) or __FILE__, + call_args + ) local cmd_str = ("%s%s -n --headless --clean --cmd %s"):format( nvim_runtime, M.shellescape(nvim_bin), - M.shellescape(("lua loadfile([[%s]])().spawn_stdio(%s)") - :format(__FILE__, call_args))) + M.shellescape(cmd) + ) return cmd_str end diff --git a/lua/fzf-lua/make_entry.lua b/lua/fzf-lua/make_entry.lua index bddccfc1..4fb39bd7 100644 --- a/lua/fzf-lua/make_entry.lua +++ b/lua/fzf-lua/make_entry.lua @@ -259,6 +259,10 @@ M.get_diff_files = function(opts) return diff_files end +---@param query string +---@param opts table +---@return string search_query +---@return string? glob_args M.glob_parse = function(query, opts) if not query or not query:find(opts.glob_separator) then return query, nil @@ -278,6 +282,10 @@ end -- reposition args before ` -e ` or ` -- ` -- enables "-e" and "--fixed-strings --" in `rg_opts` (#781, #794) +---@param cmd string +---@param args string +---@param relocate_pattern string? +---@return string M.rg_insert_args = function(cmd, args, relocate_pattern) local patterns = {} for _, a in ipairs({ @@ -361,7 +369,10 @@ M.preprocess = function(opts) opts.cmd = opts.cmd:gsub("{argv.*}", function(x) local idx = x:match("{argv(.*)}") - return vim.fn.shellescape(argv(idx)) + -- \\ -> \ characters from a regular lua strings being inserted into a literal lua strings cause problems + -- " -> """ vim.fn.shellescape wrongly adds an additional final " + return utils.__IS_WINDOWS and argv(idx):gsub([[\\]], [[\]]):gsub('"', '"""') + or vim.fn.shellescape(argv(idx)) end) end @@ -384,11 +395,15 @@ end local COLON_BYTE = string.byte(":") +---@param x string +---@param opts table +---@return string entry M.file = function(x, opts) opts = opts or {} local ret = {} local icon, hl local colon_idx = utils.find_next_char(x, COLON_BYTE) or 0 + if utils.__IS_WINDOWS then colon_idx = utils.find_next_char(x, COLON_BYTE, colon_idx) or 0 end local file_part = colon_idx > 1 and x:sub(1, colon_idx - 1) or x local rest_of_line = colon_idx > 1 and x:sub(colon_idx) or nil -- strip ansi coloring from path so we can use filters diff --git a/lua/fzf-lua/path.lua b/lua/fzf-lua/path.lua index 059e2fa7..222685e6 100644 --- a/lua/fzf-lua/path.lua +++ b/lua/fzf-lua/path.lua @@ -42,6 +42,8 @@ function M.tail(path) return path end +---@param path string +---@return string function M.extension(path) for i = #path, 1, -1 do if string_byte(path, i) == 46 then @@ -57,6 +59,8 @@ function M.to_matching_str(path) return utils.lua_regex_escape(path) end +---@param paths string[] +---@return string function M.join(paths) -- gsub to remove double separator local ret = table.concat(paths, M.SEPARATOR):gsub(M.SEPARATOR .. M.SEPARATOR, M.SEPARATOR) @@ -138,15 +142,19 @@ M.HOME = function() -- use 'os.getenv' instead of 'vim.env' due to (#452): -- E5560: nvim_exec must not be called in a lua loop callback -- M.__HOME = vim.env.HOME - M.__HOME = os.getenv("HOME") + M.__HOME = utils.__IS_WINDOWS and os.getenv("USERPROFILE") or os.getenv("HOME") end return M.__HOME end +---@param path string? +---@return string? function M.tilde_to_HOME(path) return path and path:gsub("^~", M.HOME()) or nil end +---@param path string? +---@return string? function M.HOME_to_tilde(path) return path and path:gsub("^" .. utils.lua_regex_escape(M.HOME()), "~") or nil end diff --git a/lua/fzf-lua/previewer/builtin.lua b/lua/fzf-lua/previewer/builtin.lua index 6c64623c..ce5f7d17 100644 --- a/lua/fzf-lua/previewer/builtin.lua +++ b/lua/fzf-lua/previewer/builtin.lua @@ -304,6 +304,7 @@ function Previewer.base:zero(_) -- https://github.com/junegunn/fzf/issues/3516 -- self._zero_lock = self._zero_lock or vim.fn.tempname() + if utils.__IS_WINDOWS then self._zero_lock = vim.fs.normalize(self._zero_lock) end local act = string.format("execute-silent(mkdir %s && %s)", libuv.shellescape(self._zero_lock), shell.raw_action(function(_, _, _) diff --git a/lua/fzf-lua/providers/grep.lua b/lua/fzf-lua/providers/grep.lua index abcab23f..8ea401fd 100644 --- a/lua/fzf-lua/providers/grep.lua +++ b/lua/fzf-lua/providers/grep.lua @@ -7,6 +7,10 @@ local make_entry = require "fzf-lua.make_entry" local M = {} +---@param opts table +---@param search_query string +---@param no_esc boolean +---@return string local get_grep_cmd = function(opts, search_query, no_esc) if opts.raw_cmd and #opts.raw_cmd > 0 then return opts.raw_cmd @@ -191,7 +195,7 @@ local function normalize_live_grep_opts(opts) opts.query = opts.search or "" if opts.search and #opts.search > 0 then -- escape unless the user requested not to - if not (opts.no_esc) then + if not opts.no_esc then opts.query = utils.rg_escape(opts.search) end end @@ -230,7 +234,6 @@ M.live_grep_st = function(opts) core.fzf_exec(nil, opts) end - -- multi threaded (multi-process actually) version M.live_grep_mt = function(opts) opts = normalize_live_grep_opts(opts) @@ -253,7 +256,7 @@ M.live_grep_mt = function(opts) -- FIELD INDEX EXPRESSION by 'fzf_exec' opts.cmd = get_grep_cmd(opts, core.fzf_query_placeholder, 2) local command = core.mt_cmd_wrapper(opts) - if command ~= opts.cmd then + if command ~= opts.cmd then --[[@cast command -function]] -- this means mt_cmd_wrapper wrapped the command. -- Since now the `rg` command is wrapped inside -- the shell escaped '--headless .. --cmd', we won't diff --git a/lua/fzf-lua/providers/module.lua b/lua/fzf-lua/providers/module.lua index 81609cdb..1443db7e 100644 --- a/lua/fzf-lua/providers/module.lua +++ b/lua/fzf-lua/providers/module.lua @@ -37,6 +37,8 @@ M.metatable = function(opts) core.fzf_exec(methods, opts) end +---@param dir string +---@param fn fun(fname: string, name: string, type: string) local function ls(dir, fn) local handle = vim.loop.fs_scandir(dir) while handle do diff --git a/lua/fzf-lua/shell.lua b/lua/fzf-lua/shell.lua index 8416fb44..697705b7 100644 --- a/lua/fzf-lua/shell.lua +++ b/lua/fzf-lua/shell.lua @@ -1,6 +1,7 @@ -- modified version of: -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf/actions.lua local uv = vim.loop +local utils = require "fzf-lua.utils" local path = require "fzf-lua.path" local libuv = require "fzf-lua.libuv" @@ -80,7 +81,10 @@ function M.raw_async_action(fn, fzf_field_expression, debug) -- 'nvim', it can be something else local nvim_bin = os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath local nvim_runtime = os.getenv("FZF_LUA_NVIM_BIN") and "" - or string.format("VIMRUNTIME=%s ", libuv.shellescape(vim.env.VIMRUNTIME)) + or string.format(utils.__IS_WINDOWS + and [[set "VIMRUNTIME=%s" & ]] or "VIMRUNTIME=%s ", + utils.__IS_WINDOWS and vim.fs.normalize(vim.env.VIMRUNTIME) or + libuv.shellescape(vim.env.VIMRUNTIME)) local call_args = ("fzf_lua_server=[[%s]], fnc_id=%d %s"):format( vim.g.fzf_lua_server, id, debug and ", debug=true" or "") diff --git a/lua/fzf-lua/shell_helper.lua b/lua/fzf-lua/shell_helper.lua index ac0e4ec4..f7759ade 100644 --- a/lua/fzf-lua/shell_helper.lua +++ b/lua/fzf-lua/shell_helper.lua @@ -2,8 +2,17 @@ -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/action_helper.lua local uv = vim.loop +local is_windows = vim.fn.has("win32") == 1 + +---@return string +local function windows_pipename() + local tmpname = vim.fn.tempname() + tmpname = string.gsub(tmpname, "\\", "") + return ([[\\.\pipe\%s]]):format(tmpname) +end + local function get_preview_socket() - local tmp = vim.fn.tempname() + local tmp = is_windows and windows_pipename() or vim.fn.tempname() local socket = uv.new_pipe(false) uv.pipe_bind(socket, tmp) return socket, tmp @@ -21,7 +30,7 @@ uv.listen(preview_socket, 100, function(_) uv.close(preview_receive_socket) uv.close(preview_socket) vim.schedule(function() - vim.cmd [[qall]] + vim.cmd([[qall]]) end) return end @@ -29,7 +38,6 @@ uv.listen(preview_socket, 100, function(_) end) end) - local function rpc_nvim_exec_lua(opts) local success, errmsg = pcall(function() -- fzf selection is unpacked as the argument list @@ -55,7 +63,7 @@ local function rpc_nvim_exec_lua(opts) preview_socket_path, fzf_selection, tonumber(preview_lines), - tonumber(preview_cols) + tonumber(preview_cols), }) vim.fn.chanclose(chan_id) end) @@ -74,10 +82,10 @@ local function rpc_nvim_exec_lua(opts) if not success then io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or "")) - vim.cmd [[qall]] + vim.cmd([[qall]]) end end return { - rpc_nvim_exec_lua = rpc_nvim_exec_lua + rpc_nvim_exec_lua = rpc_nvim_exec_lua, } diff --git a/lua/fzf-lua/utils.lua b/lua/fzf-lua/utils.lua index 852a500e..abada187 100644 --- a/lua/fzf-lua/utils.lua +++ b/lua/fzf-lua/utils.lua @@ -12,6 +12,7 @@ M.__HAS_NVIM_07 = vim.fn.has("nvim-0.7") == 1 M.__HAS_NVIM_08 = vim.fn.has("nvim-0.8") == 1 M.__HAS_NVIM_09 = vim.fn.has("nvim-0.9") == 1 M.__HAS_NVIM_010 = vim.fn.has("nvim-0.10") == 1 +M.__IS_WINDOWS = vim.fn.has("win32") == 1 -- limit devicons support to nvim >=0.8, although official support is >=0.7 @@ -83,6 +84,9 @@ M._if = function(bool, a, b) end end +---@param inputstr string +---@param sep string +---@return string[] M.strsplit = function(inputstr, sep) local t = {} for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do @@ -101,6 +105,9 @@ M.find_last_char = function(str, c) end end +---@param str string +---@param c integer +---@param start_idx integer M.find_next_char = function(str, c, start_idx) for i = start_idx or 1, #str do if string_byte(str, i) == c then @@ -147,13 +154,16 @@ function M.is_darwin() return vim.loop.os_uname().sysname == "Darwin" end +---@param str string +---@return string function M.rg_escape(str) if not str then return str end -- [(~'"\/$?'`*&&||;[]<>)] -- escape "\~$?*|[()^-." - return str:gsub("[\\~$?*|{\\[()^%-%.%+]", function(x) + local ret = str:gsub("[\\~$?*|{\\[()^%-%.%+]", function(x) return "\\" .. x end) + return ret end function M.sk_escape(str) @@ -282,7 +292,6 @@ M.read_file_async = function(filepath, callback) end) end - -- deepcopy can fail with: "Cannot deepcopy object of type userdata" (#353) -- this can happen when copying items/on_choice params of vim.ui.select -- run in a pcall and fallback to our poor man's clone @@ -345,6 +354,10 @@ end -- Set map value for string key -- e.g. `map_set(m, "key.sub1.sub2", value)` -- if need be, build map tree as we go along +---@param m table? +---@param k string +---@param v unknown +---@return table function M.map_set(m, k, v) m = m or {} local keys = M.strsplit(k, ".") @@ -361,6 +374,8 @@ function M.map_set(m, k, v) return m end +---@param m table? +---@return table? function M.map_tolower(m) if not m then return @@ -627,6 +642,9 @@ function M.setup_devicon_term_hls() pcall(loadstring("require'fzf-lua.make_entry'.setup_devicon_term_hls()")) end +---@param fname string +---@param name string +---@param silent boolean function M.load_profile(fname, name, silent) local profile = name or fname:match("([^%p]+)%.lua$") or "" local ok, res = pcall(dofile, fname) @@ -830,9 +848,7 @@ end -- Close a buffer without triggering an autocmd function M.nvim_buf_delete(bufnr, opts) - if not vim.api.nvim_buf_is_valid(bufnr) then - return - end + if not vim.api.nvim_buf_is_valid(bufnr) then return end local save_ei = vim.o.eventignore vim.o.eventignore = "all" vim.api.nvim_buf_delete(bufnr, opts) @@ -928,6 +944,7 @@ function M.neovim_bind_to_fzf(key) ["c"] = "ctrl", ["s"] = "shift", } + key = key:lower():gsub("[<>]", "") for k, v in pairs(conv_map) do key = key:gsub(k .. "%-", v .. "-") @@ -957,4 +974,11 @@ function M.find_version() return rc == 0 and tonumber(out[1]:match("(%d+.%d+)")) or nil end +---@return string +function M.windows_pipename() + local tmpname = vim.fn.tempname() + tmpname = string.gsub(tmpname, "\\", "") + return ([[\\.\pipe\%s]]):format(tmpname) +end + return M diff --git a/lua/fzf-lua/win.lua b/lua/fzf-lua/win.lua index 814f1858..5002a32d 100644 --- a/lua/fzf-lua/win.lua +++ b/lua/fzf-lua/win.lua @@ -300,6 +300,8 @@ function FzfWin:reset_win_highlights(win) vim.api.nvim_win_set_option(win, "winhighlight", hl) end +---@param exit_code integer +---@param fzf_bufnr integer function FzfWin:check_exit_status(exit_code, fzf_bufnr) -- see the comment in `FzfWin:close` for more info if fzf_bufnr and fzf_bufnr ~= self.fzf_bufnr then @@ -342,6 +344,8 @@ local function opt_matches(opts, key, str) return opt and opt:match(str) end +---@param o table +---@return FzfWin function FzfWin:new(o) if _self then -- utils.warn("Please close fzf-lua before starting a new instance")