From 24980df770eabce50158846d2bc29f5eb76a6853 Mon Sep 17 00:00:00 2001 From: bhagwan Date: Fri, 10 Jan 2025 20:20:11 -0800 Subject: [PATCH] feat(profiles): added `hide` (improved resume, #1686) The profile changes the default binds to hide the fzf-lua window instead of aborting it, therefore keeping cursor position, selection, etc. Note that this also continues long running operations in the background so be mindful of sending fzf to background on a large mono repo, can still be aborted with `ctrl-c` or `alt-esc`. Enable with: ```lua -- Set as base profile :lua require("fzf-lua").setup({"hide"}) -- More than one profile :lua require("fzf-lua").setup({"border-fused","hide"}) -- Or with `profiles`: :FzfLua profiles load=hide -- More than one profile :FzfLua profiles load={"border-fused","hide"} ``` --- README.md | 22 +++++------ lua/fzf-lua/config.lua | 4 ++ lua/fzf-lua/core.lua | 54 ++++++++++++++++++++------ lua/fzf-lua/defaults.lua | 2 +- lua/fzf-lua/init.lua | 11 +++--- lua/fzf-lua/profiles/README.md | 1 + lua/fzf-lua/profiles/hide.lua | 51 ++++++++++++++++++++++++ lua/fzf-lua/providers/colorschemes.lua | 14 ------- lua/fzf-lua/providers/module.lua | 5 +++ lua/fzf-lua/shell.lua | 2 +- lua/fzf-lua/utils.lua | 19 ++++++++- lua/fzf-lua/win.lua | 15 ++++--- 12 files changed, 146 insertions(+), 54 deletions(-) create mode 100644 lua/fzf-lua/profiles/hide.lua diff --git a/README.md b/README.md index c6a772c8..8ad233be 100644 --- a/README.md +++ b/README.md @@ -145,16 +145,18 @@ Alternatively, resuming work on a specific picker: > By default pressing esc or ctrl-c terminates the fzf process, > as such resume is not perfect and is limited to resuming the > picker/query and sometimes additional parameters such as regex -> in grep, etc, for a more "complete" resume press alt-esc to -> hide the fzf process instead, this will keep the fzf process -> running in the background and thus will restore the process -> entirely including cursor position and selection. +> in grep, etc, for a more complete resume use the "hide" profile, +> this will keep the fzf process running in the background allowing +> `:FzfLua resume` to restore the picker state entirely, including +> cursor position and selection. > To configure hiding by default: > ```lua -> require("fzf-lua").setup({ keymap = { builtin = { true, [""] = "hide" } } }) +> require("fzf-lua").setup({ +> "hide", +> -- your other settings here +> }) > ``` - **LIST OF AVAILABLE COMMANDS BELOW** 👇 ## Commands @@ -1077,10 +1079,6 @@ previewers = { winopts = { height = 0.55, width = 0.30, }, -- uncomment to ignore colorschemes names (lua patterns) -- ignore_patterns = { "^delek$", "^blue$" }, - -- uncomment to execute a callback on preview|close - -- e.g. a call to reset statusline highlights - -- cb_preview = function() ... end, - -- cb_exit = function() ... end, }, awesome_colorschemes = { prompt = 'Colorschemes❯ ', @@ -1099,9 +1097,6 @@ previewers = { ["ctrl-r"] = { fn = actions.cs_update, reload = true }, ["ctrl-x"] = { fn = actions.cs_delete, reload = true }, }, - -- uncomment to execute a callback on preview|close - -- cb_preview = function() ... end, - -- cb_exit = function() ... end, }, keymaps = { prompt = "Keymaps> ", @@ -1359,6 +1354,7 @@ require('fzf-lua').setup({'fzf-vim'}) | `borderless` | borderless and minimalistic seamless look & feel | | `borderless-full` | borderless with description in window title (instead of prompt) | | `border-fused` | single border around both fzf and the previewer | +| `hide` | send fzf process to background instead of termination | diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua index cbecfc77..fdfb5f9c 100644 --- a/lua/fzf-lua/config.lua +++ b/lua/fzf-lua/config.lua @@ -841,6 +841,10 @@ function M.normalize_opts(opts, globals, __resume_key) end end + if type(opts.enrich) == "function" then + opts = opts.enrich(opts) + end + -- mark as normalized opts._normalized = true diff --git a/lua/fzf-lua/core.lua b/lua/fzf-lua/core.lua index e01823c1..dacd5f92 100644 --- a/lua/fzf-lua/core.lua +++ b/lua/fzf-lua/core.lua @@ -610,7 +610,7 @@ M.create_fzf_binds = function(opts) -- Separate "transform|execute|execute-silent" binds to their own `--bind` argument, this -- way we can use `transform:...` and not be forced to use brackets, i.e. `transform(...)` -- this enables us to use brackets in the inner actions, e.g. "zero:transform:rebind(...)" - if action:match("transform") or action:match("execute") then + if action:match("transform") or action:match("execute") or action:match("reload") then table.insert(separate, bind) else table.insert(combine, bind) @@ -1103,6 +1103,12 @@ M.convert_reload_actions = function(reload_cmd, opts) local shell_action = shell.raw_action(function(items, _, _) v.fn(items, opts) end, v.field_index == false and "" or v.field_index or "{+}", opts.debug) + if type(v.prefix) == "string" and not v.prefix:match("%+$") then + v.prefix = v.prefix .. "+" + end + if type(v.postfix) == "string" and not v.postfix:match("^%+") then + v.prefix = "+" .. v.postfix + end opts.keymap.fzf[k] = { string.format("%s%sexecute-silent(%s)+reload(%s)%s", type(v.prefix) == "string" and v.prefix or "", @@ -1110,7 +1116,7 @@ M.convert_reload_actions = function(reload_cmd, opts) shell_action, reload_cmd, type(v.postfix) == "string" and v.postfix or ""), - desc = config.get_action_helpstr(v.fn) + desc = v.desc or config.get_action_helpstr(v.fn) } opts.actions[k] = nil end @@ -1125,25 +1131,51 @@ end ---@param opts table ---@return table M.convert_exec_silent_actions = function(opts) - -- Does not work with fzf version < 0.36, fzf fails with - -- "error 2: bind action not specified:" - if not utils.has(opts, "fzf", { 0, 36 }) - or utils.has(opts, "sk") then + -- `execute-silent` actions are bugged with skim (can't use quotes) + if utils.has(opts, "sk") then return opts end for k, v in pairs(opts.actions) do if type(v) == "table" and v.exec_silent then assert(type(v.fn) == "function") + local field_index = v.field_index == false and "" or v.field_index or "{q} {+}" -- replace the action with shell cmd proxy to the original action local shell_action = shell.raw_action(function(items, _, _) + if field_index:match("^{q}") then + local query = table.remove(items, 1) + config.resume_set("query", query, opts) + end v.fn(items, opts) - end, v.field_index == false and "" or v.field_index or "{+}", opts.debug) + end, field_index, opts.debug) + if type(v.prefix) == "string" and not v.prefix:match("%+$") then + v.prefix = v.prefix .. "+" + end + if type(v.postfix) == "string" and not v.postfix:match("^%+") then + v.postfix = "+" .. v.postfix + end + -- `execute-silent(...)` with fzf version < 0.36, errors with: + -- 'error 2: bind action not specified' (due to inner brackets) + -- changing to `execute-silent:...` removes the need to care for + -- brackets within the command with the limitation of not using + -- potfix (must be the last part of the arg), from `man fzf`: + -- + -- action-name:... + -- The last one is the special form that frees you from parse + -- errors as it does not expect the closing character. The catch is + -- that it should be the last one in the comma-separated list of + -- key-action pairs. + -- + local has_fzf036 = utils.has(opts, "fzf", { 0, 36 }) opts.keymap.fzf[k] = { - string.format("%sexecute-silent(%s)%s", + string.format("%sexecute-silent%s%s", type(v.prefix) == "string" and v.prefix or "", - shell_action, - type(v.postfix) == "string" and v.postfix or ""), - desc = config.get_action_helpstr(v.fn) + -- prefer "execute-silent:..." unless we have postfix + has_fzf036 and type(v.postfix) == "string" + and string.format("(%s)", shell_action) + or string.format(":%s", shell_action), + -- can't use postfix since we use "execute-silent:..." + has_fzf036 and type(v.postfix) == "string" and v.postfix or ""), + desc = v.desc or config.get_action_helpstr(v.fn) } opts.actions[k] = nil end diff --git a/lua/fzf-lua/defaults.lua b/lua/fzf-lua/defaults.lua index bf0a7f2f..315f3f7d 100644 --- a/lua/fzf-lua/defaults.lua +++ b/lua/fzf-lua/defaults.lua @@ -445,7 +445,7 @@ M.defaults.git = { prompt = "Branches> ", cmd = "git branch --all --color", preview = "git log --graph --pretty=oneline --abbrev-commit --color {1}", - remotes = "local", + remotes = "local", actions = { ["enter"] = actions.git_switch, ["ctrl-x"] = { fn = actions.git_branch_del, reload = true }, diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua index 785c33bb..1eff2e5c 100644 --- a/lua/fzf-lua/init.lua +++ b/lua/fzf-lua/init.lua @@ -146,7 +146,7 @@ end -- case the user decides not to call `setup()` M.setup_highlights() -local function load_profiles(profiles) +function M.load_profiles(profiles, silent, opts) local ret = {} profiles = type(profiles) == "table" and profiles or type(profiles) == "string" and { profiles } @@ -155,15 +155,15 @@ local function load_profiles(profiles) -- backward compat, renamed "borderless_full" > "borderless-full" if profile == "borderless_full" then profile = "borderless-full" end local fname = path.join({ vim.g.fzf_lua_directory, "profiles", profile .. ".lua" }) - local profile_opts = utils.load_profile_fname(fname, nil, 1) + local profile_opts = utils.load_profile_fname(fname, nil, silent) if type(profile_opts) == "table" then if profile_opts[1] then -- profile requires loading base profile(s) profile_opts = vim.tbl_deep_extend("keep", - profile_opts, load_profiles(profile_opts[1])) + profile_opts, M.load_profiles(profile_opts[1], silent, opts)) end if type(profile_opts.fn_load) == "function" then - profile_opts.fn_load() + profile_opts.fn_load(opts) profile_opts.fn_load = nil end ret = vim.tbl_deep_extend("force", ret, profile_opts) @@ -178,7 +178,7 @@ function M.setup(opts, do_not_reset_defaults) opts[1] = opts[1] == nil and utils.__HAS_NVIM_09 and "default-title" or opts[1] if opts[1] then -- Did the user supply profile(s) to load? - opts = vim.tbl_deep_extend("keep", opts, load_profiles(opts[1])) + opts = vim.tbl_deep_extend("keep", opts, M.load_profiles(opts[1], 1, opts)) end if do_not_reset_defaults then -- no defaults reset requested, merge with previous setup options @@ -384,6 +384,7 @@ M._exported_modules = { M._excluded_meta = { "setup", "redraw", + "load_profiles", "fzf", "fzf_raw", "fzf_wrap", diff --git a/lua/fzf-lua/profiles/README.md b/lua/fzf-lua/profiles/README.md index 535c483e..223456da 100644 --- a/lua/fzf-lua/profiles/README.md +++ b/lua/fzf-lua/profiles/README.md @@ -29,6 +29,7 @@ telescope defaults with `bat` previewer: | `borderless` | borderless and minimalistic seamless look & feel | | `borderless-full` | borderless with description in window title (instead of prompt) | | `border-fused` | single border around both fzf and the previewer | +| `hide` | send fzf process to background instead of termination | **Custom user settings which make sense and aren't mere duplications with minimal modifications diff --git a/lua/fzf-lua/profiles/hide.lua b/lua/fzf-lua/profiles/hide.lua new file mode 100644 index 00000000..9e43e90b --- /dev/null +++ b/lua/fzf-lua/profiles/hide.lua @@ -0,0 +1,51 @@ +local uv = vim.uv or vim.loop +local fzf = require("fzf-lua") +return { + desc = "hide interface instead of abort", + defaults = { + enrich = function(opts) + if opts._is_fzf_tmux then + fzf.utils.warn("'hide' profile cannot work with tmux, ignoring.") + return opts + end + -- `execute-silent` actions are bugged with skim + if fzf.utils.has(opts, "sk") then + opts.actions["esc"] = false + opts.keymap.builtin[""] = "hide" + return opts + end + -- While we can use `keymap.builtin.` (to hide) this is better + -- as it captures the query when execute-silent action is called as + -- we add "{q}" as the first field index similar to `--print-query` + local histfile = opts.fzf_opts and opts.fzf_opts["--history"] + opts.actions["esc"] = { fn = fzf.actions.dummy_abort, desc = "hide" } + opts.actions = vim.tbl_map(function(act) + act = type(act) == "function" and { fn = act } or act + act = type(act) == "table" and type(act[1]) == "function" + and { fn = act[1], noclose = true } or act + assert(type(act) == "table" and type(act.fn) == "function" or not act) + if type(act) == "table" and + not act.exec_silent and not act.reload and not act.noclose + then + local fn = act.fn + act.exec_silent = true + act.desc = act.desc or fzf.config.get_action_helpstr(fn) + act.fn = function(s, o) + fzf.hide() + fn(s, o) + if histfile and type(o.last_query) == "string" and #o.last_query > 0 then + local fd = uv.fs_open(histfile, "a", -1) + if fd then + uv.fs_write(fd, o.last_query .. "\n", nil, function(_) + uv.fs_close(fd) + end) + end + end + end + end + return act + end, opts.actions) + return opts + end, + }, +} diff --git a/lua/fzf-lua/providers/colorschemes.lua b/lua/fzf-lua/providers/colorschemes.lua index 6ede629f..66d97bc0 100644 --- a/lua/fzf-lua/providers/colorschemes.lua +++ b/lua/fzf-lua/providers/colorschemes.lua @@ -62,9 +62,6 @@ M.colorschemes = function(opts) opts.preview = shell.raw_action(function(sel) if opts.live_preview and sel then vim.cmd("colorscheme " .. sel[1]) - if type(opts.cb_preview) == "function" then - opts.cb_preview(sel, opts) - end end end, nil, opts.debug) end @@ -83,10 +80,6 @@ M.colorschemes = function(opts) -- setup fzf-lua's own highlight groups utils.setup_highlights() - - if type(opts.cb_exit) == "function" then - opts.cb_exit(selected, opts) - end end core.fzf_exec(colors, opts) @@ -486,9 +479,6 @@ M.awesome_colorschemes = function(opts) -- wrap in pcall as some colorschemes have bg triggers that can fail pcall(function() vim.o.background = opts._cur_background end) M.apply_awesome_theme(dbkey, idx, opts) - if type(opts.cb_preview) == "function" then - opts.cb_preview(sel, opts) - end else vim.cmd("colorscheme " .. opts._cur_colorscheme) vim.o.background = opts._cur_background @@ -530,10 +520,6 @@ M.awesome_colorschemes = function(opts) -- setup fzf-lua's own highlight groups utils.setup_highlights() - - if type(o.cb_exit) == "function" then - o.cb_exit(sel, o) - end end opts = core.set_header(opts, opts.headers or { "actions" }) diff --git a/lua/fzf-lua/providers/module.lua b/lua/fzf-lua/providers/module.lua index 37a624c8..72e99606 100644 --- a/lua/fzf-lua/providers/module.lua +++ b/lua/fzf-lua/providers/module.lua @@ -57,6 +57,11 @@ M.profiles = function(opts) opts = config.normalize_opts(opts, "profiles") if not opts then return end + if opts.load then + utils.load_profiles(opts.load) + return + end + local dirs = { path.join({ vim.g.fzf_lua_directory, "profiles" }) } diff --git a/lua/fzf-lua/shell.lua b/lua/fzf-lua/shell.lua index 2b4cf142..c0e7a5b6 100644 --- a/lua/fzf-lua/shell.lua +++ b/lua/fzf-lua/shell.lua @@ -12,7 +12,7 @@ local M = {} -- provider are 2 (`live_grep` with `multiprocess=false`) -- and 4 (`git_status` with preview and 3 reload binds) -- we can always increase if we need more -local _MAX_LEN = vim.g.fzf_lua_shell_maxlen or 10 +local _MAX_LEN = 50 local _index = 0 local _registry = {} local _protected = {} diff --git a/lua/fzf-lua/utils.lua b/lua/fzf-lua/utils.lua index 3eb38635..370db0fd 100644 --- a/lua/fzf-lua/utils.lua +++ b/lua/fzf-lua/utils.lua @@ -820,6 +820,10 @@ function M.CTX() return loadstring("return require'fzf-lua'.core.CTX()")() end +function M.__CTX() + return loadstring("return require'fzf-lua'.core.__CTX")() +end + function M.resume_get(what, opts) local f = loadstring("return require'fzf-lua'.config.resume_get")() return f(what, opts) @@ -841,7 +845,7 @@ end ---@param fname string ---@param name string|nil ----@param silent boolean|number +---@param silent boolean|integer function M.load_profile_fname(fname, name, silent) local profile = name or vim.fn.fnamemodify(fname, ":t:r") or "" local ok, res = pcall(dofile, fname) @@ -861,6 +865,19 @@ function M.load_profile_fname(fname, name, silent) end end +function M.load_profiles(profiles) + local serpent = require("fzf-lua.lib.serpent") + profiles = type(profiles) == "table" + and serpent.line(profiles, { comment = false, sortkeys = false }) + or type(profiles) == "string" and string.format("'%s'", profiles) + or nil + if type(profiles) == "string" then + loadstring(string.format( + "require'fzf-lua'.setup(require'fzf-lua'.load_profiles(%s, false))", + profiles))() + end +end + function M.send_ctrl_c() vim.api.nvim_feedkeys( vim.api.nvim_replace_termcodes("", true, false, true), "n", true) diff --git a/lua/fzf-lua/win.lua b/lua/fzf-lua/win.lua index bec44cd3..88927653 100644 --- a/lua/fzf-lua/win.lua +++ b/lua/fzf-lua/win.lua @@ -47,12 +47,12 @@ function TSInjector.deregister() TSInjector._setup = nil end -function TSInjector.clear_cache(buf, noassert) +function TSInjector.clear_cache(buf) -- If called from fzf-tmux buf will be `nil` (#1556) if not buf then return end TSInjector.cache[buf] = nil -- If called from `FzfWin.hide` cache will not be empty - assert(noassert or utils.tbl_isempty(TSInjector.cache)) + assert(utils.tbl_isempty(TSInjector.cache)) end ---@param buf number @@ -834,8 +834,8 @@ function FzfWin:set_winleave_autocmd() self:_nvim_create_autocmd("WinClosed", self.win_leave) end -function FzfWin:treesitter_detach(buf, noassert) - TSInjector.clear_cache(buf, noassert) +function FzfWin:treesitter_detach(buf) + TSInjector.clear_cache(buf) TSInjector.deregister() end @@ -889,7 +889,8 @@ function FzfWin:treesitter_attach() if #filepath == 0 or string.byte(text, 1) == 160 then if string.byte(text, 1) == 160 then text = text:sub(2) end -- remove A0+SPACE if string.byte(text, 1) == 32 then text = text:sub(2) end -- remove leading SPACE - local b = filepath:match("^%d+") or utils.CTX().bufnr + -- IMPORTANT: use the `__CTX` version that doesn't trigger a new context + local b = filepath:match("^%d+") or utils.__CTX().bufnr return vim.api.nvim_buf_is_valid(tonumber(b)) and b or nil end end)() @@ -943,8 +944,6 @@ function FzfWin:set_tmp_buffer(no_wipe) self:set_winleave_autocmd() -- automatically resize fzf window self:set_redraw_autocmd() - -- Use treesitter to highlight results on the main fzf window - self:treesitter_attach() -- since we have the cursorline workaround from -- issue #254, resume shows an ugly cursorline. -- remove it, nvim_win API is better than vim.wo? @@ -1099,7 +1098,7 @@ function FzfWin:close(fzf_bufnr, do_not_clear_cache) vim.api.nvim_buf_delete(self.fzf_bufnr, { force = true }) end -- Clear treesitter buffer cache and deregister decoration callbacks - self:treesitter_detach(self.fzf_bufnr, self._hidden_fzf_bufnr) + self:treesitter_detach(self._hidden_fzf_bufnr or self.fzf_bufnr) -- when using `split = "belowright new"` closing the fzf -- window may not always return to the correct source win -- depending on the user's split configuration (#397)