diff --git a/.luarc.json b/.luarc.json index 902595e5..6bc8764e 100644 --- a/.luarc.json +++ b/.luarc.json @@ -3,35 +3,24 @@ "runtime.version": "LuaJIT", "diagnostics": { "enable": true, - "globals": [ - "vim", - "describe", - "pending", - "it", - "before_each", - "after_each" - ], + "globals": ["vim"], "neededFileStatus": { "codestyle-check": "Any" }, - "disable": [ - "need-check-nil", - "missing-parameter", - "cast-local-type" - ] + "disable": ["need-check-nil", "missing-parameter", "cast-local-type"] }, "workspace": { "library": [ - "$VIMRUNTIME/lua", "lua", - "${3rd}/luv/library" + "$VIMRUNTIME/lua", + "${3rd}/luv/library", + "$XDG_DATA_HOME/nvim/lazy/plenary.nvim/lua", + "$LOCALAPPDATA/nvim-data/lazy/plenary.nvim/lua" ], "checkThirdParty": false, "maxPreload": 2000, "preloadFileSize": 1000, - "ignoreDir": [ - "tests/" - ] + "ignoreDir": ["tests/"] }, "type": { "weakNilCheck": true, diff --git a/README.md b/README.md index c5b893f4..df04dfb8 100644 --- a/README.md +++ b/README.md @@ -877,7 +877,6 @@ require'fzf-lua'.setup { .. " %(subject) %(color:blue)%(taggername)%(color:reset)' refs/tags", preview = "git log --graph --color --pretty=format:'%C(yellow)%h%Creset " .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {1}", - fzf_opts = { ["--no-multi"] = "" }, actions = { ["default"] = actions.git_checkout }, }, stash = { @@ -888,10 +887,6 @@ require'fzf-lua'.setup { ["default"] = actions.git_stash_apply, ["ctrl-x"] = { fn = actions.git_stash_drop, reload = true }, }, - fzf_opts = { - ["--no-multi"] = '', - ['--delimiter'] = "'[:]'", - }, }, icons = { ["M"] = { icon = "M", color = "yellow" }, diff --git a/doc/fzf-lua.txt b/doc/fzf-lua.txt index 3d47c44c..0a8e2334 100644 --- a/doc/fzf-lua.txt +++ b/doc/fzf-lua.txt @@ -970,7 +970,6 @@ open an issue and I'll be more than happy to help.** .. " %(subject) %(color:blue)%(taggername)%(color:reset)' refs/tags", preview = "git log --graph --color --pretty=format:'%C(yellow)%h%Creset " .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {1}", - fzf_opts = { ["--no-multi"] = "" }, actions = { ["default"] = actions.git_checkout }, }, stash = { @@ -981,10 +980,6 @@ open an issue and I'll be more than happy to help.** ["default"] = actions.git_stash_apply, ["ctrl-x"] = { fn = actions.git_stash_drop, reload = true }, }, - fzf_opts = { - ["--no-multi"] = '', - ['--delimiter'] = "'[:]'", - }, }, icons = { ["M"] = { icon = "M", color = "yellow" }, @@ -1438,4 +1433,4 @@ I missed your name feel free to contact me and I'll add it below: as baseline for the builtin previewer and his must have plugin nvim-bqf -vim:tw=78:ts=8:ft=help:norl: \ No newline at end of file +vim:tw=78:ts=8:ft=help:norl: diff --git a/lua/fzf-lua/actions.lua b/lua/fzf-lua/actions.lua index e7ad3523..dfc5e1eb 100644 --- a/lua/fzf-lua/actions.lua +++ b/lua/fzf-lua/actions.lua @@ -16,7 +16,7 @@ M.expect = function(actions) end end if #keys > 0 then - return string.format("--expect=%s", table.concat(keys, ",")) + return table.concat(keys, ",") end return nil end @@ -104,7 +104,7 @@ M.vimcmd_file = function(vimcmd, selected, opts) if entry.path == "" then goto continue end entry.ctag = opts._ctag and path.entry_to_ctag(selected[i]) local fullpath = entry.path or entry.uri and entry.uri:match("^%a+://(.*)") - if not path.starts_with_separator(fullpath) then + if not path.is_absolute(fullpath) then fullpath = path.join({ opts.cwd or opts._cwd or vim.loop.cwd(), fullpath }) end if vimcmd == "e" @@ -128,7 +128,7 @@ M.vimcmd_file = function(vimcmd, selected, opts) if vimcmd ~= "e" or curbuf ~= fullpath then if entry.path then -- do not run ': ' for uri entries (#341) - local relpath = path.relative(entry.path, vim.loop.cwd()) + local relpath = path.relative_to(entry.path, vim.loop.cwd()) if vim.o.autochdir then -- force full paths when `autochdir=true` (#882) relpath = fullpath @@ -245,7 +245,7 @@ M.file_switch = function(selected, opts) local bufnr = nil local entry = path.entry_to_file(selected[1]) local fullpath = entry.path - if not path.starts_with_separator(fullpath) then + if not path.is_absolute(fullpath) then fullpath = path.join({ opts.cwd or vim.loop.cwd(), fullpath }) end for _, b in ipairs(vim.api.nvim_list_bufs()) do @@ -629,7 +629,7 @@ end local git_exec = function(selected, opts, cmd, silent) local success for _, e in ipairs(selected) do - local file = path.relative(path.entry_to_file(e, opts).path, opts.cwd) + local file = path.relative_to(path.entry_to_file(e, opts).path, opts.cwd) local _cmd = vim.deepcopy(cmd) table.insert(_cmd, file) local output, rc = utils.io_systemlist(_cmd) @@ -707,7 +707,7 @@ M.git_buf_edit = function(selected, opts) local git_root = path.git_root(opts, true) local win = vim.api.nvim_get_current_win() local buffer_filetype = vim.bo.filetype - local file = path.relative(vim.fn.expand("%:p"), git_root) + local file = path.relative_to(vim.fn.expand("%:p"), git_root) local commit_hash = match_commit_hash(selected[1], opts) table.insert(cmd, commit_hash .. ":" .. file) local git_file_contents = utils.io_systemlist(cmd) @@ -816,8 +816,9 @@ end ---@param selected string[] ---@param opts table M.apply_profile = function(selected, opts) - local fname = utils.__IS_WINDOWS and selected[1]:match("%u?:?[^:]+") or selected[1]:match("[^:]+") - local profile = selected[1]:match(":([^%s]+)") + local entry = path.entry_to_file(selected[1]) + local fname = entry.path + local profile = entry.stripped:sub(#fname + 2):match("[^%s]+") local ok = utils.load_profile(fname, profile, opts.silent) if ok then vim.cmd(string.format([[lua require("fzf-lua").setup({"%s"})]], profile)) diff --git a/lua/fzf-lua/complete.lua b/lua/fzf-lua/complete.lua index f63ec21e..b73a09c9 100644 --- a/lua/fzf-lua/complete.lua +++ b/lua/fzf-lua/complete.lua @@ -23,14 +23,14 @@ local function find_toplevel_cwd(maybe_cwd, postfix, orig_cwd) if vim.fn.isdirectory(vim.fn.expand(maybe_cwd)) == 1 then local disp_cwd, cwd = maybe_cwd, vim.fn.expand(maybe_cwd) -- returned cwd must be full path - if cwd:sub(1, 1) == "." and cwd:sub(2, 2) == path.SEPARATOR then + if path.has_cwd_prefix(cwd) then cwd = vim.loop.cwd() .. (#cwd > 1 and cwd:sub(2) or "") -- inject "./" only if original path started with it -- otherwise ignore the "." retval from fnamemodify if #orig_cwd > 0 and orig_cwd:sub(1, 1) ~= "." then disp_cwd = nil end - elseif not path.starts_with_separator(cwd) then + elseif not path.is_absolute(cwd) then cwd = path.join({ vim.loop.cwd(), cwd }) end return disp_cwd, cwd, postfix @@ -60,15 +60,13 @@ local set_cmp_opts_path = function(opts) if not opts.prompt then opts.prompt = "." end - if not path.ends_with_separator(opts.prompt) then - opts.prompt = opts.prompt .. path.SEPARATOR - end + opts.prompt = path.add_trailing(opts.prompt) -- completion function rebuilds the line with the full path opts.complete = function(selected, o, l, _) -- query fuzzy matching is empty if #selected == 0 then return end local replace_at = col - #before - local relpath = path.relative(path.entry_to_file(selected[1], o).path, opts.cwd) + local relpath = path.relative_to(path.entry_to_file(selected[1], o).path, opts.cwd) local before_path = replace_at > 1 and l:sub(1, replace_at - 1) or "" local rest_of_line = #l >= (col + #after) and l:sub(col + #after) or "" local resolved_path = opts._cwd and path.join({ opts._cwd, relpath }) or relpath diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua index b4fea3ff..694df808 100644 --- a/lua/fzf-lua/config.lua +++ b/lua/fzf-lua/config.lua @@ -9,8 +9,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 + and path.normalize(debug.getinfo(M._devicons.setup, "S").source:gsub("^@", "")) end M._diricon_escseq = function() @@ -428,7 +427,7 @@ function M.normalize_opts(opts, globals, __resume_key) -- relative paths in cwd are inaccessible when using multiprocess -- as the external process have no awareness of our current working -- directory so we must convert to full path (#375) - if not path.starts_with_separator(opts.cwd) then + if not path.is_absolute(opts.cwd) then opts.cwd = path.join({ vim.loop.cwd(), opts.cwd }) end end diff --git a/lua/fzf-lua/core.lua b/lua/fzf-lua/core.lua index df50d2a3..d81ebbed 100644 --- a/lua/fzf-lua/core.lua +++ b/lua/fzf-lua/core.lua @@ -7,6 +7,8 @@ local win = require "fzf-lua.win" local libuv = require "fzf-lua.libuv" local shell = require "fzf-lua.shell" local make_entry = require "fzf-lua.make_entry" +local base64 = require "fzf-lua.lib.base64" +local serpent = require "fzf-lua.lib.serpent" local M = {} @@ -76,7 +78,7 @@ local contents_from_arr = function(cont_arr) return contents end ----@alias content table|function|string +---@alias content table|function|string|nil -- Main API, see: -- https://github.com/ibhagwan/fzf-lua/wiki/Advanced @@ -447,11 +449,11 @@ M.create_fzf_binds = function(binds) for key, action in pairs(dedup) do table.insert(tbl, string.format("%s:%s", key, action)) end - return vim.fn.shellescape(table.concat(tbl, ",")) + return libuv.shellescape(table.concat(tbl, ",")) end ---@param opts table ----@return string +---@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 @@ -469,17 +471,16 @@ M.build_fzf_cli = function(opts) for _, o in ipairs({ "query", "preview" }) do local flag = string.format("--%s", o) if opts[o] ~= nil then - -- opt can be 'false' (disabled) - -- don't shellescape in this case - opts.fzf_opts[flag] = opts[o] and libuv.shellescape(opts[o]) - else - opts.fzf_opts[flag] = opts.fzf_opts[flag] + -- opt can be 'false' (disabled), don't shellescape in this case + if type(opts[o]) == "string" then + opts.fzf_opts[flag] = libuv.shellescape(opts[o]) + else + opts.fzf_opts[flag] = opts[o] + end end end opts.fzf_opts["--bind"] = M.create_fzf_binds(opts.keymap.fzf) - if opts.fzf_colors then - opts.fzf_opts["--color"] = M.create_fzf_colors(opts) - end + opts.fzf_opts["--color"] = M.create_fzf_colors(opts) opts.fzf_opts["--expect"] = actions.expect(opts.actions) if opts.fzf_opts["--preview-window"] == nil then opts.fzf_opts["--preview-window"] = M.preview_window(opts) @@ -490,39 +491,10 @@ M.build_fzf_cli = function(opts) end -- shell escape the prompt opts.fzf_opts["--prompt"] = (opts.prompt or opts.fzf_opts["--prompt"]) and - vim.fn.shellescape(opts.prompt or opts.fzf_opts["--prompt"]) - -- multi | no-multi (select) - if opts.nomulti or opts.fzf_opts["--no-multi"] then - opts.fzf_opts["--multi"] = nil - opts.fzf_opts["--no-multi"] = "" - else - opts.fzf_opts["--multi"] = "" - opts.fzf_opts["--no-multi"] = nil - end - -- backward compatibility, add all previously known options - for k, v in pairs({ - ["--ansi"] = "fzf_ansi", - ["--layout"] = "fzf_layout" - }) do - if opts[v] and #opts[v] == 0 then - opts.fzf_opts[k] = nil - elseif opts[v] then - opts.fzf_opts[k] = opts[v] - end - end - local extra_args = "" - for _, o in ipairs({ - "fzf_args", - "fzf_raw_args", - "fzf_cli_args", - "_fzf_cli_args", - }) do - if opts[o] then extra_args = extra_args .. " " .. opts[o] end - end + libuv.shellescape(opts.prompt or opts.fzf_opts["--prompt"]) if opts._is_skim then + -- skim (rust version of fzf) doesn't support the '--info=' flag local info = opts.fzf_opts["--info"] - -- skim (rust version of fzf) doesn't - -- support the '--info=' flag opts.fzf_opts["--info"] = nil if info == "inline" then -- inline for skim is defined as: @@ -536,50 +508,47 @@ M.build_fzf_cli = function(opts) opts.fzf_opts["--border"] = "" end end - -- build the clip args - local cli_args = "" + -- build the cli args + local cli_args = {} -- fzf-tmux args must be included first if opts._is_fzf_tmux then for k, v in pairs(opts.fzf_tmux_opts or {}) do - if v then cli_args = cli_args .. string.format(" %s %s", k, v) end + table.insert(cli_args, k) + if type(v) == "string" and #v > 0 then + table.insert(cli_args, v) + end end end for k, v in pairs(opts.fzf_opts) do - if type(v) == "table" then - -- table argument is meaningless here - v = nil - 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) .. '"' + if type(v) == "string" or type(v) == "number" then + -- convert number type to string + v = tostring(v) + if utils.__IS_WINDOWS and v:match([[^'.*'$]]) then + -- replace single quote shellescape + -- TODO: replace all so we never get here + v = [["]] .. v:sub(2, #v - 1) .. [["]] + end + table.insert(cli_args, k) + if #v > 0 then + table.insert(cli_args, v) + end end - if v then - v = v:gsub(k .. "=", "") - cli_args = cli_args .. - (" %s%s"):format(k, #v > 0 and "=" .. v or "") + end + for _, o in ipairs({ "fzf_args", "fzf_raw_args", "fzf_cli_args", "_fzf_cli_args" }) do + if opts[o] then + table.insert(cli_args, type(opts[o]) == "table" and opts[o] or tostring(opts[o])) end end - return cli_args .. extra_args + return cli_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) + ---@return table + local filter_opts = function(o) local names = { "debug", "argv_expr", @@ -597,28 +566,44 @@ M.mt_cmd_wrapper = function(opts) "strip_cwd_prefix", "file_ignore_patterns", "rg_glob", - "__module__", + "_base64", } -- caller reqested rg with glob support if o.rg_glob then table.insert(names, "glob_flag") table.insert(names, "glob_separator") end - local str = "" + local t = {} for _, name in ipairs(names) do if o[name] ~= nil then - if #str > 0 then str = str .. "," end - local val = o[name] - if type(val) == "string" then - val = str_to_str(val) - end - if type(val) == "table" then - val = vim.inspect(val) - end - str = str .. ("%s=%s"):format(name, val) + t[name] = o[name] end end - return "{" .. str .. "}" + t.g = {} + for k, v in pairs({ + ["_fzf_lua_server"] = vim.g.fzf_lua_server, + ["_devicons_path"] = config._devicons_path, + ["_devicons_setup"] = config._devicons_setup, + }) do + t.g[k] = v + end + return t + end + + ---@param obj table|string + ---@return string + local serialize = function(obj) + local str = type(obj) == "table" + and serpent.line(obj, { comment = false, sortkeys = false }) + or tostring(obj) + if opts._base64 ~= false then + -- by default, base64 encode all arguments + return "[==[" .. base64.encode(str) .. "]==]" + else + -- if not encoding, don't string wrap the table + return type(obj) == "table" and str + or "[==[" .. str .. "]==]" + end end if not opts.requires_processing @@ -631,32 +616,25 @@ M.mt_cmd_wrapper = function(opts) elseif opts.multiprocess then assert(not opts.__mt_transform or type(opts.__mt_transform) == "string") assert(not opts.__mt_preprocess or type(opts.__mt_preprocess) == "string") - local fn_preprocess = opts.__mt_preprocess or [[return require("make_entry").preprocess]] - local fn_transform = opts.__mt_transform or [[return require("make_entry").file]] - -- replace all below 'fn.shellescape' with our version - -- replacing the surrounding single quotes with double - -- as this was causing resume to fail with fish shell - -- due to fzf replacing ' with \ (no idea why) - if not opts.no_remote_config then - fn_transform = ([[_G._fzf_lua_server=%s; %s]]):format( - -- 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 - fn_transform = ([[_G._devicons_setup=%s; %s]]):format( - libuv.shellescape(config._devicons_setup), - fn_transform) - end - if config._devicons_path then - fn_transform = ([[_G._devicons_path=%s; %s]]):format( - libuv.shellescape(config._devicons_path), - fn_transform) + if opts.argv_expr then + -- Since the `rg` command will be wrapped inside the shell escaped + -- '--headless .. --cmd', we won't be able to search single quotes + -- as it will break the escape sequence. So we use a nifty trick: + -- * replace the placeholder with {argv1} + -- * re-add the placeholder at the end of the command + -- * preprocess then replace it with vim.fn.argv(1) + -- NOTE: since we cannot guarantee the positional index + -- of arguments (#291), we use the last argument instead + opts.cmd = opts.cmd:gsub(M.fzf_query_placeholder, "{argvz}") end - local cmd = libuv.wrap_spawn_stdio(opts_to_str(opts), fn_transform, fn_preprocess) - if opts.debug_cmd or opts.debug and not (opts.debug_cmd == false) then - utils.info(string.format("multiprocess cmd: %s", cmd)) + local cmd = libuv.wrap_spawn_stdio( + serialize(filter_opts(opts)), + serialize(opts.__mt_transform or [[return require("make_entry").file]]), + serialize(opts.__mt_preprocess or [[return require("make_entry").preprocess]]) + ) + if opts.argv_expr then + -- prefix the query with `--` so we can support `--fixed-strings` (#781) + cmd = string.format("%s -- %s", cmd, M.fzf_query_placeholder) end return cmd else @@ -698,15 +676,10 @@ end M.set_header = function(opts, hdr_tbl) local function normalize_cwd(cwd) - 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 + if path.is_absolute(cwd) and not path.equals(cwd, vim.loop.cwd()) then -- since we're always converting cwd to full path -- try to convert it back to relative for display - cwd = path.relative(cwd, _cwd) + cwd = path.relative_to(cwd, vim.loop.cwd()) end -- make our home dir path look pretty return path.HOME_to_tilde(cwd) @@ -719,9 +692,7 @@ M.set_header = function(opts, hdr_tbl) #opts.prompt >= tonumber(opts.cwd_prompt_shorten_len) then opts.prompt = path.shorten(opts.prompt, tonumber(opts.cwd_prompt_shorten_val) or 1) end - if not path.ends_with_separator(opts.prompt) then - opts.prompt = opts.prompt .. path.SEPARATOR - end + opts.prompt = path.add_trailing(opts.prompt) end if opts.no_header or opts.headers == false then return opts @@ -739,7 +710,8 @@ M.set_header = function(opts, hdr_tbl) -- cwd unless the caller specifically requested if opts.cwd_header == false or opts.cwd_prompt and opts.cwd_header == nil or - opts.cwd_header == nil and (not opts.cwd or opts.cwd == vim.loop.cwd()) then + opts.cwd_header == nil and + (not opts.cwd or path.equals(opts.cwd, vim.loop.cwd())) then return end return normalize_cwd(opts.cwd or vim.loop.cwd()) @@ -968,9 +940,25 @@ M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts) if type(opts.query_delay) == "number" then reload_command = string.format("sleep %.2f; %s", opts.query_delay / 1000, reload_command) end - if not opts.exec_empty_query then - reload_command = ("[ -z %s ] || %s"):format(fzf_field_expression, reload_command) - end + local no_query_condi = opts.exec_empty_query and "" or string.format( + utils._if_win( + -- due to the reload command already being shellescaped and fzf's {q} + -- also escaping the query with ^""^ any spaces in the query + -- will fail the command, by adding caret escaping before fzf's + -- we fool CMD.exe to not terminate the quote and thus an empty query + -- will generate the experssion ^^"^" which translates to ^"" + -- our specialized libuv.shellescape will also double the escape + -- sequence if a "!" is found in our string as explained in: + -- https://ss64.com/nt/syntax-esc.html + -- TODO: open an upstream bug rgd ! as without the double escape + -- if an ! is found in the command (i.e. -g "rg ... -g !.git") + -- sending a caret will require doubling (i.e. sending ^^ for ^) + [[IF ^%s NEQ ^^"^" ]], + "[ -z %s ] || "), + -- {q} for fzf is automatically shell escaped + fzf_field_expression + ) + if opts._is_skim then -- skim interactive mode does not need a piped command opts.__fzf_init_cmd = nil @@ -995,25 +983,24 @@ M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts) opts.query = nil -- setup as interactive opts._fzf_cli_args = string.format("--interactive --cmd %s", - libuv.shellescape(reload_command)) + libuv.shellescape(no_query_condi .. reload_command)) else -- **send an empty table to avoid running $FZF_DEFAULT_COMMAND -- The above seems to create a hang in some systems -- use `true` as $FZF_DEFAULT_COMMAND instead (#510) - opts.__fzf_init_cmd = "true" + opts.__fzf_init_cmd = utils._if_win("break", "true") if opts.exec_empty_query or (opts.query and #opts.query > 0) then opts.__fzf_init_cmd = initial_command:gsub(fzf_field_expression, - libuv.shellescape(opts.query:gsub("%%", "%%%%"))) + libuv.shellescape(opts.query)) end opts.fzf_opts["--disabled"] = "" opts.fzf_opts["--query"] = libuv.shellescape(opts.query) -- OR with true to avoid fzf's "Command failed:" message if opts.silent_fail ~= false then - reload_command = ("%s || true"):format(reload_command) + reload_command = reload_command .. " || " .. utils._if_win("break", "true") end - opts._fzf_cli_args = string.format("--bind=%s", - libuv.shellescape(("change:reload:%s"):format( - ("%s"):format(reload_command)))) + opts._fzf_cli_args = string.format("--bind=%s", libuv.shellescape( + string.format("change:reload:%s%s", no_query_condi, reload_command))) end return opts diff --git a/lua/fzf-lua/defaults.lua b/lua/fzf-lua/defaults.lua index 2fb3fadb..45559448 100644 --- a/lua/fzf-lua/defaults.lua +++ b/lua/fzf-lua/defaults.lua @@ -17,7 +17,10 @@ function M._default_previewer_fn() end function M._preview_pager_fn() - return vim.fn.executable("delta") == 1 and "delta --width=$FZF_PREVIEW_COLUMNS" or nil + if vim.fn.executable("delta") ~= 1 then + return nil + end + return "delta --width=" .. utils._if_win("%COLUMNS%", "$COLUMNS") end M.defaults = { @@ -170,6 +173,10 @@ M.defaults = { cmd_deleted = "git diff --color HEAD --", cmd_modified = "git diff --color HEAD", cmd_untracked = "git diff --color --no-index /dev/null", + -- TODO: modify previewer code to accept table cmd + -- cmd_deleted = { "git", "diff", "--color", "HEAD", "--" }, + -- cmd_modified = { "git", "diff", "--color", "HEAD" }, + -- cmd_untracked = { "git", "diff", "--color", "--no-index", "/dev/null" }, _fn_git_icons = function() return M.globals.git.icons end, _ctor = previewers.fzf.git_diff, }, @@ -220,11 +227,11 @@ M.defaults.files = { cwd_prompt = true, cwd_prompt_shorten_len = 32, cwd_prompt_shorten_val = 1, - fzf_opts = { ["--info"] = "default", }, + fzf_opts = { ["--info"] = "default", ["--multi"] = "" }, git_status_cmd = { "git", "-c", "color.status=false", "--no-optional-locks", "status", "--porcelain=v1" }, find_opts = [[-type f -not -path '*/\.git/*' -printf '%P\n']], - rg_opts = "--color=never --files --hidden --follow -g '!.git'", + rg_opts = [[--color=never --files --hidden --follow -g "!.git"]], fd_opts = "--color=never --type f --hidden --follow --exclude .git", toggle_ignore_flag = "--no-ignore", _actions = function() return M.globals.actions.files end, @@ -243,6 +250,7 @@ M.defaults.git = { file_icons = true and M._has_devicons, color_icons = true, git_icons = true, + fzf_opts = { ["--multi"] = "" }, _actions = function() return M.globals.actions.files end, winopts = { preview = { winopts = { cursorline = false } } }, }, @@ -256,6 +264,7 @@ M.defaults.git = { file_icons = true and M._has_devicons, color_icons = true, git_icons = true, + fzf_opts = { ["--multi"] = "" }, _actions = function() return M.globals.actions.files end, actions = { ["right"] = { fn = actions.git_unstage, reload = true }, @@ -267,10 +276,10 @@ M.defaults.git = { }, }, commits = { - prompt = "Commits> ", - cmd = "git log --color --pretty=format:'%C(yellow)%h%Creset " - .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset'", - preview = "git show --color {1}", + prompt = "Commits> ", + cmd = [[git log --color --pretty=format:"%C(yellow)%h%Creset ]] + .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset"]], + preview = "git show --color {1}", preview_pager = M._preview_pager_fn, actions = { ["default"] = actions.git_checkout, @@ -279,10 +288,10 @@ M.defaults.git = { fzf_opts = { ["--no-multi"] = "" }, }, bcommits = { - prompt = "BCommits> ", - cmd = "git log --color --pretty=format:'%C(yellow)%h%Creset " - .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {file}", - preview = "git show --color {1} -- {file}", + prompt = "BCommits> ", + cmd = [[git log --color --pretty=format:"%C(yellow)%h%Creset ]] + .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset" {file}]], + preview = "git show --color {1} -- {file}", preview_pager = M._preview_pager_fn, actions = { ["default"] = actions.git_buf_edit, @@ -304,12 +313,12 @@ M.defaults.git = { }, tags = { prompt = "Tags> ", - cmd = "git for-each-ref --color --sort='-taggerdate' --format " - .. "'%(color:yellow)%(refname:short)%(color:reset) " - .. "%(color:green)(%(taggerdate:relative))%(color:reset)" - .. " %(subject) %(color:blue)%(taggername)%(color:reset)' refs/tags", - preview = "git log --graph --color --pretty=format:'%C(yellow)%h%Creset " - .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {1}", + cmd = [[git for-each-ref --color --sort="-taggerdate" --format ]] + .. [["%(color:yellow)%(refname:short)%(color:reset) ]] + .. [[%(color:green)(%(taggerdate:relative))%(color:reset)]] + .. [[ %(subject) %(color:blue)%(taggername)%(color:reset)" refs/tags]], + preview = [[git log --graph --color --pretty=format:"%C(yellow)%h%Creset ]] + .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset" {1}]], fzf_opts = { ["--no-multi"] = "" }, actions = { ["default"] = actions.git_checkout }, }, @@ -349,7 +358,7 @@ M.defaults.grep = { file_icons = true and M._has_devicons, color_icons = true, git_icons = true, - fzf_opts = { ["--info"] = "default", }, + fzf_opts = { ["--info"] = "default", ["--multi"] = "" }, grep_opts = utils.is_darwin() and "--binary-files=without-match --line-number --recursive --color=always " .. "--extended-regexp -e" @@ -371,6 +380,7 @@ M.defaults.args = { file_icons = true and M._has_devicons, color_icons = true, git_icons = true, + fzf_opts = { ["--multi"] = "" }, _actions = function() return M.globals.actions.files end, actions = { ["ctrl-x"] = { fn = actions.arg_del, reload = true } }, } @@ -382,7 +392,7 @@ M.defaults.oldfiles = { color_icons = true, git_icons = false, stat_file = true, - fzf_opts = { ["--tiebreak"] = "index", }, + fzf_opts = { ["--tiebreak"] = "index", ["--multi"] = "" }, _actions = function() return M.globals.actions.files end, } @@ -393,6 +403,7 @@ M.defaults.quickfix = { file_icons = true and M._has_devicons, color_icons = true, git_icons = false, + fzf_opts = { ["--multi"] = "" }, _actions = function() return M.globals.actions.files end, } @@ -400,6 +411,7 @@ M.defaults.quickfix_stack = { prompt = "Quickfix Stack> ", marker = ">", previewer = { _ctor = previewers.builtin.quickfix, }, + fzf_opts = { ["--no-multi"] = "" }, actions = { ["default"] = actions.set_qflist, }, } @@ -410,6 +422,7 @@ M.defaults.loclist = { file_icons = true and M._has_devicons, color_icons = true, git_icons = false, + fzf_opts = { ["--multi"] = "" }, _actions = function() return M.globals.actions.files end, } @@ -417,6 +430,7 @@ M.defaults.loclist_stack = { prompt = "Locations Stack> ", marker = ">", previewer = { _ctor = previewers.builtin.quickfix, }, + fzf_opts = { ["--no-multi"] = "" }, actions = { ["default"] = actions.set_qflist, }, } @@ -431,7 +445,7 @@ M.defaults.buffers = { no_action_set_cursor = true, cwd_only = false, cwd = nil, - fzf_opts = { ["--tiebreak"] = "index", }, + fzf_opts = { ["--tiebreak"] = "index", ["--multi"] = "" }, _actions = function() return M.globals.actions.buffers end, actions = { ["ctrl-x"] = { fn = actions.buf_del, reload = true } }, _cached_hls = { "buf_nr", "buf_flag_cur", "buf_flag_alt" }, @@ -450,6 +464,7 @@ M.defaults.tabs = { ["ctrl-x"] = { fn = actions.buf_del, reload = true }, }, fzf_opts = { + ["--multi"] = "", ["--delimiter"] = "'[\\):]'", ["--with-nth"] = "3..", }, @@ -465,6 +480,7 @@ M.defaults.lines = { show_unlisted = false, no_term_buffers = true, fzf_opts = { + ["--no-multi"] = "", ["--delimiter"] = "'[\\]:]'", ["--nth"] = "2..", ["--tiebreak"] = "index", @@ -488,6 +504,7 @@ M.defaults.blines = { show_unlisted = true, no_term_buffers = false, fzf_opts = { + ["--no-multi"] = "", ["--delimiter"] = "'[:]'", ["--with-nth"] = "2..", ["--tiebreak"] = "index", @@ -515,6 +532,7 @@ M.defaults.tags = { git_icons = false, color_icons = true, fzf_opts = { + ["--no-multi"] = "", ["--delimiter"] = string.format("'[:%s]'", utils.nbsp), ["--tiebreak"] = "begin", ["--info"] = "default", @@ -534,6 +552,7 @@ M.defaults.btags = { git_icons = false, color_icons = true, fzf_opts = { + ["--no-multi"] = "", ["--delimiter"] = string.format("'[:%s]'", utils.nbsp), ["--with-nth"] = "1,-1", ["--tiebreak"] = "begin", @@ -546,17 +565,14 @@ M.defaults.btags = { M.defaults.colorschemes = { prompt = "Colorschemes> ", live_preview = true, - actions = { - ["default"] = actions.colorscheme, - }, - winopts = { - height = 0.55, - width = 0.50, - }, + winopts = { height = 0.55, width = 0.50 }, + fzf_opts = { ["--no-multi"] = "" }, + actions = { ["default"] = actions.colorscheme }, } M.defaults.highlights = { prompt = "Highlights> ", + fzf_opts = { ["--no-multi"] = "" }, previewer = { _ctor = previewers.builtin.highlights, }, } @@ -569,6 +585,7 @@ M.defaults.helptags = { ["ctrl-t"] = actions.help_tab, }, fzf_opts = { + ["--no-multi"] = "", ["--delimiter"] = "'[ ]'", ["--with-nth"] = "..-2", }, @@ -586,7 +603,7 @@ M.defaults.manpages = { ["ctrl-v"] = actions.man_vert, ["ctrl-t"] = actions.man_tab, }, - fzf_opts = { ["--tiebreak"] = "begin" }, + fzf_opts = { ["--tiebreak"] = "begin", ["--no-multi"] = "" }, previewer = "man", } @@ -598,6 +615,7 @@ M.defaults.lsp = { git_icons = false, cwd_only = false, async_or_timeout = 5000, + fzf_opts = { ["--multi"] = "" }, _actions = function() return M.globals.actions.files end, } @@ -646,6 +664,7 @@ M.defaults.lsp.symbols = { ["--delimiter"] = string.format("'[:%s]'", utils.nbsp), ["--tiebreak"] = "begin", ["--info"] = "default", + ["--no-multi"] = "", }, line_field_index = "{-2}", -- line field index field_index_expr = "{}", -- entry field index @@ -707,6 +726,7 @@ M.defaults.lsp.code_actions = { async_or_timeout = 5000, previewer = "codeaction", -- previewer = "codeaction_native", + fzf_opts = { ["--no-multi"] = "" }, } M.defaults.diagnostics = { @@ -718,6 +738,7 @@ M.defaults.diagnostics = { diag_icons = true, diag_source = false, multiline = true, + fzf_opts = { ["--multi"] = "" }, _actions = function() return M.globals.actions.files end, -- signs = { -- ["Error"] = { text = "e", texthl = "DiagnosticError" }, @@ -733,9 +754,8 @@ M.defaults.builtin = { height = 0.65, width = 0.50, }, - actions = { - ["default"] = actions.run_builtin, - }, + fzf_opts = { ["--no-multi"] = "" }, + actions = { ["default"] = actions.run_builtin }, } M.defaults.profiles = { @@ -743,21 +763,17 @@ M.defaults.profiles = { prompt = "FzfLua profiles> ", fzf_opts = { ["--delimiter"] = "'[:]'", - ["--with-nth"] = "2..", - }, - actions = { - ["default"] = actions.apply_profile, + ["--with-nth"] = "-1..", + ["--no-multi"] = "", }, + actions = { ["default"] = actions.apply_profile }, } M.defaults.marks = { prompt = "Marks> ", - actions = { - ["default"] = actions.goto_mark, - }, - previewer = { - _ctor = previewers.builtin.marks, - }, + fzf_opts = { ["--no-multi"] = "" }, + actions = { ["default"] = actions.goto_mark }, + previewer = { _ctor = previewers.builtin.marks }, } M.defaults.changes = { @@ -769,12 +785,9 @@ M.defaults.changes = { M.defaults.jumps = { prompt = "Jumps> ", cmd = "jumps", - actions = { - ["default"] = actions.goto_jump, - }, - previewer = { - _ctor = previewers.builtin.jumps, - }, + fzf_opts = { ["--no-multi"] = "" }, + actions = { ["default"] = actions.goto_jump }, + previewer = { _ctor = previewers.builtin.jumps }, } M.defaults.tagstack = { @@ -782,6 +795,7 @@ M.defaults.tagstack = { file_icons = true and M._has_devicons, color_icons = true, git_icons = true, + fzf_opts = { ["--multi"] = "" }, previewer = M._default_previewer_fn, _actions = function() return M.globals.actions.files end, } @@ -800,12 +814,13 @@ M.defaults.autocmds = { fzf_opts = { ["--delimiter"] = "'[:]'", ["--with-nth"] = "3..", + ["--no-multi"] = "", }, } M.defaults.command_history = { prompt = "Command History> ", - fzf_opts = { ["--tiebreak"] = "index", }, + fzf_opts = { ["--tiebreak"] = "index", ["--no-multi"] = "" }, actions = { ["default"] = actions.ex_run_cr, ["ctrl-e"] = actions.ex_run, @@ -814,7 +829,7 @@ M.defaults.command_history = { M.defaults.search_history = { prompt = "Search History> ", - fzf_opts = { ["--tiebreak"] = "index", }, + fzf_opts = { ["--tiebreak"] = "index", ["--no-multi"] = "" }, actions = { ["default"] = actions.search_cr, ["ctrl-e"] = actions.search, @@ -824,16 +839,15 @@ M.defaults.search_history = { M.defaults.registers = { prompt = "Registers> ", ignore_empty = true, - actions = { - ["default"] = actions.paste_register, - }, + actions = { ["default"] = actions.paste_register }, + fzf_opts = { ["--no-multi"] = "" }, } M.defaults.keymaps = { prompt = "Keymaps> ", previewer = { _ctor = previewers.builtin.keymaps }, winopts = { preview = { layout = "vertical" } }, - fzf_opts = { ["--tiebreak"] = "index", }, + fzf_opts = { ["--tiebreak"] = "index", ["--no-multi"] = "" }, ignore_patterns = { "^", "^" }, actions = { ["default"] = actions.keymap_apply, @@ -876,24 +890,27 @@ M.defaults.tmux = { prompt = "Tmux Buffers> ", cmd = "tmux list-buffers", register = [["]], - actions = { - ["default"] = actions.tmux_buf_set_reg, - }, + actions = { ["default"] = actions.tmux_buf_set_reg }, + fzf_opts = { ["--no-multi"] = "" }, }, } M.defaults.dap = { commands = { prompt = "DAP Commands> ", + fzf_opts = { ["--no-multi"] = "" }, }, configurations = { prompt = "DAP Configurations> ", + fzf_opts = { ["--no-multi"] = "" }, }, variables = { prompt = "DAP Variables> ", + fzf_opts = { ["--no-multi"] = "" }, }, frames = { prompt = "DAP Frames> ", + fzf_opts = { ["--no-multi"] = "" }, }, breakpoints = { prompt = "DAP Breakpoints> ", @@ -905,6 +922,7 @@ M.defaults.dap = { fzf_opts = { ["--delimiter"] = "'[\\]:]'", ["--with-nth"] = "2..", + ["--no-multi"] = "", }, }, } @@ -914,6 +932,7 @@ M.defaults.complete_path = { file_icons = false, git_icons = false, color_icons = true, + fzf_opts = { ["--no-multi"] = "" }, actions = { ["default"] = actions.complete }, } @@ -927,6 +946,7 @@ M.defaults.complete_file = { actions = { ["default"] = actions.complete }, previewer = M._default_previewer_fn, winopts = { preview = { hidden = "hidden" } }, + fzf_opts = { ["--no-multi"] = "" }, } M.defaults.complete_line = { complete = true } diff --git a/lua/fzf-lua/fzf.lua b/lua/fzf-lua/fzf.lua index 641bd42f..33fd0dc9 100644 --- a/lua/fzf-lua/fzf.lua +++ b/lua/fzf-lua/fzf.lua @@ -6,6 +6,7 @@ local uv = vim.loop local utils = require "fzf-lua.utils" +local libuv = require "fzf-lua.libuv" local M = {} @@ -29,69 +30,11 @@ 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 fzf_cli_args string[] ---@param opts table ---@return table selected ---@return integer exit_code @@ -102,7 +45,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 cmd = { opts.fzf_bin or "fzf" } local fifotmpname = utils.__IS_WINDOWS and utils.windows_pipename() or tempname() local outputtmpname = tempname() @@ -113,8 +56,12 @@ function M.raw_fzf(contents, fzf_cli_args, opts) -- instance never terminates which hangs fzf on exit local FZF_DEFAULT_COMMAND = nil - if fzf_cli_args then cmd = cmd .. " " .. fzf_cli_args end - if opts.fzf_cli_args then cmd = cmd .. " " .. opts.fzf_cli_args end + utils.tbl_extend(cmd, fzf_cli_args or {}) + if type(opts.fzf_cli_args) == "table" then + utils.tbl_extend(cmd, opts.fzf_cli_args) + elseif type(opts.fzf_cli_args) == "string" then + utils.tbl_extend(cmd, { opts.fzf_cli_args }) + end if contents then if type(contents) == "string" and #contents > 0 then @@ -134,14 +81,16 @@ function M.raw_fzf(contents, fzf_cli_args, opts) local bin_is_sk = opts.fzf_bin and opts.fzf_bin:match("sk$") local fish_shell = vim.o.shell and vim.o.shell:match("fish$") if not fish_shell or bin_is_sk then - cmd = ("%s < %s"):format(cmd, vim.fn.shellescape(fifotmpname)) + table.insert(cmd, "<") + table.insert(cmd, libuv.shellescape(fifotmpname)) else - FZF_DEFAULT_COMMAND = string.format("cat %s", vim.fn.shellescape(fifotmpname)) + FZF_DEFAULT_COMMAND = string.format("cat %s", libuv.shellescape(fifotmpname)) end end end - cmd = ("%s > %s"):format(cmd, vim.fn.shellescape(outputtmpname)) + table.insert(cmd, ">") + table.insert(cmd, libuv.shellescape(outputtmpname)) local fd, output_pipe = nil, nil local finish_called = false @@ -292,26 +241,25 @@ function M.raw_fzf(contents, fzf_cli_args, opts) end if opts.debug then - print("[Fzf-lua]: fzf cmd:", cmd) + print("[Fzf-lua]: FZF_DEFAULT_COMMAND:", FZF_DEFAULT_COMMAND) + print("[Fzf-lua]: fzf cmd:", table.concat(cmd, " ")) end local co = coroutine.running() local jobstart = opts.is_fzf_tmux and vim.fn.jobstart or vim.fn.termopen - local shell = utils.__IS_WINDOWS and "cmd" or "sh" - ---@type string - local shell_cmd + local shell_cmd = utils.__IS_WINDOWS + and { "cmd", "/d", "/e:off", "/f:off", "/v:off", "/c" } + or { "sh", "-c" } if utils.__IS_WINDOWS then - cmd = windows_cmd_escape(cmd) - shell_cmd = { shell, "/d", "/e:off", "/f:off", "/v:off", "/c", cmd } + utils.tbl_extend(shell_cmd, cmd) else - shell_cmd = { shell, "-c", cmd } + table.insert(shell_cmd, table.concat(cmd, " ")) end - jobstart(shell_cmd, { cwd = cwd, pty = true, env = { - ["SHELL"] = shell, + ["SHELL"] = shell_cmd[1], ["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND, ["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND, }, diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua index 4ed78ea6..022f1a18 100644 --- a/lua/fzf-lua/init.lua +++ b/lua/fzf-lua/init.lua @@ -1,3 +1,9 @@ +-- make value truthy so we can load the path module and subsequently +-- the libuv module without overriding the global require used only +-- for spawn_stdio headless instances, this way we can call +-- require("fzf-lua") from test specs (which also run headless) +vim.g.fzf_lua_directory = "" + local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" @@ -6,8 +12,7 @@ do -- using the latest nightly 'NVIM v0.6.0-dev+569-g2ecf0a4c6' -- 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 + vim.g.fzf_lua_directory = path.normalize(path.parent(currFile)) -- Manually source the vimL script containing ':FzfLua' cmd if not vim.g.loaded_fzf_lua then diff --git a/lua/fzf-lua/lib/base64.lua b/lua/fzf-lua/lib/base64.lua new file mode 100644 index 00000000..ce82b6bf --- /dev/null +++ b/lua/fzf-lua/lib/base64.lua @@ -0,0 +1,206 @@ +--[[ + +Source: https://github.com/iskolbin/lbase64 + + base64 -- v1.5.3 public domain Lua base64 encoder/decoder + no warranty implied; use at your own risk + + Needs bit32.extract function. If not present it's implemented using BitOp + or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua + implementation inspired by Rici Lake's post: + http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html + + author: Ilya Kolbin (iskolbin@gmail.com) + url: github.com/iskolbin/lbase64 + + COMPATIBILITY + + Lua 5.1+, LuaJIT + + LICENSE + + See end of file for license information. + +--]] +---@diagnostic disable + +local base64 = {} + +local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode +if not extract then + if _G.bit then -- LuaJIT + local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band + extract = function(v, from, width) + return band(shr(v, from), shl(1, width) - 1) + end + elseif _G._VERSION == "Lua 5.1" then + extract = function(v, from, width) + local w = 0 + local flag = 2 ^ from + for i = 0, width - 1 do + local flag2 = flag + flag + if v % flag2 >= flag then + w = w + 2 ^ i + end + flag = flag2 + end + return w + end + else -- Lua 5.3+ + extract = load [[return function( v, from, width ) + return ( v >> from ) & ((1 << width) - 1) + end]] () + end +end + + +function base64.makeencoder(s62, s63, spad) + local encoder = {} + for b64code, char in pairs { [0] = 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', + '3', '4', '5', '6', '7', '8', '9', s62 or '+', s63 or '/', spad or '=' } do + encoder[b64code] = char:byte() + end + return encoder +end + +function base64.makedecoder(s62, s63, spad) + local decoder = {} + for b64code, charcode in pairs(base64.makeencoder(s62, s63, spad)) do + decoder[charcode] = b64code + end + return decoder +end + +local DEFAULT_ENCODER = base64.makeencoder() +local DEFAULT_DECODER = base64.makedecoder() + +local char, concat = string.char, table.concat + +function base64.encode(str, encoder, usecaching) + encoder = encoder or DEFAULT_ENCODER + local t, k, n = {}, 1, #str + local lastn = n % 3 + local cache = {} + for i = 1, n - lastn, 3 do + local a, b, c = str:byte(i, i + 2) + local v = a * 0x10000 + b * 0x100 + c + local s + if usecaching then + s = cache[v] + if not s then + s = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], + encoder[extract(v, 0, 6)]) + cache[v] = s + end + else + s = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], + encoder[extract(v, 0, 6)]) + end + t[k] = s + k = k + 1 + end + if lastn == 2 then + local a, b = str:byte(n - 1, n) + local v = a * 0x10000 + b * 0x100 + t[k] = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], + encoder[64]) + elseif lastn == 1 then + local v = str:byte(n) * 0x10000 + t[k] = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[64], encoder[64]) + end + return concat(t) +end + +function base64.decode(b64, decoder, usecaching) + decoder = decoder or DEFAULT_DECODER + local pattern = '[^%w%+%/%=]' + if decoder then + local s62, s63 + for charcode, b64code in pairs(decoder) do + if b64code == 62 then s62 = charcode + elseif b64code == 63 then s63 = charcode + end + end + pattern = ('[^%%w%%%s%%%s%%=]'):format(char(s62), char(s63)) + end + b64 = b64:gsub(pattern, '') + local cache = usecaching and {} + local t, k = {}, 1 + local n = #b64 + local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0 + for i = 1, padding > 0 and n - 4 or n, 4 do + local a, b, c, d = b64:byte(i, i + 3) + local s + if usecaching then + local v0 = a * 0x1000000 + b * 0x10000 + c * 0x100 + d + s = cache[v0] + if not s then + local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40 + decoder[d] + s = char(extract(v, 16, 8), extract(v, 8, 8), extract(v, 0, 8)) + cache[v0] = s + end + else + local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40 + decoder[d] + s = char(extract(v, 16, 8), extract(v, 8, 8), extract(v, 0, 8)) + end + t[k] = s + k = k + 1 + end + if padding == 1 then + local a, b, c = b64:byte(n - 3, n - 1) + local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40 + t[k] = char(extract(v, 16, 8), extract(v, 8, 8)) + elseif padding == 2 then + local a, b = b64:byte(n - 3, n - 2) + local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + t[k] = char(extract(v, 16, 8)) + end + return concat(t) +end + +return base64 + +--[[ +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2018 Ilya Kolbin +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +--]] diff --git a/lua/fzf-lua/lib/serpent.lua b/lua/fzf-lua/lib/serpent.lua new file mode 100644 index 00000000..502aab8d --- /dev/null +++ b/lua/fzf-lua/lib/serpent.lua @@ -0,0 +1,158 @@ +--[[ + +Source: https://github.com/pkulchenko/serpent + +--]] +---@diagnostic disable +local n, v = "serpent", "0.303" -- (C) 2012-18 Paul Kulchenko; MIT License +local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" +local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} +local badtype = {thread = true, userdata = true, cdata = true} +local getmetatable = debug and debug.getmetatable or getmetatable +local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+ +local keyword, globals, G = {}, {}, (_G or _ENV) +for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', + 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', + 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end +for k,v in pairs(G) do globals[v] = k end -- build func to name mapping +for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do + for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end + +local function s(t, opts) + local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum + local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge + local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) + local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring + local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) + local numformat = opts.numformat or "%.17g" + local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 + local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", + -- tostring(val) is needed because __tostring may return a non-string value + function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end + local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or numformat:format(s)) + or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 + or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end + -- handle radix changes in some locales + if opts.fixradix and (".1f"):format(1.2) ~= "1.2" then + local origsafestr = safestr + safestr = function(s) return type(s) == "number" + and (nohuge and snum[tostring(s)] or numformat:format(s):gsub(",",".")) or origsafestr(s) + end + end + local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end + local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal + and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end + local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] + local n = name == nil and '' or name + local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] + local safe = plain and n or '['..safestr(n)..']' + return (path or '')..(plain and path and '.' or '')..safe, safe end + local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding + local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} + local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end + table.sort(k, function(a,b) + -- sort numeric keys first: k[key] is not nil for numerical keys + return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) + < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end + local function val2str(t, name, indent, insref, path, plainindex, level) + local ttype, level, mt = type(t), (level or 0), getmetatable(t) + local spath, sname = safename(path, name) + local tag = plainindex and + ((type(name) == "number") and '' or name..space..'='..space) or + (name ~= nil and sname..space..'='..space or '') + if seen[t] then -- already seen this element + sref[#sref+1] = spath..space..'='..space..seen[t] + return tag..'nil'..comment('ref', level) + end + -- protect from those cases where __tostring may fail + if type(mt) == 'table' and metatostring ~= false then + local to, tr = pcall(function() return mt.__tostring(t) end) + local so, sr = pcall(function() return mt.__serialize(t) end) + if (to or so) then -- knows how to serialize itself + seen[t] = insref or spath + t = so and sr or tr + ttype = type(t) + end -- new value falls through to be serialized + end + if ttype == "table" then + if level >= maxl then return tag..'{}'..comment('maxlvl', level) end + seen[t] = insref or spath + if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty + if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end + local maxn, o, out = math.min(#t, maxnum or #t), {}, {} + for key = 1, maxn do o[key] = key end + if not maxnum or #o < maxnum then + local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables + for key in pairs(t) do + if o[key] ~= key then n = n + 1; o[n] = key end + end + end + if maxnum and #o > maxnum then o[maxnum+1] = nil end + if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end + local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) + for n, key in ipairs(o) do + local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse + if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing + or opts.keyallow and not opts.keyallow[key] + or opts.keyignore and opts.keyignore[key] + or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types + or sparse and value == nil then -- skipping nils; do nothing + elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then + if not seen[key] and not globals[key] then + sref[#sref+1] = 'placeholder' + local sname = safename(iname, gensym(key)) -- iname is table for local variables + sref[#sref] = val2str(key,sname,indent,sname,iname,true) + end + sref[#sref+1] = 'placeholder' + local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' + sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) + else + out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1) + if maxlen then + maxlen = maxlen - #out[#out] + if maxlen < 0 then break end + end + end + end + local prefix = string.rep(indent or '', level) + local head = indent and '{\n'..prefix..indent or '{' + local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) + local tail = indent and "\n"..prefix..'}' or '}' + return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level) + elseif badtype[ttype] then + seen[t] = insref or spath + return tag..globerr(t, level) + elseif ttype == 'function' then + seen[t] = insref or spath + if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end + local ok, res = pcall(string.dump, t) + local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level) + return tag..(func or globerr(t, level)) + else return tag..safestr(t) end -- handle all other types + end + local sepr = indent and "\n" or ";"..space + local body = val2str(t, name, indent) -- this call also populates sref + local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' + local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' + return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" +end + +local function deserialize(data, opts) + local env = (opts and opts.safe == false) and G + or setmetatable({}, { + __index = function(t,k) return t end, + __call = function(t,...) error("cannot call functions") end + }) + local f, res = (loadstring or load)('return '..data, nil, nil, env) + if not f then f, res = (loadstring or load)(data, nil, nil, env) end + if not f then return f, res end + if setfenv then setfenv(f, env) end + return pcall(f) +end + +local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end +return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, + load = deserialize, + dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, + line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, + block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } diff --git a/lua/fzf-lua/libuv.lua b/lua/fzf-lua/libuv.lua index 957bd4a3..a4997eb2 100644 --- a/lua/fzf-lua/libuv.lua +++ b/lua/fzf-lua/libuv.lua @@ -1,6 +1,6 @@ local uv = vim.loop -local is_windows = vim.fn.has("win32") == 1 +local is_windows = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 local M = {} @@ -42,26 +42,41 @@ if not vim.g.fzf_lua_directory and #vim.api.nvim_list_uis() == 0 then end end +local base64 = require("fzf-lua.lib.base64") +local serpent = require("fzf-lua.lib.serpent") + -- save to upvalue for performance reasons local string_byte = string.byte local string_sub = string.sub -local function find_last_newline(str) +local function find_last(str, bytecode) for i = #str, 1, -1 do - if string_byte(str, i) == 10 then + if string_byte(str, i) == bytecode then return i end end end -local function find_next_newline(str, start_idx) +local function find_next(str, bytecode, start_idx) + local bytecodes = type(bytecode) == "table" and bytecode or { bytecode } for i = start_idx or 1, #str do - if string_byte(str, i) == 10 then - return i + for _, b in ipairs(bytecodes) do + if string_byte(str, i) == b then + return i, string.char(b) + end end end end +local function find_last_newline(str) + return find_last(str, 10) +end + +local function find_next_newline(str, start_idx) + return find_next(str, 10, start_idx) +end + + local function process_kill(pid, signal) if not pid or not tonumber(pid) then return false end if type(uv.os_getpriority(pid)) == "number" then @@ -99,7 +114,7 @@ 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 opts {cwd: string, cmd: string|table, env: table?, 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) @@ -125,12 +140,22 @@ M.spawn = function(opts, fn_transform, fn_done) -- https://github.com/luvit/luv/blob/master/docs.md -- uv.spawn returns tuple: handle, pid 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 args = is_windows and { "/d", "/e:off", "/f:off", "/v:on", "/c" } or { "-c" } + if type(opts.cmd) == "table" then + if is_windows then + ---@diagnostic disable-next-line: deprecated + table.move(opts.cmd, 1, #opts.cmd, #args + 1, args) + else + table.insert(args, table.concat(opts.cmd, " ")) + end + else + table.insert(args, tostring(opts.cmd)) + end local handle, pid = uv.spawn(shell, { args = args, stdio = { nil, output_pipe, error_pipe }, cwd = opts.cwd, + env = opts.env, verbatim = is_windows, }, function(code, signal) output_pipe:read_stop() @@ -304,9 +329,18 @@ M.spawn_nvim_fzf_cmd = function(opts, fn_transform, fn_preprocess) end ---@param opts table ----@param fn_transform string ----@param fn_preprocess string -M.spawn_stdio = function(opts, fn_transform, fn_preprocess) +---@param fn_transform_str string +---@param fn_preprocess_str string +M.spawn_stdio = function(opts, fn_transform_str, fn_preprocess_str) + -- attempt base64 decoding on all params + ---@param str string|table + ---@return string|table + local base64_conditional_decode = function(str) + if opts._base64 == false or type(str) ~= "string" then return str end + local ok, decoded = pcall(base64.decode, str) + return ok and decoded or str + end + ---@param fn_str string ---@return function? local function load_fn(fn_str) @@ -320,6 +354,17 @@ M.spawn_stdio = function(opts, fn_transform, fn_preprocess) return fn_loaded end + -- conditionally base64 decode, if not a base64 string, returns original value + opts = base64_conditional_decode(opts) + fn_transform_str = base64_conditional_decode(fn_transform_str) + fn_preprocess_str = base64_conditional_decode(fn_preprocess_str) + + -- opts must be a table, if opts is a string deserialize + if type(opts) == "string" then + _, opts = serpent.load(opts) + assert(type(opts) == "table") + end + -- stdin/stdout are already buffered, not stderr. This means -- that every character is flushed immedietely which caused -- rendering issues on Mac (#316, #287) and Linux (#414) @@ -334,13 +379,27 @@ M.spawn_stdio = function(opts, fn_transform, fn_preprocess) opts.stderr_to_stdout = true end - fn_transform = load_fn(fn_transform) - fn_preprocess = load_fn(fn_preprocess) + -- setup global vars + for k, v in pairs(opts.g or {}) do + _G[k] = v + if opts.debug == "v" or opts.debug == "verbose" then + io.stdout:write(string.format("[DEBUGV]: %s=%s\n", k, v)) + end + end + + local fn_transform = load_fn(fn_transform_str) + local fn_preprocess = load_fn(fn_preprocess_str) -- run the preprocessing fn if fn_preprocess then fn_preprocess(opts) end - if opts.debug then + if opts.debug == "v" or opts.debug == "verbose" then + for k, v in pairs(opts) do + io.stdout:write(string.format("[DEBUGV]: %s=%s\n", k, v)) + end + io.stdout:write(string.format("[DEBUGV]: fn_transform=%s\n", fn_transform_str)) + io.stdout:write(string.format("[DEBUGV]: fn_preprocess=%s\n", fn_preprocess_str)) + elseif opts.debug then io.stdout:write("[DEBUG]: " .. opts.cmd .. "\n") end @@ -456,9 +515,116 @@ end -- 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 blackslashes and +-- for nested quotes we double the blackslashes 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 -M.shellescape = function(s) +M.shellescape = function(s, win_style) + if is_windows 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('[%(%)%%!%^<>&|"]', 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) @@ -494,23 +660,18 @@ M.wrap_spawn_stdio = function(opts, fn_transform, fn_preprocess) 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( - is_windows and 'set "VIMRUNTIME=%s" & ' or "VIMRUNTIME=%s ", + 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 lua_cmd = ("lua loadfile([[%s]])().spawn_stdio(%s,%s,%s)") + :format( + is_windows and vim.fs.normalize(__FILE__) or __FILE__, + opts, fn_transform, fn_preprocess + ) local cmd_str = ("%s%s -n --headless --clean --cmd %s"):format( nvim_runtime, - M.shellescape(nvim_bin), - M.shellescape(cmd) + M.shellescape(is_windows and vim.fs.normalize(nvim_bin) or nvim_bin), + M.shellescape(lua_cmd) ) return cmd_str end diff --git a/lua/fzf-lua/make_entry.lua b/lua/fzf-lua/make_entry.lua index 4fb39bd7..e8a58fe9 100644 --- a/lua/fzf-lua/make_entry.lua +++ b/lua/fzf-lua/make_entry.lua @@ -2,6 +2,7 @@ local M = {} local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" +local libuv = require "fzf-lua.libuv" local config = nil -- attempt to load the current config @@ -274,8 +275,7 @@ M.glob_parse = function(query, opts) local glob_args = "" local search_query, glob_str = query:match("(.*)" .. opts.glob_separator .. "(.*)") for _, s in ipairs(utils.strsplit(glob_str, "%s")) do - glob_args = glob_args .. ("%s %s ") - :format(opts.glob_flag, vim.fn.shellescape(s)) + glob_args = glob_args .. ("%s %s "):format(opts.glob_flag, libuv.shellescape(s)) end return search_query, glob_args end @@ -336,11 +336,16 @@ M.preprocess = function(opts) -- arguments already supplied by 'wrap_spawn_stdio'. -- If no index was supplied use the last argument local idx = tonumber(i) and tonumber(i) + 6 or #vim.v.argv - if debug then - io.stdout:write(("[DEBUG]: argv(%d) = %s\n") - :format(idx, vim.fn.shellescape(vim.v.argv[idx]))) + local arg = vim.v.argv[idx] + if utils.__IS_WINDOWS then + -- fzf's {q} will send escaped blackslahes, unescape + arg = arg:gsub([[\\]], [[\]]) end - return vim.v.argv[idx] + if debug == "v" or debug == "verbose" then + io.stdout:write(("[DEBUGV]: raw_argv(%d) = %s\n"):format(idx, arg)) + io.stdout:write(("[DEBUGV]: esc_argv(%d) = %s\n"):format(idx, libuv.shellescape(arg))) + end + return arg end -- live_grep replace pattern with last argument @@ -350,7 +355,7 @@ M.preprocess = function(opts) -- did the caller request rg with glob support? -- manipulation needs to be done before the argv hack if opts.rg_glob and has_argvz then - local query = argv() + local query = argv(nil, opts.debug) local search_query, glob_args = M.glob_parse(query, opts) if glob_args then -- gsub doesn't like single % on rhs @@ -359,7 +364,7 @@ M.preprocess = function(opts) -- insert glob args before `-- {argvz}` or `-e {argvz}` repositioned -- at the end of the command preceding the search query (#781, #794) opts.cmd = M.rg_insert_args(opts.cmd, glob_args, argvz) - opts.cmd = opts.cmd:gsub(argvz, vim.fn.shellescape(search_query)) + opts.cmd = opts.cmd:gsub(argvz, libuv.shellescape(search_query)) end end @@ -369,13 +374,24 @@ M.preprocess = function(opts) opts.cmd = opts.cmd:gsub("{argv.*}", function(x) local idx = x:match("{argv(.*)}") - -- \\ -> \ 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)) + return libuv.shellescape(argv(idx, not opts.rg_glob and opts.debug)) end) end + if utils.__IS_WINDOWS and opts.cmd:match("!") 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 ^ + opts.cmd = opts.cmd:gsub('[%(%)%%!%^<>&|"]', function(x) + return "^" .. x + end) + -- make sure all ! are escaped at least twice + opts.cmd = opts.cmd:gsub("[^%^]%^!", function(x) + return x:sub(1, 1) .. "^" .. x:sub(2) + end) + end + return opts end @@ -402,8 +418,18 @@ 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 colon_start_idx = 1 + if utils.__IS_WINDOWS then + if string.byte(x, #x) == 13 then + -- strip ^M added by the "dir /s/b" command + x = x:sub(1, #x - 1) + end + if path.is_absolute(x) then + -- ignore the first colon in the drive spec, e.g c:\ + colon_start_idx = 3 + end + end + local colon_idx = utils.find_next_char(x, COLON_BYTE, colon_start_idx) or 0 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 @@ -416,18 +442,18 @@ M.file = function(x, opts) -- fd v8.3 requires adding '--strip-cwd-prefix' to remove -- the './' prefix, will not work with '--color=always' -- https://github.com/sharkdp/fd/blob/master/CHANGELOG.md - if not (opts.strip_cwd_prefix == false) and path.starts_with_cwd(filepath) then + if not (opts.strip_cwd_prefix == false) then filepath = path.strip_cwd_prefix(filepath) end -- make path relative if opts.cwd and #opts.cwd > 0 then - filepath = path.relative(filepath, opts.cwd) + filepath = path.relative_to(filepath, opts.cwd) end - if path.starts_with_separator(filepath) then + if path.is_absolute(filepath) then -- filter for cwd only if opts.cwd_only then local cwd = opts.cwd or vim.loop.cwd() - if not path.is_relative(filepath, cwd) then + if not path.is_relative_to(filepath, cwd) then return nil end end diff --git a/lua/fzf-lua/path.lua b/lua/fzf-lua/path.lua index 222685e6..b58f6af3 100644 --- a/lua/fzf-lua/path.lua +++ b/lua/fzf-lua/path.lua @@ -1,136 +1,231 @@ local utils = require "fzf-lua.utils" +local libuv = require "fzf-lua.libuv" local string_sub = string.sub local string_byte = string.byte local M = {} -M.SEPARATOR = "/" +M.dot_byte = string_byte(".") +M.colon_byte = string_byte(":") +M.fslash_byte = string_byte("/") +M.bslash_byte = string_byte([[\]]) -M.separator = function() - return M.SEPARATOR +---@param path string? +---@return string +M.separator = function(path) + -- auto-detect separator from fully qualified paths, e.g. "C:\..." or "~/..." + if utils.__IS_WINDOWS and path then + local maybe_separators = { string_byte(path, 3), string_byte(path, 2) } + for _, s in ipairs(maybe_separators) do + if M.byte_is_separator(s) then + return string.char(s) + end + end + end + return string.char(utils._if_win(M.bslash_byte, M.fslash_byte)) end -M.dot_byte = string_byte(".") -M.separator_byte = string_byte(M.SEPARATOR) - -M.starts_with_separator = function(path) - return string_byte(path, 1) == M.separator_byte +M.separator_byte = function(path) + return string_byte(M.separator(path), 1) end -M.ends_with_separator = function(path) - return string_byte(path, #path) == M.separator_byte +---@param byte number +---@return boolean +M.byte_is_separator = function(byte) + if utils.__IS_WINDOWS then + -- path on windows can also be the result of `vim.fs.normalize` + -- so we need to test for the presense of both slash types + return byte == M.bslash_byte or byte == M.fslash_byte + else + return byte == M.fslash_byte + end end -M.starts_with_cwd = function(path) - return #path > 1 - and string_byte(path, 1) == M.dot_byte - and string_byte(path, 2) == M.separator_byte - -- return path:match("^."..M.SEPARATOR) ~= nil +M.is_separator = function(c) + return M.byte_is_separator(string_byte(c, 1)) end -M.strip_cwd_prefix = function(path) - return #path > 2 and path:sub(3) +---@param path string +---@return boolean +M.ends_with_separator = function(path) + return M.byte_is_separator(string_byte(path, #path)) end -function M.tail(path) - local end_idx = M.ends_with_separator(path) and (#path - 1) or #path - for i = end_idx, 1, -1 do - if string_byte(path, i) == M.separator_byte then - return path:sub(i + 1) - end +---@param path string +---@return string +function M.add_trailing(path) + if M.ends_with_separator(path) then + return path end - return path + return path .. M.separator(path) end ---@param path string ---@return string -function M.extension(path) - for i = #path, 1, -1 do - if string_byte(path, i) == 46 then - return path:sub(i + 1) - end +function M.remove_trailing(path) + while M.ends_with_separator(path) do + path = path:sub(1, #path - 1) end return path end -function M.to_matching_str(path) - -- return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)'):gsub('(%_)', '(%%_)') - -- above is missing other lua special chars like '+' etc (#315) - return utils.lua_regex_escape(path) +---@param path string +---@return boolean +M.is_absolute = function(path) + return utils._if_win( + string_byte(path, 2) == M.colon_byte, + string_byte(path, 1) == M.fslash_byte) 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) - return ret +---@param path string +---@return boolean +M.has_cwd_prefix = function(path) + return #path > 1 + and string_byte(path, 1) == M.dot_byte + and M.byte_is_separator(string_byte(path, 2)) end -function M.split(path) - return path:gmatch("[^" .. M.SEPARATOR .. "]+" .. M.SEPARATOR .. "?") +---@param path string +---@return string +M.strip_cwd_prefix = function(path) + if M.has_cwd_prefix(path) then + return #path > 2 and path:sub(3) or "" + else + return path + end end ----Get the basename of the given path. +---Get the basename|tail of the given path. ---@param path string ---@return string -function M.basename(path) - path = M.remove_trailing(path) - local i = path:match("^.*()" .. M.SEPARATOR) - if not i then return path end - return path:sub(i + 1, #path) +function M.tail(path) + local end_idx = M.ends_with_separator(path) and #path - 1 or #path + for i = end_idx, 1, -1 do + if M.byte_is_separator(string_byte(path, i)) then + return path:sub(i + 1) + end + end + return path end ----Get the path to the parent directory of the given path. Returns `nil` if the ----path has no parent. +M.basename = M.tail + +---Get the path to the parent directory of the given path. +-- Returns `nil` if the path has no parent. ---@param path string ---@param remove_trailing boolean ----@return string|nil +---@return string? function M.parent(path, remove_trailing) - path = " " .. M.remove_trailing(path) - local i = path:match("^.+()" .. M.SEPARATOR) - if not i then return nil end - path = path:sub(2, i) - if remove_trailing then - path = M.remove_trailing(path) + path = M.remove_trailing(path) + for i = #path, 1, -1 do + if M.byte_is_separator(string_byte(path, i)) then + local parent = path:sub(1, i) + if remove_trailing then + parent = M.remove_trailing(parent) + end + return parent + end end - return path end ----Get a path relative to another path. ---@param path string ----@param relative_to string ---@return string -function M.relative(path, relative_to) - local p, _ = path:gsub("^" .. M.to_matching_str(M.add_trailing(relative_to)), "") +function M.normalize(path) + local p = M.tilde_to_HOME(path) + if utils.__IS_WINDOWS then + p = p:gsub([[\]], [[/]]) + end return p end -function M.is_relative(path, relative_to) - local p = path:match("^" .. M.to_matching_str(M.add_trailing(relative_to))) - return p ~= nil +---@param p1 string +---@param p2 string +---@return boolean +function M.equals(p1, p2) + p1 = M.normalize(M.remove_trailing(p1)) + p2 = M.normalize(M.remove_trailing(p2)) + if utils.__IS_WINDOWS then + p1 = string.lower(p1) + p2 = string.lower(p2) + end + return p1 == p2 end -function M.add_trailing(path) - if path:sub(-1) == M.SEPARATOR then - return path - end +---@param path string +---@param relative_to string +---@return boolean, string? +function M.is_relative_to(path, relative_to) + -- make sure paths end with a separator + local path_no_trailing = M.tilde_to_HOME(path) + path = M.add_trailing(path_no_trailing) + relative_to = M.add_trailing(M.tilde_to_HOME(relative_to)) + local pidx, ridx = 1, 1 + repeat + local pbyte = string.byte(path, pidx) + local rbyte = string.byte(relative_to, ridx) + if M.byte_is_separator(pbyte) and M.byte_is_separator(rbyte) then + -- both path and relative_to have a separator part + -- which may differ in length if there are multiple + -- separators, e.g. "/some/path" and "//some//path" + repeat + pidx = pidx + 1 + until not M.byte_is_separator(string.byte(path, pidx)) + repeat + ridx = ridx + 1 + until not M.byte_is_separator(string.byte(relative_to, ridx)) + elseif utils.__IS_WINDOWS + -- case insensitive matching on windows + and string.char(pbyte):lower() == string.char(rbyte):lower() + -- byte matching on Unix/BSD + or pbyte == rbyte then + -- character matches, move to next + pidx = pidx + 1 + ridx = ridx + 1 + else + -- characters don't match + return false, nil + end + until ridx > #relative_to + return true, pidx <= #path_no_trailing and path_no_trailing:sub(pidx) or "." +end - return path .. M.SEPARATOR +---Get a path relative to another path. +---@param path string +---@param relative_to string +---@return string +function M.relative_to(path, relative_to) + local is_relative_to, relative_path = M.is_relative_to(path, relative_to) + return is_relative_to and relative_path or path end -function M.remove_trailing(path) - local p, _ = path:gsub(M.SEPARATOR .. "$", "") - return p +---@param path string +---@return string +function M.extension(path) + for i = #path, 1, -1 do + if string_byte(path, i) == M.dot_byte then + return path:sub(i + 1) + end + end + return "" end -local function find_next(str, char, start_idx) - local i_char = string_byte(char, 1) - for i = start_idx or 1, #str do - if string_byte(str, i) == i_char then - return i +---@param paths string[] +---@return string +function M.join(paths) + -- Separator is always / (even on windows) unless we + -- detect it from fully qualified paths, e.g. "C:\..." + local separator = M.separator(paths[1]) + local ret = "" + for i = 1, #paths do + local p = paths[i] + if p then + if i < #paths and not M.ends_with_separator(p) then + p = p .. separator + end + ret = ret .. p end end + return ret end -- I'm not sure why this happens given that neovim is single threaded @@ -141,8 +236,7 @@ M.HOME = function() if not M.__HOME then -- 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 = utils.__IS_WINDOWS and os.getenv("USERPROFILE") or os.getenv("HOME") + M.__HOME = utils._if_win(os.getenv("USERPROFILE"), os.getenv("HOME")) end return M.__HOME end @@ -156,16 +250,43 @@ 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 + if not path then return end + if utils.__IS_WINDOWS then + local home = M.HOME() + if path:sub(1, #home):lower() == home:lower() then + path = "~" .. path:sub(#home + 1) + end + else + path = path:gsub("^" .. utils.lua_regex_escape(M.HOME()), "~") + end + return path +end + +local function find_next_separator(str, start_idx) + local SEPARATOR_BYTES = utils._if_win( + { M.fslash_byte, M.bslash_byte }, { M.fslash_byte }) + for i = start_idx or 1, #str do + for _, byte in ipairs(SEPARATOR_BYTES) do + if string_byte(str, i) == byte then + return i + end + end + end end function M.shorten(path, max_len) - local sep = M.SEPARATOR + local sep = M.separator(path) local parts = {} local start_idx = 1 max_len = max_len and tonumber(max_len) > 0 and max_len or 1 + if utils.__IS_WINDOWS and M.is_absolute(path) then + -- do not shorten "C:\" to "C", for glob to succeed + -- we need the paths to start with a valid drive spec + table.insert(parts, path:sub(1, 2)) + start_idx = 4 + end repeat - local i = find_next(path, sep, start_idx) + local i = find_next_separator(path, start_idx) local end_idx = i and start_idx + math.min(i - start_idx, max_len) - 1 or nil local part = string_sub(path, start_idx, end_idx) if end_idx and part == "." and i - start_idx > 1 then @@ -179,11 +300,24 @@ end function M.lengthen(path) -- we use 'glob_escape' to escape \{} (#548) - path = utils.glob_escape(path) - return vim.fn.glob(path:gsub(M.SEPARATOR, "%*" .. M.SEPARATOR) - -- remove the starting '*/' if any - :gsub("^%*" .. M.SEPARATOR, M.SEPARATOR)):match("[^\n]+") - or string.format("", path) + local separator = M.separator(path) + local glob_expr = utils.glob_escape(path) + local glob_expr_prefix = "" + if M.is_absolute(path) then + -- don't prefix with * the leading / on UNIX or C:\ on windows + if utils.__IS_WINDOWS then + glob_expr_prefix = glob_expr:sub(1, 3) + glob_expr = glob_expr:sub(4) + else + glob_expr_prefix = glob_expr:sub(1, 1) + glob_expr = glob_expr:sub(2) + end + end + -- replace separator with wildcard + separator + glob_expr = glob_expr_prefix .. glob_expr:gsub(separator, "%*" .. separator) + return vim.fn.glob(glob_expr):match("[^\n]+") + -- or string.format("", path) + or string.format("", glob_expr) end local function lastIndexOf(haystack, needle) @@ -246,8 +380,7 @@ function M.entry_to_file(entry, opts, force_uri) stripped = M.tilde_to_HOME(stripped) local isURI = stripped:match("^%a+://") -- Prepend cwd before constructing the URI (#341) - if cwd and #cwd > 0 and not isURI and - not M.starts_with_separator(stripped) then + if cwd and #cwd > 0 and not isURI and not M.is_absolute(stripped) then stripped = M.join({ cwd, stripped }) end -- #336: force LSP jumps using 'vim.lsp.util.jump_to_location' @@ -272,6 +405,11 @@ function M.entry_to_file(entry, opts, force_uri) end local s = utils.strsplit(stripped, ":") if not s[1] then return {} end + if utils.__IS_WINDOWS and M.is_absolute(stripped) then + -- adjust split for "C:\..." + s[1] = s[1] .. ":" .. s[2] + table.remove(s, 2) + end local file = s[1] local line = tonumber(s[2]) local col = tonumber(s[3]) @@ -336,7 +474,7 @@ function M.git_cwd(cmd, opts) for _, a in ipairs(git_args) do if o[a[1]] then o[a[1]] = a.noexpand and o[a[1]] or vim.fn.expand(o[a[1]]) - args = args .. ("%s %s "):format(a[2], vim.fn.shellescape(o[a[1]])) + args = args .. ("%s %s "):format(a[2], libuv.shellescape(o[a[1]])) end end cmd = cmd:gsub("^git ", "git " .. args) diff --git a/lua/fzf-lua/previewer/builtin.lua b/lua/fzf-lua/previewer/builtin.lua index ce5f7d17..e5941d05 100644 --- a/lua/fzf-lua/previewer/builtin.lua +++ b/lua/fzf-lua/previewer/builtin.lua @@ -287,7 +287,9 @@ end function Previewer.base:cmdline(_) local act = shell.raw_action(function(items, _, _) - self:display_entry(items[1]) + -- TODO: is this the right place to remove the double blackslash + -- added by fzf's field index expression on windows? + self:display_entry(items[1]:gsub([[\\]], [[\]])) return "" end, "{}", self.opts.debug) return act @@ -303,8 +305,7 @@ function Previewer.base:zero(_) -- currently awaiting an upstream fix: -- 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 + self._zero_lock = self._zero_lock or path.normalize(vim.fn.tempname()) local act = string.format("execute-silent(mkdir %s && %s)", libuv.shellescape(self._zero_lock), shell.raw_action(function(_, _, _) @@ -430,7 +431,7 @@ function Previewer.buffer_or_file:start_ueberzug() utils.io_system({ "mkfifo", self._ueberzug_fifo }) self._ueberzug_job = vim.fn.jobstart({ "sh", "-c", ("tail --follow %s | ueberzug layer --parser json") - :format(vim.fn.shellescape(self._ueberzug_fifo)) + :format(libuv.shellescape(self._ueberzug_fifo)) }, { on_exit = function(_, rc, _) if rc ~= 0 and rc ~= 143 then @@ -506,7 +507,7 @@ function Previewer.buffer_or_file:populate_terminal_cmd(tmpbuf, cmd, entry) width = wincfg.width, height = wincfg.height, scaler = self.ueberzug_scaler, - path = path.starts_with_separator(entry.path) and entry.path or + path = path.is_absolute(entry.path) and entry.path or path.join({ self.opts.cwd or uv.cwd(), entry.path }), } local json = vim.json.encode(params) @@ -584,7 +585,7 @@ function Previewer.buffer_or_file:populate_preview_buf(entry_str) -- buffer is not loaded, can happen when calling "lines" with `set nohidden` -- or when starting nvim with an arglist, fix entry.path since it contains -- filename only - entry.path = path.relative(vim.api.nvim_buf_get_name(entry.bufnr), vim.loop.cwd()) + entry.path = path.relative_to(vim.api.nvim_buf_get_name(entry.bufnr), vim.loop.cwd()) end if not self:should_load_buffer(entry) then -- same file/buffer as previous entry @@ -870,7 +871,7 @@ function Previewer.buffer_or_file:update_border(entry) local filepath = entry.path if filepath then if self.opts.cwd then - filepath = path.relative(entry.path, self.opts.cwd) + filepath = path.relative_to(entry.path, self.opts.cwd) end filepath = path.HOME_to_tilde(filepath) end @@ -1025,7 +1026,7 @@ function Previewer.marks:parse_entry(entry_str) else filepath = res end - filepath = path.relative(filepath, vim.loop.cwd()) + filepath = path.relative_to(filepath, vim.loop.cwd()) end return { bufnr = bufnr, @@ -1048,7 +1049,7 @@ function Previewer.jumps:parse_entry(entry_str) if filepath then local ok, res = pcall(vim.fn.expand, filepath) if ok then - filepath = path.relative(res, vim.loop.cwd()) + filepath = path.relative_to(res, vim.loop.cwd()) end if not vim.loop.fs_stat(filepath) then -- file is not accessible, @@ -1216,7 +1217,7 @@ function Previewer.quickfix:populate_preview_buf(entry_str) local lines = {} for _, e in ipairs(qf_list.items) do table.insert(lines, string.format("%s|%d col %d|%s", - path.HOME_to_tilde(path.relative( + path.HOME_to_tilde(path.relative_to( vim.api.nvim_buf_get_name(e.bufnr), vim.loop.cwd())), e.lnum, e.col, e.text)) end diff --git a/lua/fzf-lua/previewer/fzf.lua b/lua/fzf-lua/previewer/fzf.lua index 4c72d331..da263cb0 100644 --- a/lua/fzf-lua/previewer/fzf.lua +++ b/lua/fzf-lua/previewer/fzf.lua @@ -1,6 +1,7 @@ local path = require "fzf-lua.path" local shell = require "fzf-lua.shell" local utils = require "fzf-lua.utils" +local libuv = require "fzf-lua.libuv" local Object = require "fzf-lua.class" local Previewer = {} @@ -190,7 +191,7 @@ function Previewer.cmd_async:parse_entry_and_verify(entrystr) -- verify the file exists on disk and is accessible if #filepath == 0 or not vim.loop.fs_stat(filepath) then errcmd = ([[echo "%s: NO SUCH FILE OR ACCESS DENIED"]]):format( - filepath and #filepath > 0 and vim.fn.shellescape(filepath) or "") + filepath and #filepath > 0 and libuv.shellescape(filepath) or "") end return filepath, entry, errcmd end @@ -200,9 +201,7 @@ function Previewer.cmd_async:cmdline(o) local act = shell.raw_preview_action_cmd(function(items) local filepath, _, errcmd = self:parse_entry_and_verify(items[1]) local cmd = errcmd or ("%s %s %s"):format( - self.cmd, self.args, vim.fn.shellescape(filepath)) - -- uncomment to see the command in the preview window - -- cmd = vim.fn.shellescape(cmd) + self.cmd, self.args, libuv.shellescape(filepath)) return cmd end, "{}", self.opts.debug) return act @@ -234,9 +233,7 @@ function Previewer.bat_async:cmdline(o) self.theme and string.format([[--theme="%s"]], self.theme) or "", self.opts.line_field_index and string.format("--highlight-line=%d", entry.line) or "", line_range, - vim.fn.shellescape(filepath)) - -- uncomment to see the command in the preview window - -- cmd = vim.fn.shellescape(cmd) + libuv.shellescape(filepath)) return cmd end, "{}", self.opts.debug) return act @@ -298,7 +295,7 @@ function Previewer.git_diff:cmdline(o) elseif is_untracked then local stat = vim.loop.fs_stat(file.path) if stat and stat.type == "directory" then - cmd = "ls -la" + cmd = utils._if_win({ "dir" }, { "ls", "-la" }) else cmd = self.cmd_untracked end @@ -308,6 +305,12 @@ function Previewer.git_diff:cmdline(o) if self.pager and #self.pager > 0 and vim.fn.executable(self.pager:match("[^%s]+")) == 1 then pager = "| " .. self.pager + if utils.__IS_WINDOWS then + -- we are unable to use variables within a "cmd /c" without "!var!" variable expansion + -- https://superuser.com/questions/223104/setting-and-using-variable-within-same-command-line-in-windows-cmd-ex + pager = pager:gsub("%$[%a%d]+", function(x) return "!" .. x:sub(2) .. "!" end) + pager = pager:gsub("%%[%a%d]+%%", function(x) return "!" .. x:sub(2, #x - 1) .. "!" end) + end end -- with default commands we add the filepath at the end. -- If the user configured a more complex command, e.g.: @@ -316,7 +319,7 @@ function Previewer.git_diff:cmdline(o) -- } -- we use ':format' directly on the user's command, see -- issue #392 for more info (limiting diff output width) - local fname_escaped = vim.fn.shellescape(file.path) + local fname_escaped = libuv.shellescape(file.path) if cmd:match("[<{]file[}>]") then cmd = cmd:gsub("[<{]file[}>]", fname_escaped) elseif cmd:match("%%s") then @@ -324,12 +327,15 @@ function Previewer.git_diff:cmdline(o) else cmd = string.format("%s %s", cmd, fname_escaped) end - cmd = ("LINES=%d;COLUMNS=%d;FZF_PREVIEW_LINES=%d;FZF_PREVIEW_COLUMNS=%d;%s %s") - :format(fzf_lines, fzf_columns, fzf_lines, fzf_columns, cmd, pager) - cmd = "sh -c " .. vim.fn.shellescape(cmd) - -- uncomment to see the command in the preview window - -- cmd = vim.fn.shellescape(cmd) - return cmd + local env = { + ["LINES"] = fzf_lines, + ["COLUMNS"] = fzf_columns, + ["FZF_PREVIEW_LINES"] = fzf_lines, + ["FZF_PREVIEW_COLUMNS"] = fzf_columns, + } + local setenv = utils.shell_setenv_str(env) + cmd = string.format("%s %s %s", table.concat(setenv, " "), cmd, pager) + return { cmd = cmd, env = env } end, "{}", self.opts.debug) return act end @@ -359,9 +365,7 @@ function Previewer.man_pages:cmdline(o) local act = shell.raw_preview_action_cmd(function(items) -- local manpage = require'fzf-lua.providers.manpages'.getmanpage(items[1]) local manpage = items[1]:match("[^[,( ]+") - local cmd = self.cmd:format(vim.fn.shellescape(manpage)) - -- uncomment to see the command in the preview window - -- cmd = vim.fn.shellescape(cmd) + local cmd = self.cmd:format(libuv.shellescape(manpage)) return cmd end, "{}", self.opts.debug) return act @@ -383,7 +387,7 @@ function Previewer.help_tags:cmdline(o) local vimdoc = items[1]:match("[^%s]+$") local tag = items[1]:match("^[^%s]+") local ext = path.extension(vimdoc) - local cmd = self.cmd:format(vim.fn.shellescape(vimdoc)) + local cmd = self.cmd:format(libuv.shellescape(vimdoc)) -- If 'bat' is available attempt to get the helptag line -- and start the display of the help file from the tag if self.cmd:match("^bat ") then @@ -395,8 +399,6 @@ function Previewer.help_tags:cmdline(o) cmd = cmd .. string.format(" --line-range=%d:", tonumber(line)) end end - -- uncomment to see the command in the preview window - -- cmd = vim.fn.shellescape(cmd) return cmd end, "{}", self.opts.debug) return act diff --git a/lua/fzf-lua/providers/buffers.lua b/lua/fzf-lua/providers/buffers.lua index d9af3e93..51568e52 100644 --- a/lua/fzf-lua/providers/buffers.lua +++ b/lua/fzf-lua/providers/buffers.lua @@ -34,9 +34,9 @@ local filter_buffers = function(opts, unfiltered) excluded[b] = true elseif opts.no_term_buffers and utils.is_term_buffer(b) then excluded[b] = true - elseif opts.cwd_only and not path.is_relative(vim.api.nvim_buf_get_name(b), vim.loop.cwd()) then + elseif opts.cwd_only and not path.is_relative_to(vim.api.nvim_buf_get_name(b), vim.loop.cwd()) then excluded[b] = true - elseif opts.cwd and not path.is_relative(vim.api.nvim_buf_get_name(b), opts.cwd) then + elseif opts.cwd and not path.is_relative_to(vim.api.nvim_buf_get_name(b), opts.cwd) then excluded[b] = true end if utils.buf_is_qf(b) then @@ -118,7 +118,7 @@ local function gen_buffer_entry(opts, buf, max_bufnr, cwd) local flags = hidden .. readonly .. changed local leftbr = "[" local rightbr = "]" - local bufname = #buf.info.name > 0 and path.relative(buf.info.name, cwd or vim.loop.cwd()) + local bufname = #buf.info.name > 0 and path.relative_to(buf.info.name, cwd or vim.loop.cwd()) if opts.filename_only then bufname = path.basename(bufname) end @@ -299,10 +299,6 @@ M.buffer_lines = function(opts) end)() end - if opts.search and #opts.search > 0 then - opts.fzf_opts["--query"] = vim.fn.shellescape(opts.search) - end - opts = core.set_fzf_field_index(opts, "{3}", opts._is_skim and "{}" or "{..-2}") core.fzf_exec(contents, opts) diff --git a/lua/fzf-lua/providers/colorschemes.lua b/lua/fzf-lua/providers/colorschemes.lua index b2b5faa7..88a3d11b 100644 --- a/lua/fzf-lua/providers/colorschemes.lua +++ b/lua/fzf-lua/providers/colorschemes.lua @@ -34,8 +34,6 @@ M.colorschemes = function(opts) end, colors) end - opts.fzf_opts["--no-multi"] = "" - if opts.live_preview then -- must add ':nohidden' or fzf ignores the preview action opts.fzf_opts["--preview-window"] = "nohidden:right:0" @@ -120,8 +118,6 @@ M.highlights = function(opts) end end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(contents, opts) end diff --git a/lua/fzf-lua/providers/dap.lua b/lua/fzf-lua/providers/dap.lua index 37c07efe..0437df97 100644 --- a/lua/fzf-lua/providers/dap.lua +++ b/lua/fzf-lua/providers/dap.lua @@ -1,6 +1,7 @@ local core = require "fzf-lua.core" local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" +local libuv = require "fzf-lua.libuv" local config = require "fzf-lua.config" local actions = require "fzf-lua.actions" local make_entry = require "fzf-lua.make_entry" @@ -41,8 +42,6 @@ M.commands = function(opts) end, } - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end @@ -77,8 +76,6 @@ M.configurations = function(opts) end, } - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end @@ -145,7 +142,7 @@ M.breakpoints = function(opts) end if opts.fzf_opts["--header"] == nil then - opts.fzf_opts["--header"] = vim.fn.shellescape((":: %s to delete a Breakpoint") + opts.fzf_opts["--header"] = libuv.shellescape((":: %s to delete a Breakpoint") :format(utils.ansi_codes.yellow(""))) end @@ -226,8 +223,6 @@ M.frames = function(opts) )) end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end diff --git a/lua/fzf-lua/providers/files.lua b/lua/fzf-lua/providers/files.lua index 2a424663..a63d4860 100644 --- a/lua/fzf-lua/providers/files.lua +++ b/lua/fzf-lua/providers/files.lua @@ -31,6 +31,12 @@ local get_files_cmd = function(opts) command = string.format("fd %s", opts.fd_opts) elseif vim.fn.executable("rg") == 1 then command = string.format("rg %s", opts.rg_opts) + elseif utils.__IS_WINDOWS then + -- `dir` command returns absolute paths with ^M for EOL + -- `make_entry.file` will strip the ^M + -- set `opts.cwd` for relative path display + command = "dir /s/b/a:-d" + opts.cwd = opts.cwd or vim.loop.cwd() else POSIX_find_compat(opts.find_opts) command = string.format("find -L . %s", opts.find_opts) @@ -44,7 +50,7 @@ M.files = function(opts) if opts.ignore_current_file then local curbuf = vim.api.nvim_buf_get_name(0) if #curbuf > 0 then - curbuf = path.relative(curbuf, opts.cwd or vim.loop.cwd()) + curbuf = path.relative_to(curbuf, opts.cwd or vim.loop.cwd()) opts.file_ignore_patterns = opts.file_ignore_patterns or {} table.insert(opts.file_ignore_patterns, "^" .. utils.lua_regex_escape(curbuf) .. "$") diff --git a/lua/fzf-lua/providers/git.lua b/lua/fzf-lua/providers/git.lua index 141c331e..789f0a37 100644 --- a/lua/fzf-lua/providers/git.lua +++ b/lua/fzf-lua/providers/git.lua @@ -129,7 +129,7 @@ M.bcommits = function(opts) end local git_root = path.git_root(opts) if not git_root then return end - local file = path.relative(vim.fn.expand("%:p"), git_root) + local file = path.relative_to(vim.fn.expand("%:p"), git_root) local range if utils.mode_is_visual() then local _, sel = utils.get_visual_selection() @@ -157,7 +157,6 @@ end M.branches = function(opts) opts = config.normalize_opts(opts, "git.branches") if not opts then return end - opts.fzf_opts["--no-multi"] = "" if opts.preview then opts.__preview = path.git_cwd(opts.preview, opts) opts.preview = shell.raw_preview_action_cmd(function(items) diff --git a/lua/fzf-lua/providers/grep.lua b/lua/fzf-lua/providers/grep.lua index c604968f..57bd4ec7 100644 --- a/lua/fzf-lua/providers/grep.lua +++ b/lua/fzf-lua/providers/grep.lua @@ -9,7 +9,7 @@ local M = {} ---@param opts table ---@param search_query string ----@param no_esc boolean +---@param no_esc boolean|number ---@return string local get_grep_cmd = function(opts, search_query, no_esc) if opts.raw_cmd and #opts.raw_cmd > 0 then @@ -137,10 +137,6 @@ local function normalize_live_grep_opts(opts) -- we need this for 'actions.grep_lgrep` opts.__ACT_TO = opts.__ACT_TO or M.grep - -- NOT NEEDED SINCE RESUME DATA REFACTOR - -- (was used by `make_entry.set_config_section` - -- opts.__module__ = opts.__module__ or "grep" - -- prepend prompt with "*" to indicate "live" query opts.prompt = type(opts.prompt) == "string" and opts.prompt or "" opts.prompt = opts.prompt:match("^%*") and opts.prompt or ("*" .. opts.prompt) @@ -246,21 +242,6 @@ 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 --[[@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 - -- be able to search single quotes as it will break - -- the escape sequence. So we use a nifty trick - -- * replace the placeholder with {argv1} - -- * re-add the placeholder at the end of the command - -- * preprocess then replace it with vim.fn.argv(1) - -- NOTE: since we cannot guarantee the positional index - -- of arguments (#291), we use the last argument instead - command = command:gsub(core.fzf_query_placeholder, "{argvz}") - -- prefix the query with `--` so we can support `--fixed-strings` (#781) - .. " -- " .. core.fzf_query_placeholder - end -- signal 'fzf_exec' to set 'change:reload' parameters -- or skim's "interactive" mode (AKA "live query") @@ -392,7 +373,7 @@ M.grep_curbuf = function(opts, lgrep) utils.info("Rg current buffer requires file on disk") return else - opts.filename = path.relative(opts.filename, vim.loop.cwd()) + opts.filename = path.relative_to(opts.filename, vim.loop.cwd()) end -- rg globs are meaningless here since we searching a single file opts.rg_glob = false diff --git a/lua/fzf-lua/providers/helptags.lua b/lua/fzf-lua/providers/helptags.lua index 8590b392..2ac23e8d 100644 --- a/lua/fzf-lua/providers/helptags.lua +++ b/lua/fzf-lua/providers/helptags.lua @@ -93,9 +93,6 @@ end M.helptags = function(opts) opts = config.normalize_opts(opts, "helptags") if not opts then return end - - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(fzf_fn, opts) end diff --git a/lua/fzf-lua/providers/lsp.lua b/lua/fzf-lua/providers/lsp.lua index e889d0d9..2dffdb51 100644 --- a/lua/fzf-lua/providers/lsp.lua +++ b/lua/fzf-lua/providers/lsp.lua @@ -98,7 +98,7 @@ local function location_handler(opts, cb, _, result, ctx, _) result = vim.tbl_filter(function(x) local item = vim.lsp.util.locations_to_items({ x }, encoding)[1] table.insert(items, item) - if opts.cwd_only and not path.is_relative(item.filename, opts.cwd) then + if opts.cwd_only and not path.is_relative_to(item.filename, opts.cwd) then return false end return true diff --git a/lua/fzf-lua/providers/manpages.lua b/lua/fzf-lua/providers/manpages.lua index d2a6207e..f4eff026 100644 --- a/lua/fzf-lua/providers/manpages.lua +++ b/lua/fzf-lua/providers/manpages.lua @@ -13,8 +13,6 @@ M.manpages = function(opts) return string.format("%-45s %s", man, desc) end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(opts.cmd, opts) end diff --git a/lua/fzf-lua/providers/module.lua b/lua/fzf-lua/providers/module.lua index 1443db7e..99b11391 100644 --- a/lua/fzf-lua/providers/module.lua +++ b/lua/fzf-lua/providers/module.lua @@ -28,7 +28,6 @@ M.metatable = function(opts) opts.fzf_opts["--preview"] = prev_act opts.fzf_opts["--preview-window"] = "hidden:down:10" - opts.fzf_opts["--no-multi"] = "" -- builtin is excluded from global resume -- as the behavior might confuse users (#267) diff --git a/lua/fzf-lua/providers/nvim.lua b/lua/fzf-lua/providers/nvim.lua index 5391ccb3..26319dbe 100644 --- a/lua/fzf-lua/providers/nvim.lua +++ b/lua/fzf-lua/providers/nvim.lua @@ -1,6 +1,7 @@ local core = require "fzf-lua.core" local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" +local libuv = require "fzf-lua.libuv" local shell = require "fzf-lua.shell" local config = require "fzf-lua.config" local make_entry = require "fzf-lua.make_entry" @@ -58,7 +59,6 @@ M.commands = function(opts) table.sort(entries, function(a, b) return a < b end) end - opts.fzf_opts["--no-multi"] = "" opts.fzf_opts["--preview"] = prev_act core.fzf_exec(entries, opts) @@ -78,16 +78,13 @@ local history = function(opts, str) string.sub(item, finish + 1)) end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end local arg_header = function(sel_key, edit_key, text) sel_key = utils.ansi_codes.yellow(sel_key) edit_key = utils.ansi_codes.yellow(edit_key) - return vim.fn.shellescape((":: %s to %s, %s to edit") - :format(sel_key, text, edit_key)) + return libuv.shellescape((":: %s to %s, %s to edit"):format(sel_key, text, edit_key)) end M.command_history = function(opts) @@ -133,7 +130,6 @@ M.jumps = function(opts) table.insert(entries, 1, string.format("%6s %s %s %s", opts.h1 or "jump", "line", "col", "file/text")) - opts.fzf_opts["--no-multi"] = "" opts.fzf_opts["--header-lines"] = "1" core.fzf_exec(entries, opts) @@ -166,8 +162,7 @@ M.tagstack = function(opts) local entries = {} for i, tag in ipairs(tags) do - local bufname = path.HOME_to_tilde( - path.relative(tag.filename, vim.loop.cwd())) + local bufname = path.HOME_to_tilde(path.relative_to(tag.filename, vim.loop.cwd())) local buficon, hl if opts.file_icons then local filename = path.tail(bufname) @@ -192,8 +187,6 @@ M.tagstack = function(opts) tag.text)) end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end @@ -224,7 +217,7 @@ M.marks = function(opts) for i = #marks, 3, -1 do local mark, line, col, text = marks[i]:match("(.)%s+(%d+)%s+(%d+)%s+(.*)") col = tostring(tonumber(col) + 1) - if path.starts_with_separator(text) then + if path.is_absolute(text) then text = path.HOME_to_tilde(text) end if not filter or vim.tbl_contains(filter, mark) then @@ -241,7 +234,6 @@ M.marks = function(opts) string.format("%-5s %s %s %s", "mark", "line", "col", "file/text")) -- opts.fzf_opts['--preview'] = prev_act - opts.fzf_opts["--no-multi"] = "" opts.fzf_opts["--header-lines"] = "1" core.fzf_exec(entries, opts) @@ -294,7 +286,6 @@ M.registers = function(opts) end end - opts.fzf_opts["--no-multi"] = "" opts.fzf_opts["--preview"] = prev_act core.fzf_exec(entries, opts) @@ -360,7 +351,6 @@ M.keymaps = function(opts) table.insert(entries, v.str) end - opts.fzf_opts["--no-multi"] = "" opts.fzf_opts["--header-lines"] = "1" -- sort alphabetically @@ -382,8 +372,6 @@ M.spell_suggest = function(opts) if vim.tbl_isempty(entries) then return end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end @@ -394,8 +382,6 @@ M.filetypes = function(opts) local entries = vim.fn.getcompletion("", "filetype") if vim.tbl_isempty(entries) then return end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end @@ -407,8 +393,6 @@ M.packadd = function(opts) if vim.tbl_isempty(entries) then return end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end @@ -441,8 +425,6 @@ M.menus = function(opts) return end - opts.fzf_opts["--no-multi"] = "" - core.fzf_exec(entries, opts) end diff --git a/lua/fzf-lua/providers/tags.lua b/lua/fzf-lua/providers/tags.lua index edda3b38..96991f05 100644 --- a/lua/fzf-lua/providers/tags.lua +++ b/lua/fzf-lua/providers/tags.lua @@ -19,7 +19,7 @@ local function get_tags_cmd(opts) if opts.filename and #opts.filename > 0 then -- tags use relative paths, by now we should -- have the correct cwd from `get_ctags_cwd` - query = libuv.shellescape(path.relative(opts.filename, opts.cwd or vim.loop.cwd())) + query = libuv.shellescape(path.relative_to(opts.filename, opts.cwd or vim.loop.cwd())) elseif opts.search and #opts.search > 0 then filter = ([[%s -v "^!"]]):format(bin) query = libuv.shellescape(opts.no_esc and opts.search or @@ -29,7 +29,7 @@ local function get_tags_cmd(opts) end return ("%s %s %s %s"):format( bin, flags, query, - opts._ctags_file and vim.fn.shellescape(opts._ctags_file) or "" + opts._ctags_file and libuv.shellescape(opts._ctags_file) or "" ), filter end @@ -76,7 +76,7 @@ local function tags(opts) -- tags file should always resolve to an absolute path, already "expanded" by -- `get_ctags_file` we take care of the case where `opts.ctags_file = "tags"` - if not path.starts_with_separator(opts._ctags_file) then + if not path.is_absolute(opts._ctags_file) then opts._ctags_file = path.join({ opts.cwd or vim.loop.cwd(), opts.ctags_file }) end diff --git a/lua/fzf-lua/providers/tmux.lua b/lua/fzf-lua/providers/tmux.lua index 942c4573..16f605f5 100644 --- a/lua/fzf-lua/providers/tmux.lua +++ b/lua/fzf-lua/providers/tmux.lua @@ -14,7 +14,6 @@ M.buffers = function(opts) return string.format("[%s] %s", utils.ansi_codes.yellow(buf), data) end - opts.fzf_opts["--no-multi"] = "" opts.fzf_opts["--delimiter"] = "'[:]'" opts.fzf_opts["--preview"] = shell.preview_action_cmd(function(items) local buf = items[1]:match("^%[(.-)%]") diff --git a/lua/fzf-lua/shell.lua b/lua/fzf-lua/shell.lua index 697705b7..7c008c83 100644 --- a/lua/fzf-lua/shell.lua +++ b/lua/fzf-lua/shell.lua @@ -80,11 +80,10 @@ function M.raw_async_action(fn, fzf_field_expression, debug) -- this is for windows WSL and AppImage users, their nvim path isn't just -- '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(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 nvim_runtime = os.getenv("FZF_LUA_NVIM_BIN") and "" or string.format( + utils._if_win([[set VIMRUNTIME=%s& ]], "VIMRUNTIME=%s "), + utils._if_win(path.normalize(vim.env.VIMRUNTIME), + 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 "") @@ -96,7 +95,7 @@ function M.raw_async_action(fn, fzf_field_expression, debug) -- worktrees (#600) local action_cmd = ("%s%s -n --headless --clean --cmd %s -- %s"):format( nvim_runtime, - libuv.shellescape(nvim_bin), + libuv.shellescape(path.normalize(nvim_bin)), libuv.shellescape(("lua loadfile([[%s]])().rpc_nvim_exec_lua({%s})") :format(path.join { vim.g.fzf_lua_directory, "shell_helper.lua" }, call_args)), fzf_field_expression) @@ -106,7 +105,7 @@ end function M.async_action(fn, fzf_field_expression, debug) local action_string, id = M.raw_async_action(fn, fzf_field_expression, debug) - return vim.fn.shellescape(action_string), id + return libuv.shellescape(action_string), id end function M.raw_action(fn, fzf_field_expression, debug) @@ -140,12 +139,12 @@ end function M.action(fn, fzf_field_expression, debug) local action_string, id = M.raw_action(fn, fzf_field_expression, debug) - return vim.fn.shellescape(action_string), id + return libuv.shellescape(action_string), id end M.preview_action_cmd = function(fn, fzf_field_expression, debug) local action_string, id = M.raw_preview_action_cmd(fn, fzf_field_expression, debug) - return vim.fn.shellescape(action_string), id + return libuv.shellescape(action_string), id end M.raw_preview_action_cmd = function(fn, fzf_field_expression, debug) @@ -168,12 +167,17 @@ M.raw_preview_action_cmd = function(fn, fzf_field_expression, debug) libuv.process_kill(M.__pid_preview) M.__pid_preview = nil - return libuv.spawn({ - cmd = fn(...), + local opts = fn(...) + if type(opts) == "string" then + --backward compat + opts = { cmd = opts } + end + + return libuv.spawn(vim.tbl_extend("force", opts, { cb_finish = on_finish, cb_write = on_write, cb_pid = function(pid) M.__pid_preview = pid end, - }, false) + })) end, fzf_field_expression, debug) end diff --git a/lua/fzf-lua/shell_helper.lua b/lua/fzf-lua/shell_helper.lua index f7759ade..d23bf764 100644 --- a/lua/fzf-lua/shell_helper.lua +++ b/lua/fzf-lua/shell_helper.lua @@ -2,7 +2,7 @@ -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/action_helper.lua local uv = vim.loop -local is_windows = vim.fn.has("win32") == 1 +local is_windows = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 ---@return string local function windows_pipename() @@ -78,6 +78,9 @@ local function rpc_nvim_exec_lua(opts) for i = 1, vim.fn.argc() do io.stderr:write(("[DEBUG]\targ[%d] = %s\n"):format(i, vim.fn.argv(i - 1))) end + for _, var in ipairs({ "LINES", "COLUMNS" }) do + io.stderr:write(("[DEBUG]\t$%s = %s\n"):format(var, os.getenv(var) or "")) + end end if not success then diff --git a/lua/fzf-lua/utils.lua b/lua/fzf-lua/utils.lua index 65efc5b8..a7a0408f 100644 --- a/lua/fzf-lua/utils.lua +++ b/lua/fzf-lua/utils.lua @@ -12,7 +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 +M.__IS_WINDOWS = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 -- limit devicons support to nvim >=0.8, although official support is >=0.7 @@ -79,6 +79,27 @@ M._if = function(bool, a, b) end end +M._if_win = function(a, b) + if M.__IS_WINDOWS then + return a + else + return b + end +end + +---@param vars table +---@return table +M.shell_setenv_str = function(vars) + local ret = {} + for k, v in pairs(vars or {}) do + table.insert(ret, M._if_win( + string.format([[set %s=%s&&]], tostring(k), tostring(v)), + string.format("%s=%s;", tostring(k), tostring(v)) + )) + end + return ret +end + ---@param inputstr string ---@param sep string ---@return string[] @@ -186,7 +207,7 @@ end function M.glob_escape(str) if not str then return str end - return str:gsub("[\\%{}[%]]", function(x) + return str:gsub("[%{}[%]]", function(x) return [[\]] .. x end) end @@ -326,6 +347,7 @@ function M.tbl_isempty(T) end function M.tbl_extend(t1, t2) + ---@diagnostic disable-next-line: deprecated return table.move(t2, 1, #t2, #t1 + 1, t1) end @@ -638,7 +660,7 @@ function M.setup_devicon_term_hls() end ---@param fname string ----@param name string +---@param name string|nil ---@param silent boolean function M.load_profile(fname, name, silent) local profile = name or fname:match("([^%p]+)%.lua$") or "" diff --git a/lua/fzf-lua/win.lua b/lua/fzf-lua/win.lua index cafe6fa1..3f7dff06 100644 --- a/lua/fzf-lua/win.lua +++ b/lua/fzf-lua/win.lua @@ -344,6 +344,7 @@ local function opt_matches(opts, key, str) return opt and opt:match(str) end +---@alias FzfWin table ---@param o table ---@return FzfWin function FzfWin:new(o) @@ -968,7 +969,7 @@ function FzfWin.win_leave() self._previewer:win_leave() end if not self or self.closing then return end - _self:close() + self:close() end function FzfWin:update_scrollbar_border(o) diff --git a/tests/init_spec.lua b/tests/init_spec.lua index 008c3355..5588ed4d 100644 --- a/tests/init_spec.lua +++ b/tests/init_spec.lua @@ -7,7 +7,6 @@ describe("FzfLua", function() describe("configuration", function() it("initial setup", function() fzf.setup({}) - _G.dump(assert.is) assert.is.truthy(fzf) end) end) diff --git a/tests/libuv_spec.lua b/tests/libuv_spec.lua new file mode 100644 index 00000000..1cfe403b --- /dev/null +++ b/tests/libuv_spec.lua @@ -0,0 +1,78 @@ +local libuv = require("fzf-lua.libuv") + + +describe("Testing libuv module", function() + it("shellescape (win bslash)", function() + assert.are.same(libuv.shellescape([[]], 1), [[""]]) + assert.are.same(libuv.shellescape([[^]], 1), [["^"]]) + assert.are.same(libuv.shellescape([[""]], 1), [["\"\""]]) + assert.are.same(libuv.shellescape([["^"]], 1), [["\"^\""]]) + assert.are.same(libuv.shellescape([[foo]], 1), [["foo"]]) + assert.are.same(libuv.shellescape([["foo"]], 1), [["\"foo\""]]) + assert.are.same(libuv.shellescape([["foo"bar"]], 1), [["\"foo\"bar\""]]) + assert.are.same(libuv.shellescape([[foo"bar]], 1), [["foo\"bar"]]) + assert.are.same(libuv.shellescape([[foo""bar]], 1), [["foo\"\"bar"]]) + assert.are.same(libuv.shellescape([["foo\"bar"]], 1), [["\"foo\\\"bar\""]]) + assert.are.same(libuv.shellescape([[foo\]], 1), [["foo\\"]]) + assert.are.same(libuv.shellescape([[foo\\]], 1), [["foo\\\\"]]) + assert.are.same(libuv.shellescape([[foo\^]], 1), [["foo\^"]]) + assert.are.same(libuv.shellescape([[foo\\\\]], 1), [["foo\\\\\\\\"]]) + assert.are.same(libuv.shellescape([[foo\"]], 1), [["foo\\\""]]) + assert.are.same(libuv.shellescape([["foo\"]], 1), [["\"foo\\\""]]) + assert.are.same(libuv.shellescape([["foo\""]], 1), [["\"foo\\\"\""]]) + assert.are.same(libuv.shellescape([[foo\bar]], 1), [["foo\bar"]]) + assert.are.same(libuv.shellescape([[foo\\bar]], 1), [["foo\\bar"]]) + assert.are.same(libuv.shellescape([[foo\\"bar]], 1), [["foo\\\\\"bar"]]) + assert.are.same(libuv.shellescape([[foo\\\"bar]], 1), [["foo\\\\\\\"bar"]]) + end) + + it("shellescape (win caret)", function() + assert.are.same(libuv.shellescape([[]], 2), [[^"^"]]) + assert.are.same(libuv.shellescape([["]], 2), [[^"\^"^"]]) + assert.are.same(libuv.shellescape([[^"]], 2), [[^"^^\^"^"]]) + assert.are.same(libuv.shellescape([[\"]], 2), [[^"\\\^"^"]]) + assert.are.same(libuv.shellescape([[\^"]], 2), [[^"^^\\\^"^"]]) + assert.are.same(libuv.shellescape([[^"^"]], 2), [[^"^^\^"^^\^"^"]]) + assert.are.same(libuv.shellescape([[__^^"^"__]], 2), [[^"__^^^^\^"^^\^"__^"]]) + assert.are.same(libuv.shellescape([[__!^^"^"__]], 2), + -- 1st: ^"_^!^^^^\^"^^\^"_^" + -- 2nd: ^"_^^^!^^^^^^^^^^\\\^"^^^^^^\\\^"_^" + [[^"__^^^!^^^^^^^^^^\\\^"^^^^^^\\\^"__^"]]) + assert.are.same(libuv.shellescape([[__^^^^\^"^^\^"__]], 2), + [[^"__^^^^^^^^^^\\\^"^^^^^^\\\^"__^"]]) + assert.are.same(libuv.shellescape([[^]], 2), [[^"^^^"]]) + assert.are.same(libuv.shellescape([[^^]], 2), [[^"^^^^^"]]) + assert.are.same(libuv.shellescape([[^^^]], 2), [[^"^^^^^^^"]]) + assert.are.same(libuv.shellescape([[^!^]], 2), [[^"^^^^^^^!^^^^^"]]) + assert.are.same(libuv.shellescape([[!^"]], 2), + -- 1st inner: ^!^^\^" + -- 2nd inner: ^^^!^^^^^^\\\^" + [[^"^^^!^^^^^^\\\^"^"]]) + assert.are.same(libuv.shellescape([[!\"]], 2), [[^"^^^!^^\\\\\\\^"^"]]) + assert.are.same(libuv.shellescape([[!\^"]], 2), [[^"^^^!^^^^^^\\\\\\\^"^"]]) + assert.are.same(libuv.shellescape([[()%^"<>&|]], 2), [[^"^(^)^%^^\^"^<^>^&^|^"]]) + assert.are.same(libuv.shellescape([[()%^"<>&|!]], 2), + -- 1st inner: ^(^)^%^^\^"^<^>^&^|^! + -- 2nd inner: ^^^(^^^)^^^%^^^^^^\^"^^^<^^^>^^^&^^^|^^^! + [[^"^^^(^^^)^^^%^^^^^^\\\^"^^^<^^^>^^^&^^^|^^^!^"]]) + assert.are.same(libuv.shellescape([[foo]], 2), [[^"foo^"]]) + assert.are.same(libuv.shellescape([[foo\]], 2), [[^"foo\\^"]]) + assert.are.same(libuv.shellescape([[foo^]], 2), [[^"foo^^^"]]) + assert.are.same(libuv.shellescape([[foo\\]], 2), [[^"foo\\\\^"]]) + assert.are.same(libuv.shellescape([[foo\\\]], 2), [[^"foo\\\\\\^"]]) + assert.are.same(libuv.shellescape([[foo\\\\]], 2), [[^"foo\\\\\\\\^"]]) + assert.are.same(libuv.shellescape([[f!oo]], 2), [[^"f^^^!oo^"]]) + assert.are.same(libuv.shellescape([[^"foo^"]], 2), [[^"^^\^"foo^^\^"^"]]) + assert.are.same(libuv.shellescape([[\^"foo\^"]], 2), [[^"^^\\\^"foo^^\\\^"^"]]) + assert.are.same(libuv.shellescape([[foo""bar]], 2), [[^"foo\^"\^"bar^"]]) + assert.are.same(libuv.shellescape([[foo^"^"bar]], 2), [[^"foo^^\^"^^\^"bar^"]]) + assert.are.same(libuv.shellescape([["foo\"bar"]], 2), [[^"\^"foo\\\^"bar\^"^"]]) + assert.are.same(libuv.shellescape([[foo\^"]], 2), [[^"foo^^\\\^"^"]]) + assert.are.same(libuv.shellescape([[foo\"]], 2), [[^"foo\\\^"^"]]) + assert.are.same(libuv.shellescape([[^"foo\^"^"]], 2), [[^"^^\^"foo^^\\\^"^^\^"^"]]) + assert.are.same(libuv.shellescape([[foo\"bar]], 2), [[^"foo\\\^"bar^"]]) + assert.are.same(libuv.shellescape([[foo\\"bar]], 2), [[^"foo\\\\\^"bar^"]]) + assert.are.same(libuv.shellescape([[foo\\^^"bar]], 2), [[^"foo\\^^^^\^"bar^"]]) + assert.are.same(libuv.shellescape([[foo\\\^^^"]], 2), [[^"foo\\\^^^^^^\^"^"]]) + end) +end) diff --git a/tests/path_spec.lua b/tests/path_spec.lua new file mode 100644 index 00000000..14102213 --- /dev/null +++ b/tests/path_spec.lua @@ -0,0 +1,323 @@ +local fzf = require("fzf-lua") +local path = fzf.path +local utils = fzf.utils + +describe("Testing path module", function() + it("separator", function() + utils.__IS_WINDOWS = false + assert.are.same(path.separator(), "/") + assert.are.same(path.separator(""), "/") + assert.are.same(path.separator("~/foo"), "/") + assert.are.same(path.separator([[~\foo]]), "/") + assert.are.same(path.separator([[c:\foo]]), "/") + + utils.__IS_WINDOWS = true + assert.are.same(path.separator(), [[\]]) + assert.are.same(path.separator(""), [[\]]) + assert.are.same(path.separator("~/foo"), "/") + assert.are.same(path.separator([[~\foo]]), [[\]]) + assert.are.same(path.separator([[c:\foo]]), [[\]]) + assert.are.same(path.separator([[foo\bar]]), [[\]]) + end) + + it("Ends with separator", function() + utils.__IS_WINDOWS = false + assert.is.False(path.ends_with_separator("")) + assert.is.True(path.ends_with_separator("/")) + assert.is.False(path.ends_with_separator([[\]])) + assert.is.False(path.ends_with_separator("/some/path")) + assert.is.True(path.ends_with_separator("/some/path/")) + + utils.__IS_WINDOWS = true + assert.is.False(path.ends_with_separator("")) + assert.is.True(path.ends_with_separator("/")) + assert.is.True(path.ends_with_separator([[\]])) + assert.is.False(path.ends_with_separator("/some/path")) + assert.is.True(path.ends_with_separator("/some/path/")) + assert.is.False(path.ends_with_separator([[c:\some\path]])) + assert.is.True(path.ends_with_separator([[c:\some\path\]])) + end) + + it("Add trailing separator", function() + utils.__IS_WINDOWS = false + assert.are.equal(path.add_trailing(""), "/") + assert.are.equal(path.add_trailing("/"), "/") + assert.are.equal(path.add_trailing("/some"), "/some/") + assert.are.equal(path.add_trailing("/some/"), "/some/") + utils.__IS_WINDOWS = true + assert.are.equal(path.add_trailing(""), [[\]]) + assert.are.equal(path.add_trailing("/"), [[/]]) + assert.are.equal(path.add_trailing("/some"), [[/some\]]) + assert.are.equal(path.add_trailing("/some/"), [[/some/]]) + assert.are.equal(path.add_trailing([[C:\some\]]), [[C:\some\]]) + assert.are.equal(path.add_trailing([[C:\some]]), [[C:\some\]]) + assert.are.equal(path.add_trailing([[C:/some]]), [[C:/some/]]) + assert.are.equal(path.add_trailing([[~/some]]), [[~/some/]]) + assert.are.equal(path.add_trailing([[some\path]]), [[some\path\]]) + end) + + it("Remove trailing separator", function() + utils.__IS_WINDOWS = false + assert.are.equal(path.remove_trailing(""), "") + assert.are.equal(path.remove_trailing("/"), "") + assert.are.equal(path.remove_trailing("//"), "") + assert.are.equal(path.remove_trailing("/some"), "/some") + assert.are.equal(path.remove_trailing("/some/"), "/some") + assert.are.equal(path.remove_trailing("/some/////"), "/some") + assert.are.equal(path.remove_trailing([[/some\]]), [[/some\]]) + utils.__IS_WINDOWS = true + assert.are.equal(path.remove_trailing(""), "") + assert.are.equal(path.remove_trailing("/"), "") + assert.are.equal(path.remove_trailing("//"), "") + assert.are.equal(path.remove_trailing("/some"), "/some") + assert.are.equal(path.remove_trailing("/some/"), "/some") + assert.are.equal(path.remove_trailing("/some/////"), "/some") + assert.are.equal(path.remove_trailing([[/some\]]), [[/some]]) + assert.are.equal(path.remove_trailing([[C:\some\]]), [[C:\some]]) + assert.are.equal(path.remove_trailing([[C:\some\\\\//]]), [[C:\some]]) + end) + + it("Is absolute", function() + utils.__IS_WINDOWS = false + assert.is.False(path.is_absolute("")) + assert.is.True(path.is_absolute("/")) + assert.is.False(path.is_absolute([[\]])) + assert.is.True(path.is_absolute("/some/path")) + assert.is.False(path.is_absolute("./some/path/")) + assert.is.False(path.is_absolute([[c:\some\path\]])) + + utils.__IS_WINDOWS = true + assert.is.False(path.is_absolute("")) + assert.is.False(path.is_absolute("/")) + assert.is.False(path.is_absolute([[\]])) + assert.is.False(path.is_absolute("/some/path")) + assert.is.False(path.is_absolute("./some/path/")) + assert.is.False(path.is_absolute([[.\some\path/]])) + assert.is.True(path.is_absolute([[c:\some\path]])) + assert.is.True(path.is_absolute([[C:\some\path\]])) + end) + + it("Has cwd prefix", function() + utils.__IS_WINDOWS = false + assert.is.False(path.has_cwd_prefix("")) + assert.is.True(path.has_cwd_prefix("./")) + assert.is.False(path.has_cwd_prefix([[.\]])) + assert.is.False(path.has_cwd_prefix("/some/path")) + assert.is.True(path.has_cwd_prefix("./some/path/")) + + utils.__IS_WINDOWS = true + assert.is.False(path.has_cwd_prefix("")) + assert.is.True(path.has_cwd_prefix("./")) + assert.is.True(path.has_cwd_prefix([[.\]])) + assert.is.False(path.has_cwd_prefix("/some/path")) + assert.is.True(path.has_cwd_prefix("./some/path/")) + assert.is.True(path.has_cwd_prefix([[.\some\path/]])) + assert.is.False(path.has_cwd_prefix([[c:\some\path]])) + assert.is.False(path.has_cwd_prefix([[c:\some\path\]])) + assert.is.False(path.has_cwd_prefix([[c:\some/path\]])) + end) + + it("Strip cwd prefix", function() + utils.__IS_WINDOWS = false + assert.are.equal(path.strip_cwd_prefix(""), "") + assert.are.equal(path.strip_cwd_prefix("./"), "") + assert.are.equal(path.strip_cwd_prefix([[.\]]), [[.\]]) + assert.are.equal(path.strip_cwd_prefix("/some/path"), "/some/path") + assert.are.equal(path.strip_cwd_prefix("./some/path/"), "some/path/") + + utils.__IS_WINDOWS = true + assert.are.equal(path.strip_cwd_prefix(""), "") + assert.are.equal(path.strip_cwd_prefix("./"), "") + assert.are.equal(path.strip_cwd_prefix([[.\]]), "") + assert.are.equal(path.strip_cwd_prefix("/some/path"), "/some/path") + assert.are.equal(path.strip_cwd_prefix("./some/path/"), "some/path/") + assert.are.equal(path.strip_cwd_prefix([[.\some\path/]]), [[some\path/]]) + assert.are.equal(path.strip_cwd_prefix([[c:\some\path]]), [[c:\some\path]]) + end) + + it("Tail", function() + utils.__IS_WINDOWS = false + assert.are.equal(path.tail(""), "") + assert.are.equal(path.tail("/"), "/") + assert.are.equal(path.tail("/foo"), "foo") + assert.are.equal(path.tail("/foo/"), "foo/") + assert.are.equal(path.tail("/foo/bar"), "bar") + assert.are.equal(path.tail([[/foo\bar]]), [[foo\bar]]) + + utils.__IS_WINDOWS = true + assert.are.equal(path.tail(""), "") + assert.are.equal(path.tail("/"), "/") + assert.are.equal(path.tail([[\]]), [[\]]) + assert.are.equal(path.tail([[c:\foo]]), "foo") + assert.are.equal(path.tail([[c:\foo\]]), [[foo\]]) + assert.are.equal(path.tail([[c:\foo\bar]]), "bar") + assert.are.equal(path.tail([[c:/foo\bar]]), "bar") + assert.are.equal(path.tail([[c:/foo//\//bar]]), "bar") + end) + + it("Parent", function() + utils.__IS_WINDOWS = false + assert.are.equal(path.parent(""), nil) + assert.are.equal(path.parent("/"), nil) + assert.are.equal(path.parent("/foo"), "/") + assert.are.equal(path.parent("/foo/bar"), "/foo/") + assert.are.equal(path.parent("/foo/bar", true), "/foo") + assert.are.equal(path.parent([[/foo\bar]]), [[/]]) + + utils.__IS_WINDOWS = true + assert.are.equal(path.parent(""), nil) + assert.are.equal(path.parent("/"), nil) + assert.are.equal(path.parent([[\]]), nil) + assert.are.equal(path.parent([[c:]]), nil) + assert.are.equal(path.parent([[c:\foo]]), [[c:\]]) + assert.are.equal(path.parent([[c:\foo]], true), [[c:]]) + assert.are.equal(path.parent([[c:\foo\bar]]), [[c:\foo\]]) + assert.are.equal(path.parent([[c:\foo/bar]]), [[c:\foo/]]) + assert.are.equal(path.parent([[c:/foo//\//bar]], true), [[c:/foo]]) + end) + + it("Normalize", function() + utils.__IS_WINDOWS = false + assert.are.equal(path.normalize("/some/path"), "/some/path") + assert.are.equal(path.normalize([[\some\path]]), [[\some\path]]) + assert.are.equal(path.normalize("~/some/path"), path.HOME() .. "/some/path") + utils.__IS_WINDOWS = true + assert.are.equal(path.normalize("/some/path"), "/some/path") + assert.are.equal(path.normalize([[\some\path]]), "/some/path") + assert.are.equal(path.normalize("~/some/path"), path.normalize(path.HOME()) .. "/some/path") + end) + + it("Equals", function() + utils.__IS_WINDOWS = false + assert.is.False(path.equals("/some/path", "/some/path/foo")) + assert.is.True(path.equals("/some/path", "/some/path/")) + assert.is.False(path.equals("/some/Path", "/some/path")) + assert.is.True(path.equals("~/some/path", path.HOME() .. "/some/path")) + assert.is.False(path.equals([[/some\\path]], "/some/path/")) + utils.__IS_WINDOWS = true + assert.is.False(path.equals("/some/path", "/some/path/foo")) + assert.is.True(path.equals("/some/path", "/some/path/")) + assert.is.True(path.equals("/some/PATH", "/some/path")) + assert.is.True(path.equals("~/some/path", path.HOME() .. "/some/path")) + assert.is.True(path.equals([[/some\path\\]], "/some/path/")) + end) + + it("Is relative to", function() + -- Testing both `path.is_relative_to` and `path.relative_to` + -- [1]: path + -- [2]: relative_to + -- [3]: expected result (bool, relative_path) + local unix = { + { "/some/path", "/some/path", { true, "." } }, + { "/some/path", "/some/path//", { true, "." } }, + { "/some/path//", "/some/path", { true, "." } }, + { "/some", "/somepath", { false, nil } }, + { "some", "somepath", { false, nil } }, + { "some", "some/path", { false, nil } }, + { "some/path", "some", { true, "path" } }, + { "some/path/", "some", { true, "path/" } }, + { "some/path//", "some", { true, "path//" } }, + { "some/path/", [[some\]], { false, nil } }, + { [[some\path]], "some", { false, nil } }, + { "/some/path/to", "/some///", { true, "path/to" } }, + { "/SOME/PATH", "/some", { false, nil } }, + { "a///b//////c", "a//b", { true, "c" } }, + { "~", path.HOME(), { true, "." } }, + { "~/a/b/c", "~/a", { true, "b/c" } }, + { "~//a/b/c", path.HOME() .. "/a/b", { true, "c" } }, + { path.HOME() .. "/a/b", "~/a/", { true, "b" } }, + } + local win = { + { [[\some\path]], [[\some/path]], { true, "." } }, + { [[/some/path]], [[/some/path\/\/]], { true, "." } }, + { "/some/path/", "/some/path", { true, "." } }, + { [[\some]], [[\somepath]], { false, nil } }, + { "some/path", "some", { true, "path" } }, + { [[some\path\]], [[some\/\]], { true, [[path\]] } }, + { [[c:\some\path]], [[c:\some]], { true, [[path]] } }, + { [[c:\some\path\]], [[c:\some]], { true, [[path\]] } }, + { [[c:\some\path\\]], [[c:\some]], { true, [[path\\]] } }, + { [[c:\some\path\]], [[c:\some\\///]], { true, [[path\]] } }, + { [[c:\SOME\PATH]], [[C:\some/\/\]], { true, [[PATH]] } }, + { [[~\SOME\PATH\to]], [[~\some/]], { true, [[PATH\to]] } }, + { [[~\SOME\PATH/to]], [[~\some]], { true, [[PATH/to]] } }, + { [[C:\a/\/b///\\\c]], [[c:\/\a\/b]], { true, "c" } }, + { [[C:\a/b\c\d\e]], [[c:\A\/b\]], { true, [[c\d\e]] } }, + } + utils.__IS_WINDOWS = false + for _, v in ipairs(unix) do + assert.are.same({ path.is_relative_to(v[1], v[2]) }, v[3], + string.format('\n\nis_relative_to("%s", "%s") ~= "%s"\n', v[1], v[2], v[3][2])) + end + utils.__IS_WINDOWS = true + for _, v in ipairs(win) do + assert.are.same({ path.is_relative_to(v[1], v[2]) }, v[3], + string.format('\n\nis_relative_to("%s", "%s") ~= "%s"\n', v[1], v[2], v[3][2])) + end + end) + + it("Extension", function() + utils.__IS_WINDOWS = false + assert.is.same(path.extension("/some/path/foobar"), "") + assert.is.same(path.extension("/some/path/foo.bar"), "bar") + assert.is.same(path.extension("/some/path/foo.bar.baz"), "baz") + end) + + it("Join", function() + utils.__IS_WINDOWS = false + assert.is.same(path.join({ "some" }), "some") + assert.is.same(path.join({ nil, "path" }), "path") + assert.is.same(path.join({ "/some", "path" }), "/some/path") + assert.is.same(path.join({ "/some", "path/" }), "/some/path/") + assert.is.same(path.join({ "/some/", "path" }), "/some/path") + assert.is.same(path.join({ "/some//", "path" }), "/some//path") + assert.is.same(path.join({ "~/some/", "path" }), "~/some/path") + assert.is.same(path.join({ [[~\some\]], "path" }), [[~\some\/path]]) + assert.is.same(path.join({ [[c:\some\]], "path" }), [[c:\some\/path]]) + utils.__IS_WINDOWS = true + assert.is.same(path.join({ "some" }), "some") + assert.is.same(path.join({ nil, "path" }), "path") + assert.is.same(path.join({ "some", "path" }), [[some\path]]) + assert.is.same(path.join({ "some", [[path\]] }), [[some\path\]]) + assert.is.same(path.join({ "c:/some", "path" }), "c:/some/path") + assert.is.same(path.join({ [[c:\some\]], "path" }), [[c:\some\path]]) + assert.is.same(path.join({ [[c:\some\]], "path", "foo", "bar" }), [[c:\some\path\foo\bar]]) + assert.is.same(path.join({ "~/some/", "path" }), "~/some/path") + assert.is.same(path.join({ [[~\some\]], "path" }), [[~\some\path]]) + end) + + it("Shorten", function() + utils.__IS_WINDOWS = false + assert.are.equal(path.shorten(""), "") + assert.are.equal(path.shorten("/"), "/") + assert.are.equal(path.shorten("/foo"), "/foo") + assert.are.equal(path.shorten("/foo/"), "/f/") + assert.are.equal(path.shorten("/foo/bar"), "/f/bar") + assert.are.equal(path.shorten("/foo/bar/baz"), "/f/b/baz") + assert.are.equal(path.shorten("~/foo/bar/baz"), "~/f/b/baz") + assert.are.equal(path.shorten("~/foo/bar/baz/"), "~/f/b/b/") + assert.are.equal(path.shorten("~/foo/bar/baz//"), "~/f/b/b//") + assert.are.equal(path.shorten("~/.foo/.bar/baz"), "~/.f/.b/baz") + assert.are.equal(path.shorten("~/foo/bar/baz", 2), "~/fo/ba/baz") + assert.are.equal(path.shorten("~/fo/barrr/baz", 3), "~/fo/bar/baz") + assert.are.equal(path.shorten([[/foo\bar]]), [[/foo\bar]]) + + utils.__IS_WINDOWS = true + assert.are.equal(path.shorten("/foo"), [[\foo]]) + assert.are.equal(path.shorten([[/foo\bar]]), [[\f\bar]]) + assert.are.equal(path.shorten([[/foo/bar]]), [[\f\bar]]) + assert.are.equal(path.shorten([[/foo/bar\baz]]), [[\f\b\baz]]) + assert.are.equal(path.shorten([[\]]), [[\]]) + assert.are.equal(path.shorten([[c:/]]), [[c:/]]) + assert.are.equal(path.shorten([[c:\]]), [[c:\]]) + assert.are.equal(path.shorten([[c:\foo]]), [[c:\foo]]) + assert.are.equal(path.shorten([[c:\foo\bar]]), [[c:\f\bar]]) + assert.are.equal(path.shorten([[c:\foo\bar\baz]]), [[c:\f\b\baz]]) + assert.are.equal(path.shorten([[c:\.foo\.bar\baz]]), [[c:\.f\.b\baz]]) + assert.are.equal(path.shorten([[c:/foo\bar]]), [[c:/f/bar]]) + assert.are.equal(path.shorten([[~/foo/bar]]), [[~/f/bar]]) + assert.are.equal(path.shorten([[~\foo\bar]]), [[~\f\bar]]) + assert.are.equal(path.shorten([[~\foo/bar]]), [[~\f\bar]]) + assert.are.equal(path.shorten([[~\foo\bar\]]), [[~\f\b\]]) + end) +end)