From 817df87a8ebd2137aa42c2fa370e33b980e313dc Mon Sep 17 00:00:00 2001 From: bhagwan Date: Sat, 25 Jun 2022 19:22:06 -0700 Subject: [PATCH] Major refactor, read below if you're having issues This patch is quite massive, hopefully I got everything right in testing, changes include: - New API interface for "live" queries, for usage refer to: https://github.com/ibhagwan/fzf-lua/wiki/Advanced - All providers now use 'fzf_exec' API (previously 'fzf_wrap') - All "live" queries now use 'fzf_live' API - Better resume support for "live" queries - Fzf initial command now uses $FZF_DEFAULT_COMMAND instead of piping the command, this delegates the responsiblity to fzf which kills the command on exit resulting in better responsibness when exiting fzf - Added 'silent_fail' option (default:'true') to prevent fzf from displaying [Command failed:...] when commands exit with error code - Exposed 'config.globals' as 'require'fzf-lua'.defaults' - Fix: 'libuv.shellescape' with special chars in fish shells - Manpages: moved fzf option `--tiebreak=begin' to config - Buffer actions: navigate to line if exists - Lsp_diagnostics: properly use a coroutine - make_entry signatures changed (entry before opts) - Removed make_entry shortcuts from 'core' - Removed 'coroutine.yield' where unecessary - Fix: 'git_icons' with 'live_grep({multiprocess=false})' - tagstack: use relative paths and replace '$HOME' with '~' - Deprecated 'core.fzf_files' - Fix: resume query if cancelled while loading indicator is shown - Fix: resume query when command failed with 'silent_fail=false' --- README.md | 26 +- doc/fzf-lua.txt | 6 +- lua/fzf-lua/actions.lua | 63 +++-- lua/fzf-lua/config.lua | 1 + lua/fzf-lua/core.lua | 341 +++++++++++++------------ lua/fzf-lua/fzf.lua | 40 +-- lua/fzf-lua/init.lua | 9 +- lua/fzf-lua/libuv.lua | 8 +- lua/fzf-lua/make_entry.lua | 29 ++- lua/fzf-lua/previewer/fzf.lua | 1 - lua/fzf-lua/providers/buffers.lua | 42 +-- lua/fzf-lua/providers/colorschemes.lua | 7 +- lua/fzf-lua/providers/dap.lua | 40 +-- lua/fzf-lua/providers/files.lua | 10 +- lua/fzf-lua/providers/git.lua | 17 +- lua/fzf-lua/providers/grep.lua | 114 +++------ lua/fzf-lua/providers/helptags.lua | 19 +- lua/fzf-lua/providers/lsp.lua | 74 +++--- lua/fzf-lua/providers/manpages.lua | 49 +--- lua/fzf-lua/providers/module.lua | 9 +- lua/fzf-lua/providers/nvim.lua | 84 ++---- lua/fzf-lua/providers/oldfiles.lua | 10 +- lua/fzf-lua/providers/quickfix.lua | 9 +- lua/fzf-lua/providers/tags.lua | 16 +- lua/fzf-lua/shell.lua | 142 ++++++++-- lua/fzf-lua/utils.lua | 13 +- lua/fzf-lua/win.lua | 4 +- 27 files changed, 556 insertions(+), 627 deletions(-) diff --git a/README.md b/README.md index 1c7fed9..0c323d9 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ ![Demo](https://raw.githubusercontent.com/wiki/ibhagwan/fzf-lua/demo.gif) -[fzf](https://github.com/junegunn/fzf) changed my life, it can change yours too, if you allow it. +[fzf](https://github.com/junegunn/fzf) changed my command life, it can change +yours too, if you allow it. - + ## Rationale @@ -30,9 +31,9 @@ the new shiny fuzzy finders for neovim. ## Why Fzf-Lua -... and not, to name a few, -[telescope](https://github.com/nvim-telescope/telescope.nvim) or -[vim-clap](https://github.com/liuchengxu/vim-clap)? +... and not +[telescope](https://github.com/nvim-telescope/telescope.nvim) +or any other vim/neovim household name? As [@junegunn](https://github.com/junegunn) himself put it, “because you can and you love `fzf`”. @@ -55,7 +56,7 @@ at it. That, **and colorful file icons and git indicators!**. (optional) > `fzf` version > `0.27` is recommended but it's still possible to use `fzf` -> version > `0.24` by setting `fzf_opts = { ['--border'] = false }`, see +> version > `0.25` by setting `fzf_opts = { ['--border'] = false }`, see > [Customization](#customization). ### Optional dependencies @@ -249,9 +250,14 @@ vim.api.nvim_set_keymap('n', '', ## Customization -I tried to make it as customizable as possible, if you find you need to change something that isn’t below, open an issue and I’ll do my best to add it. +> **[ADVANCED CUSTOMIZATION](https://github.com/ibhagwan/fzf-lua/wiki/Advanced) +: to create your own fzf-lua commands see +[Wiki/ADVANCED](https://github.com/ibhagwan/fzf-lua/wiki/Advanced)** -customization can be achieved by calling the `setup()` function or individually sending parameters to a builtin command, for example: +I tried to make this plugin as customizable as possible, if you find you need to +change something that isn’t below, open an issue and I’ll do my best to add it. + +Customization can be achieved by calling the `setup()` function or individually sending parameters to a builtin command, for example: ```lua :lua require('fzf-lua').files({ fzf_opts = {['--layout'] = 'reverse-list'} }) ``` @@ -603,7 +609,7 @@ require'fzf-lua'.setup { -- 'rg_glob_fn' to return a pair: -- first returned argument is the new search query -- second returned argument are addtional rg flags - -- rg_glob_fn = function(opts, query) + -- rg_glob_fn = function(query, opts) -- ... -- return new_query, flags -- end, @@ -810,7 +816,7 @@ and plugin codes that I probably forgot where I found some samples from so if I missed your name feel free to contact me and I'll add it below: - [@vijaymarupudi](https://github.com/vijaymarupudi/) for his wonderful - [nvim-fzf](https://github.com/vijaymarupudi/nvim-fzf) plugin which is in the + [nvim-fzf](https://github.com/vijaymarupudi/nvim-fzf) plugin which is at the core of this plugin - [@tjdevries](https://github.com/tjdevries/) for too many great things to list here and for borrowing some of his diff --git a/doc/fzf-lua.txt b/doc/fzf-lua.txt index 2f1a6ca..b462891 100644 --- a/doc/fzf-lua.txt +++ b/doc/fzf-lua.txt @@ -66,7 +66,7 @@ DEPENDENCIES *fzf-lua-dependencies* - nvim-web-devicons (optional) `fzf` version > `0.27` is recommended but it's still possible to use `fzf` - version > `0.24` by setting `fzf_opts = { ['--border'] = false }`, see + version > `0.25` by setting `fzf_opts = { ['--border'] = false }`, see Customization <#customization>. @@ -648,7 +648,7 @@ Consult the list below for available settings: -- 'rg_glob_fn' to return a pair: -- first returned argument is the new search query -- second returned argument are addtional rg flags - -- rg_glob_fn = function(opts, query) + -- rg_glob_fn = function(query, opts) -- ... -- return new_query, flags -- end, @@ -874,4 +874,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 3db17c2..69e018e 100644 --- a/lua/fzf-lua/actions.lua +++ b/lua/fzf-lua/actions.lua @@ -213,27 +213,37 @@ M.file_switch_or_edit = function(...) end -- buffer actions -M.vimcmd_buf = function(vimcmd, selected, _) +M.vimcmd_buf = function(vimcmd, selected, opts) local curbuf = vim.api.nvim_get_current_buf() + local lnum = vim.api.nvim_win_get_cursor(0)[1] + local is_term = utils.is_term_buffer(0) for i = 1, #selected do - local bufnr = string.match(selected[i], "%[(%d+)") - if bufnr then - if vimcmd == 'b' - and curbuf ~= tonumber(bufnr) - and not vim.o.hidden and - utils.buffer_is_dirty(nil, true) then - -- warn the user when trying to switch from a dirty buffer - -- when `:set nohidden` - return - end - if vimcmd ~= "b" or curbuf ~= tonumber(bufnr) then - local cmd = vimcmd .. " " .. bufnr - local ok, res = pcall(vim.cmd, cmd) - if not ok then - utils.warn(("':%s' failed: %s"):format(cmd, res)) - end + local entry = path.entry_to_file(selected[i], opts) + if not entry.bufnr then return end + assert(type(entry.bufnr) == 'number') + if vimcmd == 'b' + and curbuf ~= entry.bufnr + and not vim.o.hidden and + utils.buffer_is_dirty(nil, true) then + -- warn the user when trying to switch from a dirty buffer + -- when `:set nohidden` + return + end + -- add current location to jumplist + if not is_term then vim.cmd("normal! m`") end + if vimcmd ~= "b" or curbuf ~= entry.bufnr then + local cmd = vimcmd .. " " .. entry.bufnr + local ok, res = pcall(vim.cmd, cmd) + if not ok then + utils.warn(("':%s' failed: %s"):format(cmd, res)) end end + if curbuf ~= entry.bufnr or lnum ~= entry.line then + -- make sure we have valid column + entry.col = entry.col and entry.col>0 and entry.col or 1 + vim.api.nvim_win_set_cursor(0, {tonumber(entry.line), tonumber(entry.col)-1}) + end + if not is_term then vim.cmd("norm! zvzz") end end end @@ -431,19 +441,25 @@ M.help_tab = function(selected) M.vimcmd(vimcmd, helptags(selected), true) end +local function mantags(s) + return vim.tbl_map(function(x) + return x:match("[^[,( ]+") + end, s) +end + M.man = function(selected) local vimcmd = "Man" - M.vimcmd(vimcmd, selected) + M.vimcmd(vimcmd, mantags(selected)) end M.man_vert = function(selected) local vimcmd = "vert Man" - M.vimcmd(vimcmd, selected) + M.vimcmd(vimcmd, mantags(selected)) end M.man_tab = function(selected) local vimcmd = "tab Man" - M.vimcmd(vimcmd, selected) + M.vimcmd(vimcmd, mantags(selected)) end @@ -586,8 +602,7 @@ end M.grep_lgrep = function(_, opts) - -- 'FNCREF' is set only on 'M.live_grep' calls - -- 'MODULE' is set on 'M.grep' and 'live_grep' calls + -- 'MODULE' is set on 'grep' and 'live_grep' calls assert(opts.__MODULE__ and type(opts.__MODULE__.grep) == 'function' or type(opts.__MODULE__.live_grep) == 'function') @@ -601,7 +616,8 @@ M.grep_lgrep = function(_, opts) requires_processing = opts.rg_glob or opts.__call_opts.rg_glob, }, opts.__call_opts or {}) - if opts.__FNCREF__ then + -- 'fn_reload' is set only on 'live_grep' calls + if opts.fn_reload then opts.__MODULE__.grep(o) else opts.__MODULE__.live_grep(o) @@ -609,3 +625,4 @@ M.grep_lgrep = function(_, opts) end return M + diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua index 8bf025f..88e4a7d 100644 --- a/lua/fzf-lua/config.lua +++ b/lua/fzf-lua/config.lua @@ -484,6 +484,7 @@ M.globals.manpages = { ["ctrl-v"] = actions.man_vert, ["ctrl-t"] = actions.man_tab, }, + fzf_opts = { ['--tiebreak'] = 'begin' }, previewer = "man", } M.globals.lsp = { diff --git a/lua/fzf-lua/core.lua b/lua/fzf-lua/core.lua index 11fe01d..714d232 100644 --- a/lua/fzf-lua/core.lua +++ b/lua/fzf-lua/core.lua @@ -10,6 +10,73 @@ local make_entry = require "fzf-lua.make_entry" local M = {} +-- Main API, see: +-- https://github.com/ibhagwan/fzf-lua/wiki/Advanced +M.fzf_exec = function(contents, opts) + if not opts or not opts._normalized then + opts = config.normalize_opts(opts or {}, {}) + if not opts then return end + end + opts.fn_selected = opts.fn_selected or function(selected) + if not selected then return end + actions.act(opts.actions, selected, opts) + end + -- wrapper for command transformer + if type(contents) == 'string' and + (opts.fn_transform or opts.fn_preprocess) then + contents = libuv.spawn_nvim_fzf_cmd({ + cmd = contents, + cwd = opts.cwd, + pid_cb = opts._pid_cb, + }, + opts.fn_transform or function(x) return x end, + opts.fn_preprocess) + end + -- setup as "live": disables fuzzy matching and reload the content + -- every keystroke (query changed), utlizes fzf's 'change:reload' + -- event trigger or skim's "interactive" mode + if type(opts.fn_reload) == 'string' then + if not opts.fn_transform then + -- TODO: add support for 'fn_transform' using 'mt_cmd_wrapper' + -- functions can be stored using 'config.bytecode' which uses + -- 'string.dump' to convert to function code to bytes + opts = M.setup_fzf_interactive_native(opts.fn_reload, opts) + contents = opts.__fzf_init_cmd + else + -- the caller requested to transform, we need to convert + -- to a function that returns string so that libuv.spawn + -- is called + local cmd = opts.fn_reload + opts.fn_reload = function(q) + if cmd:match(M.fzf_query_placeholder) then + return cmd:gsub(M.fzf_query_placeholder, q or '') + else + return string.format("%s %s", cmd, q or '') + end + end + end + end + if type(opts.fn_reload) == 'function' then + opts.__fn_transform = opts.fn_transform + opts.__fn_reload = function(query) + if config.__resume_data then + config.__resume_data.last_query = query + end + return opts.fn_reload(query) + end + opts = M.setup_fzf_interactive_wrap(opts) + contents = opts.__fzf_init_cmd + end + return M.fzf_wrap(opts, contents)() +end + +M.fzf_live = function(contents, opts) + assert(contents) + opts = opts or {} + opts.fn_reload = contents + return M.fzf_exec(nil, opts) +end + M.fzf_resume = function(opts) if not config.__resume_data or not config.__resume_data.opts then utils.info("No resume data available, is 'global_resume' enabled?") @@ -17,26 +84,16 @@ M.fzf_resume = function(opts) end opts = vim.tbl_deep_extend("force", config.__resume_data.opts, opts or {}) local last_query = config.__resume_data.last_query - if last_query and #last_query>0 then - last_query = vim.fn.shellescape(last_query) - else + if not last_query or #last_query==0 then -- in case we continue from another resume -- reset the previous query which was saved -- inside "fzf_opts['--query']" argument last_query = false end opts.__resume = true - if opts.__FNCREF__ then - -- HACK for 'live_grep' and 'lsp_live_workspace_symbols' - opts.cmd = nil - opts.query = nil - opts.search = nil - opts.resume = true - opts.__FNCREF__(opts) - else - opts.fzf_opts['--query'] = last_query - M.fzf_wrap(opts, config.__resume_data.contents)() - end + opts.query = last_query + opts.fzf_opts['--query'] = last_query and vim.fn.shellescape(last_query) + M.fzf_exec(config.__resume_data.contents, opts) end M.fzf_wrap = function(opts, contents, fn_selected) @@ -50,26 +107,10 @@ M.fzf_wrap = function(opts, contents, fn_selected) end) end -M.fzf_exec = function(contents, opts) - opts = config.normalize_opts(opts or {}, {}) - opts.fn_selected = opts.fn_selected or function(selected) - if not selected then return end - actions.act(opts.actions, selected, opts) - end - -- wrapper for command transformer - if type(contents) == 'string' and opts.fn_transform then - contents = libuv.spawn_nvim_fzf_cmd({ - cmd = contents, - cwd = opts.cwd, - }, opts.fn_transform) - end - return M.fzf_wrap(opts, contents)() -end - M.fzf = function(opts, contents) -- normalize with globals if not already normalized - if not opts._normalized then - opts = config.normalize_opts(opts, {}) + if not opts or not opts._normalized then + opts = config.normalize_opts(opts or {}, {}) if not opts then return end end if opts.fn_pre_win then @@ -163,9 +204,12 @@ M.fzf = function(opts, contents) opts.fzf_opts['--preview-window'] = 'hidden:right:0' end + -- some functions such as buffers|tabs + -- need to reacquire current buffer|tab state + if opts._fn_pre_fzf then + opts._fn_pre_fzf(opts) + end if opts.fn_pre_fzf then - -- some functions such as buffers|tabs - -- need to reacquire current buffer|tab state opts.fn_pre_fzf(opts) end @@ -175,7 +219,7 @@ M.fzf = function(opts, contents) -- lose overrides by 'winopts_fn|winopts_raw' opts.winopts.preview = fzf_win.winopts.preview local selected, exit_code = fzf.raw_fzf(contents, M.build_fzf_cli(opts), - { fzf_binary = opts.fzf_bin, fzf_cwd = opts.cwd }) + { fzf_bin = opts.fzf_bin, cwd = opts.cwd, silent_fail = opts.silent_fail }) -- This was added by 'resume': -- when '--print-query' is specified -- we are guaranteed to have the query @@ -191,6 +235,9 @@ M.fzf = function(opts, contents) end table.remove(selected, 1) end + if opts._fn_post_fzf then + opts._fn_post_fzf(opts, selected) + end if opts.fn_post_fzf then opts.fn_post_fzf(opts, selected) end @@ -227,10 +274,12 @@ M.get_color = function(hl_group, what) end -- Create fzf --color arguments from a table of vim highlight groups. -M.create_fzf_colors = function(colors) - if not colors then - return "" +M.create_fzf_colors = function(opts) + local colors = opts and opts.fzf_colors + if type(colors) == 'function' then + colors = colors(opts) end + if not colors then return end local tbl = {} for highlight, list in pairs(colors) do @@ -247,7 +296,7 @@ M.create_fzf_colors = function(colors) end end - return string.format("--color=%s", table.concat(tbl, ",")) + return not vim.tbl_isempty(tbl) and table.concat(tbl, ",") end M.create_fzf_binds = function(binds) @@ -282,7 +331,7 @@ M.build_fzf_cli = function(opts) 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.fzf_colors) + opts.fzf_opts["--color"] = M.create_fzf_colors(opts) end opts.fzf_opts["--expect"] = actions.expect(opts.actions) opts.fzf_opts["--preview"] = opts.preview or opts.fzf_opts["--preview"] @@ -399,8 +448,10 @@ M.mt_cmd_wrapper = function(opts) -- command does not require any processing return opts.cmd elseif opts.multiprocess then - local fn_preprocess = opts._fn_preprocess_str or [[return require("make_entry").preprocess]] - local fn_transform = opts._fn_transform_str or [[return require("make_entry").file]] + 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 @@ -427,40 +478,23 @@ M.mt_cmd_wrapper = function(opts) end return cmd else + assert(not opts.__mt_transform or type(opts.__mt_transform) == 'function') + assert(not opts.__mt_preprocess or type(opts.__mt_preprocess) == 'function') return libuv.spawn_nvim_fzf_cmd(opts, function(x) - return opts._fn_transform - and opts._fn_transform(opts, x) - or make_entry.file(opts, x) + return opts.__mt_transform + and opts.__mt_transform(x, opts) + or make_entry.file(x, opts) end, function(o) -- setup opts.cwd and git diff files - return opts._fn_preprocess - and opts._fn_preprocess(o) + return opts.__mt_preprocess + and opts.__mt_preprocess(o) or make_entry.preprocess(o) end) end end --- shortcuts to make_entry -M.get_devicon = make_entry.get_devicon -M.make_entry_file = make_entry.file -M.make_entry_preprocess = make_entry.preprocess - -M.make_entry_lcol = function(opts, entry) - if not entry then return nil end - local filename = entry.filename or vim.api.nvim_buf_get_name(entry.bufnr) - return string.format("%s:%s:%s:%s%s", - -- uncomment to test URIs - -- "file://" .. filename, - filename, --utils.ansi_codes.magenta(filename), - utils.ansi_codes.green(tostring(entry.lnum)), - utils.ansi_codes.blue(tostring(entry.col)), - entry.text and #entry.text>0 and " " or "", - not entry.text and "" or - (opts.trim_entry and vim.trim(entry.text)) or entry.text) -end - -- given the default delimiter ':' this is the -- fzf experssion field index for the line number -- when entry format is 'file:line:col: text' @@ -542,7 +576,7 @@ M.set_header = function(opts, hdr_tbl) if opts.no_header_i then return end for k, v in pairs(opts.actions) do if type(v) == 'table' and v[1] == actions.grep_lgrep then - local to = opts.__FNCREF__ and 'Grep' or 'Live Grep' + local to = opts.fn_reload and 'Grep' or 'Live Grep' return (':: <%s> to %s'):format( utils.ansi_codes.yellow(k), utils.ansi_codes.red(to)) @@ -583,127 +617,108 @@ M.set_header = function(opts, hdr_tbl) return opts end - +-- NOT IN USE, here for backward compat M.fzf_files = function(opts, contents) + utils.warn("'core.fzf_files' is deprecated, use 'fzf_exec' instead," + .. " see github@fzf-lua/wiki/Advanced.") + M.fzf_exec(contents or opts and opts.fzf_fn and opts.fzf_fn, opts) +end - if not opts then return end +M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts) + -- query cannot be 'nil' + opts.query = opts.query or '' - M.fzf_wrap(opts, contents or opts.fzf_fn, function(selected) + -- by redirecting the error stream to stdout + -- we make sure a clear error message is displayed + -- when the user enters bad regex expressions + local initial_command = command + if (opts.stderr_to_stdout ~= false) and + not initial_command:match("2>") then + initial_command = command .. " 2>&1" + end - if opts.post_select_cb then - opts.post_select_cb() + local reload_command = initial_command + if not opts.exec_empty_query then + reload_command = ('[ -z %s ] || %s'):format(fzf_field_expression, reload_command) + end + if opts._is_skim then + -- skim interactive mode does not need a piped command + opts.__fzf_init_cmd = nil + opts.prompt = opts.prompt or opts.fzf_opts['--prompt'] + if opts.prompt then + opts.fzf_opts['--prompt'] = opts.prompt:match("[^%*]+") + opts.fzf_opts['--cmd-prompt'] = libuv.shellescape(opts.prompt) + opts.prompt = nil end - - if not selected then return end - - if #selected > 1 then - local idx = utils.tbl_length(opts.actions)>1 and 2 or 1 - for i = idx, #selected do - selected[i] = path.entry_to_file(selected[i], opts).stripped - end + -- '--query' was set by 'resume()', skim has the option to switch back and + -- forth between interactive command and fuzzy matching (using 'ctrl-q') + -- setting both '--query' and '--cmd-query' will use to fuzzy match + -- on top of our result set double filtering our results (undesierable) + opts.fzf_opts['--query'] = nil + -- since we surrounded the skim placeholder with quotes + -- we need to escape them in the initial query + opts.fzf_opts['--cmd-query'] = libuv.shellescape(utils.sk_escape(opts.query)) + opts._fzf_cli_args = string.format("--interactive --cmd %s", + vim.fn.shellescape(reload_command)) + else + -- send an empty table to avoid running $FZF_DEFAULT_COMMAND + opts.__fzf_init_cmd = {} + 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("%%", "%%%%"))) 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) + end + opts._fzf_cli_args = string.format('--bind=%s', + vim.fn.shellescape(("change:reload:%s"):format( + ("%s"):format(reload_command)))) + end - actions.act(opts.actions, selected, opts) - - end)() - + return opts end -M.set_fzf_interactive_cmd = function(opts) - - if not opts then return end +-- query placeholder for "live" queries +M.fzf_query_placeholder = "" +M.fzf_field_expression = function(opts) -- fzf already adds single quotes around the placeholder when expanding -- for skim we surround it with double quotes or single quote searches fail - local placeholder = utils._if(opts._is_skim, '"{}"', '{q}') - local raw_async_act = shell.reload_action_cmd(opts, placeholder) - return M.set_fzf_interactive(opts, raw_async_act, placeholder) + return opts and opts._is_skim and '"{}"' or '{q}' end -M.set_fzf_interactive_cb = function(opts) - - if not opts then return end - - -- fzf already adds single quotes around the placeholder when expanding - -- for skim we surround it with double quotes or single quote searches fail - local placeholder = utils._if(opts._is_skim, '"{}"', '{q}') - - local uv = vim.loop - local raw_async_act = shell.raw_async_action(function(pipe, args) +-- Sets up the flags and commands require for running a "live" interface +-- @param fn_reload :function called for reloading contents +-- @param fn_transform :function to transform entries when using shell cmd +M.setup_fzf_interactive_wrap = function(opts) - coroutine.wrap(function() + assert(opts and opts.__fn_reload) - local co = coroutine.running() - local results = opts._reload_action(args[1]) - - local close_pipe = function() - if pipe and not uv.is_closing(pipe) then - uv.close(pipe) - pipe = nil - end - coroutine.resume(co) - end + -- neovim shell wrapper for parsing the query and loading contents + local fzf_field_expression = M.fzf_field_expression(opts) + local command = shell.reload_action_cmd(opts, fzf_field_expression) + return M.setup_fzf_interactive_flags(command, fzf_field_expression, opts) - if type(results) == 'table' and not vim.tbl_isempty(results) then - uv.write(pipe, - vim.tbl_map(function(x) return x.."\n" end, results), - function(_) - close_pipe() - end) - -- wait for write to finish - coroutine.yield() - end - -- does nothing if write finished successfully - close_pipe() - - end)() - end, placeholder, opts.debug) - - return M.set_fzf_interactive(opts, raw_async_act, placeholder) end -M.set_fzf_interactive = function(opts, act_cmd, placeholder) +M.setup_fzf_interactive_native = function(command, opts) - if not opts or not act_cmd or not placeholder then return end + local fzf_field_expression = M.fzf_field_expression(opts) - -- cannot be nil - local query = opts.query or '' - - if opts._is_skim then - -- do not run an empty string query unless the user requested - if not opts.exec_empty_query then - act_cmd = "sh -c " .. vim.fn.shellescape( - ("[ -z %s ] || %s"):format(placeholder, act_cmd)) - else - act_cmd = vim.fn.shellescape(act_cmd) - end - -- skim interactive mode does not need a piped command - opts.fzf_fn = nil - opts.fzf_opts['--prompt'] = opts.prompt:match("[^%*]+") - opts.fzf_opts['--cmd-prompt'] = libuv.shellescape(opts.prompt) - opts.prompt = nil - -- since we surrounded the skim placeholder with quotes - -- we need to escape them in the initial query - opts.fzf_opts['--cmd-query'] = libuv.shellescape(utils.sk_escape(query)) - opts._fzf_cli_args = string.format( "-i -c %s", act_cmd) + -- replace placeholder with the field index expression + -- if the command doesn't contain our placeholder append + -- the field index expression instead + if command:match(M.fzf_query_placeholder) then + command = opts.fn_reload:gsub(M.fzf_query_placeholder, fzf_field_expression) else - -- fzf already adds single quotes - -- around the place holder - opts.fzf_fn = {} - if opts.exec_empty_query or (query and #query>0) then - opts.fzf_fn = act_cmd:gsub(placeholder, - #query>0 and utils.lua_escape(libuv.shellescape(query)) or "''") - end - opts.fzf_opts['--phony'] = '' - opts.fzf_opts['--query'] = libuv.shellescape(query) - opts._fzf_cli_args = string.format('--bind=%s', - vim.fn.shellescape(string.format("change:reload:%s || true", act_cmd))) + command = ("%s %s"):format(command, fzf_field_expression) end - return opts - + return M.setup_fzf_interactive_flags(command, fzf_field_expression, opts) end - return M diff --git a/lua/fzf-lua/fzf.lua b/lua/fzf-lua/fzf.lua index a89e9bf..3e111e7 100644 --- a/lua/fzf-lua/fzf.lua +++ b/lua/fzf-lua/fzf.lua @@ -7,15 +7,6 @@ local uv = vim.loop local M = {} -local function get_lines_from_file(file) - local t = {} - for v in file:lines() do - table.insert(t, v) - end - return t -end - - -- workaround to a potential 'tempname' bug? (#222) -- neovim doesn't guarantee the existence of the -- parent temp dir potentially failing `mkfifo` @@ -41,21 +32,31 @@ end -- behavior. function M.raw_fzf(contents, fzf_cli_args, opts) if not coroutine.running() then - error("please run function in a coroutine") + error("[Fzf-lua] function must be called inside a coroutine.") end if not opts then opts = {} end local cwd = opts.fzf_cwd or opts.cwd - local cmd = opts.fzf_binary or opts.fzf_bin or 'fzf' + local cmd = opts.fzf_bin or 'fzf' local fifotmpname = tempname() local outputtmpname = tempname() + -- we use a temporary env $FZF_DEFAULT_COMMAND instead of piping + -- the command to fzf, this way fzf kills the command when it exits + -- this is especially important with our shell helper as io.write fails + -- to delect when the pipe is broken (EPIPE) so the neovim headless + -- 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 if contents then if type(contents) == "string" and #contents>0 then - cmd = ("%s | %s"):format(contents, cmd) + if opts.silent_fail ~= false then + contents = ("%s || true"):format(contents) + end + FZF_DEFAULT_COMMAND = contents else cmd = ("%s < %s"):format(cmd, vim.fn.shellescape(fifotmpname)) end @@ -134,11 +135,20 @@ function M.raw_fzf(contents, fzf_cli_args, opts) local co = coroutine.running() vim.fn.termopen({"sh", "-c", cmd}, { cwd = cwd, - env = { ['SHELL'] = 'sh' }, + env = { + ['SHELL'] = 'sh', + ['FZF_DEFAULT_COMMAND'] = FZF_DEFAULT_COMMAND, + ['SKIM_DEFAULT_COMMAND'] = FZF_DEFAULT_COMMAND, + }, on_exit = function(_, rc, _) + local output = {} local f = io.open(outputtmpname) - local output = get_lines_from_file(f) - f:close() + if f then + for v in f:lines() do + table.insert(output, v) + end + f:close() + end finish(1) vim.fn.delete(fifotmpname) vim.fn.delete(outputtmpname) diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua index 37ee9dc..878c2b6 100644 --- a/lua/fzf-lua/init.lua +++ b/lua/fzf-lua/init.lua @@ -155,9 +155,11 @@ M.dap_frames = require'fzf-lua.providers.dap'.frames -- API shortcuts M.fzf = require'fzf-lua.core'.fzf +M.fzf_raw = require'fzf-lua.fzf'.raw_fzf M.fzf_wrap = require'fzf-lua.core'.fzf_wrap M.fzf_exec = require'fzf-lua.core'.fzf_exec -M.raw_fzf = require'fzf-lua.fzf'.raw_fzf +M.fzf_live = require'fzf-lua.core'.fzf_live +M.defaults = config.globals -- exported modules M._exported_modules = { @@ -169,15 +171,18 @@ M._exported_modules = { 'shell', 'config', 'actions', + 'make_entry', } -- excluded from builtin / auto-complete M._excluded_meta = { 'setup', 'fzf', + 'fzf_raw', 'fzf_wrap', 'fzf_exec', - 'raw_fzf', + 'fzf_live', + 'defaults', '_excluded_meta', '_excluded_metamap', '_exported_modules', diff --git a/lua/fzf-lua/libuv.lua b/lua/fzf-lua/libuv.lua index 54fef96..273264b 100644 --- a/lua/fzf-lua/libuv.lua +++ b/lua/fzf-lua/libuv.lua @@ -418,7 +418,7 @@ M.spawn_stdio = function(opts, fn_transform, fn_preprocess) cb_err = on_err, }, fn_transform and function(x) - return fn_transform(opts, x) + return fn_transform(x, opts) end) end @@ -443,7 +443,11 @@ M.shellescape = function(s) -- replace surrounding single quote with double quotes -- temporarily replace all single quotes with double -- quotes and restore after the call to shellescape - ret = vim.fn.shellescape(s:gsub([[']], [["]])) + -- NOTE: we use '({s:gsub(...)})[1]' to extract the + -- modified string without the multival # of changes + -- otherwise the number will be sent to shellescape + -- as {special} triggering an escape for ! % and # + ret = vim.fn.shellescape(({s:gsub([[']], [["]])})[1]) ret = [["]] .. ret:gsub([["]], [[']]):sub(2, #ret-1) .. [["]] else ret = vim.fn.shellescape(s) diff --git a/lua/fzf-lua/make_entry.lua b/lua/fzf-lua/make_entry.lua index ea73f7c..26a1ebc 100644 --- a/lua/fzf-lua/make_entry.lua +++ b/lua/fzf-lua/make_entry.lua @@ -206,12 +206,12 @@ M.get_diff_files = function(opts) return diff_files end -M.glob_parse = function(opts, query) +M.glob_parse = function(query, opts) if not query or not query:find(opts.glob_separator) then return query, nil end if config.globals.grep.rg_glob_fn then - return config.globals.grep.rg_glob_fn(opts, query) + return config.globals.grep.rg_glob_fn(query, opts) end local glob_args = "" local search_query, glob_str = query:match("(.*)"..opts.glob_separator.."(.*)") @@ -245,7 +245,7 @@ M.preprocess = function(opts) -- live_grep replace pattern with last argument local argvz = "{argvz}" - local has_argvz = opts.cmd:match(argvz) + local has_argvz = opts.cmd and opts.cmd:match(argvz) -- save our last search argument for resume if opts.argv_expr and has_argvz then @@ -261,7 +261,7 @@ M.preprocess = function(opts) -- mannipulation needs to be done before the argv hack if opts.rg_glob and has_argvz then local query = argv() - local search_query, glob_args = M.glob_parse(opts, query) + local search_query, glob_args = M.glob_parse(query, opts) if glob_args then -- gsub doesn't like single % on rhs search_query = search_query:gsub("%%", "%%%%") @@ -284,7 +284,22 @@ M.preprocess = function(opts) return opts end -M.file = function(opts, x) +M.lcol = function(entry, opts) + if not entry then return nil end + local filename = entry.filename or vim.api.nvim_buf_get_name(entry.bufnr) + return string.format("%s:%s:%s:%s%s", + -- uncomment to test URIs + -- "file://" .. filename, + filename, --utils.ansi_codes.magenta(filename), + utils.ansi_codes.green(tostring(entry.lnum)), + utils.ansi_codes.blue(tostring(entry.col)), + entry.text and #entry.text>0 and " " or "", + not entry.text and "" or + (opts and opts.trim_entry and vim.trim(entry.text)) or entry.text) +end + +M.file = function(x, opts) + opts = opts or {} local ret = {} local icon, hl local file = utils.strip_ansi_coloring(string.match(x, '[^:]*')) @@ -351,7 +366,7 @@ M.file = function(opts, x) return table.concat(ret) end -M.tag = function(opts, x) +M.tag = function(x, opts) local name, file, text = x:match("([^\t]+)\t([^\t]+)\t(.*)") if not file or not name or not text then return x end text = text:match('(.*);"') or text -- remove ctag comments @@ -364,7 +379,7 @@ M.tag = function(opts, x) local line, tag = text:match("(%d-);?(/.*/)") line = line and #line>0 and tonumber(line) return ("%s%s: %s %s"):format( - M.file(opts, file), + M.file(file, opts), not line and "" or ":"..utils.ansi_codes.green(tostring(line)), utils.ansi_codes.magenta(name), utils.ansi_codes.green(tag)) diff --git a/lua/fzf-lua/previewer/fzf.lua b/lua/fzf-lua/previewer/fzf.lua index 18999ff..6c9faab 100644 --- a/lua/fzf-lua/previewer/fzf.lua +++ b/lua/fzf-lua/previewer/fzf.lua @@ -164,7 +164,6 @@ local grep_tag = function(file, tag) else utils.warn(("previewer: unable to find pattern '%s' in file '%s'"):format(pattern, file)) end - -- if line == 1 then print(cmd) end return line end diff --git a/lua/fzf-lua/providers/buffers.lua b/lua/fzf-lua/providers/buffers.lua index 8d39ad0..ca669f0 100644 --- a/lua/fzf-lua/providers/buffers.lua +++ b/lua/fzf-lua/providers/buffers.lua @@ -2,7 +2,7 @@ local core = require "fzf-lua.core" local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" -local actions = require "fzf-lua.actions" +local make_entry = require "fzf-lua.make_entry" local M = {} @@ -143,11 +143,11 @@ local function gen_buffer_entry(opts, buf, hl_curbuf) if opts.file_icons then if utils.is_term_bufname(buf.info.name) then -- get shell-like icon for terminal buffers - buficon, hl = core.get_devicon(buf.info.name, "sh") + buficon, hl = make_entry.get_devicon(buf.info.name, "sh") else local filename = path.tail(buf.info.name) local extension = path.extension(filename) - buficon, hl = core.get_devicon(filename, extension) + buficon, hl = make_entry.get_devicon(filename, extension) end if opts.color_icons then buficon = utils.ansi_codes[hl](buficon) @@ -193,13 +193,7 @@ M.buffers = function(opts) opts = core.set_fzf_field_index(opts) - core.fzf_wrap(opts, contents, function(selected) - - if not selected then return end - - actions.act(opts.actions, selected, opts) - - end)() + core.fzf_exec(contents, opts) end M.lines = function(opts) @@ -239,7 +233,7 @@ M.buffer_lines = function(opts) if opts.file_icons then local filename = path.tail(bufname) local extension = path.extension(filename) - buficon, hl = core.get_devicon(filename, extension) + buficon, hl = make_entry.get_devicon(filename, extension) if opts.color_icons then buficon = utils.ansi_codes[hl](buficon) end @@ -267,23 +261,7 @@ M.buffer_lines = function(opts) opts = core.set_fzf_field_index(opts, 3, opts._is_skim and "{}" or "{..-2}") - core.fzf_wrap(opts, items, function(selected) - if not selected then return end - - -- get the line number - local line = tonumber(selected[2]:match(":(%d+):")) - - actions.act(opts.actions, selected, opts) - - if line then - -- add current location to jumplist - local is_term = utils.is_term_buffer(0) - if not is_term then vim.cmd("normal! m`") end - vim.api.nvim_win_set_cursor(0, {line, 0}) - if not is_term then vim.cmd("norm! zz") end - end - - end)() + core.fzf_exec(items, opts) end M.tabs = function(opts) @@ -351,13 +329,7 @@ M.tabs = function(opts) opts = core.set_fzf_field_index(opts, 3, "{}") - core.fzf_wrap(opts, contents, function(selected) - - if not selected then return end - - actions.act(opts.actions, selected, opts) - - end)() + core.fzf_exec(contents, opts) end return M diff --git a/lua/fzf-lua/providers/colorschemes.lua b/lua/fzf-lua/providers/colorschemes.lua index b55d4c8..a7c6fcc 100644 --- a/lua/fzf-lua/providers/colorschemes.lua +++ b/lua/fzf-lua/providers/colorschemes.lua @@ -115,12 +115,7 @@ M.highlights = function(opts) opts.fzf_opts['--no-multi'] = '' - core.fzf_wrap(opts, contents, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(contents, opts) end return M diff --git a/lua/fzf-lua/providers/dap.lua b/lua/fzf-lua/providers/dap.lua index 96e2114..578211d 100644 --- a/lua/fzf-lua/providers/dap.lua +++ b/lua/fzf-lua/providers/dap.lua @@ -3,6 +3,7 @@ local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" local actions = require "fzf-lua.actions" +local make_entry = require "fzf-lua.make_entry" local _has_dap, _dap = nil, nil @@ -42,12 +43,7 @@ M.commands = function(opts) opts.fzf_opts['--no-multi'] = '' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end M.configurations = function(opts) @@ -83,12 +79,7 @@ M.configurations = function(opts) opts.fzf_opts['--no-multi'] = '' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end M.breakpoints = function(opts) @@ -133,14 +124,14 @@ M.breakpoints = function(opts) local contents = function (cb) local entries = {} for _, entry in ipairs(opts._locations) do - table.insert(entries, core.make_entry_lcol(opts, entry)) + table.insert(entries, make_entry.lcol(entry, opts)) end for i, x in ipairs(entries) do x = ("[%s] %s"):format( -- tostring(opts._locations[i].bufnr), utils.ansi_codes.yellow(tostring(opts._locations[i].bufnr)), - core.make_entry_file(opts, x)) + make_entry.file(x, opts)) if x then cb(x, function(err) if err then return end @@ -160,12 +151,7 @@ M.breakpoints = function(opts) opts = core.set_fzf_field_index(opts, 3, opts._is_skim and "{}" or "{..-2}") - core.fzf_wrap(opts, contents, function(selected) - - if not selected then return end - actions.act(opts.actions, selected, opts) - - end)() + core.fzf_exec(contents, opts) end @@ -197,12 +183,7 @@ M.variables = function(opts) end end - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end @@ -249,12 +230,7 @@ M.frames = function(opts) opts.fzf_opts['--no-multi'] = '' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected, opts) - - end)() + core.fzf_exec(entries, opts) end diff --git a/lua/fzf-lua/providers/files.lua b/lua/fzf-lua/providers/files.lua index 10e44d3..0000fe5 100644 --- a/lua/fzf-lua/providers/files.lua +++ b/lua/fzf-lua/providers/files.lua @@ -1,6 +1,7 @@ local core = require "fzf-lua.core" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" +local make_entry = require "fzf-lua.make_entry" local M = {} @@ -39,7 +40,7 @@ M.files = function(opts) opts.cmd = get_files_cmd(opts) local contents = core.mt_cmd_wrapper(opts) opts = core.set_header(opts, opts.headers or {"cwd"}) - return core.fzf_files(opts, contents) + return core.fzf_exec(contents, opts) end M.args = function(opts) @@ -54,7 +55,7 @@ M.args = function(opts) local contents = function (cb) local function add_entry(x, co) - x = core.make_entry_file(opts, x) + x = make_entry.file(x, opts) if not x then return end cb(x, function(err) coroutine.resume(co) @@ -89,14 +90,13 @@ M.args = function(opts) -- end; print("took", os.time()-start, "seconds.") -- done - cb(nil, function() coroutine.resume(co) end) - coroutine.yield() + cb(nil) end)() end opts = core.set_header(opts, opts.headers or {"cwd"}) - return core.fzf_files(opts, contents) + return core.fzf_exec(contents, opts) end return M diff --git a/lua/fzf-lua/providers/git.lua b/lua/fzf-lua/providers/git.lua index 19b732a..dd81bae 100644 --- a/lua/fzf-lua/providers/git.lua +++ b/lua/fzf-lua/providers/git.lua @@ -2,9 +2,9 @@ local core = require "fzf-lua.core" local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" -local actions = require "fzf-lua.actions" local libuv = require "fzf-lua.libuv" local shell = require "fzf-lua.shell" +local make_entry = require "fzf-lua.make_entry" local M = {} @@ -23,7 +23,7 @@ M.files = function(opts) if not opts.cwd then return end local contents = core.mt_cmd_wrapper(opts) opts = core.set_header(opts, opts.headers or {"cwd"}) - return core.fzf_files(opts, contents) + return core.fzf_exec(contents, opts) end M.status = function(opts) @@ -67,8 +67,8 @@ M.status = function(opts) if f1:match("%s%->%s") then f1, f2 = f1:match("(.*)%s%->%s(.*)") end - f1 = f1 and core.make_entry_file(opts, f1) - f2 = f2 and core.make_entry_file(opts, f2) + f1 = f1 and make_entry.file(f1, opts) + f2 = f2 and make_entry.file(f2, opts) local staged = git_iconify(x:sub(1,1):gsub("?", " ")) local unstaged = git_iconify(x:sub(2,2)) local entry = ("%s%s%s%s%s"):format( @@ -77,20 +77,17 @@ M.status = function(opts) return entry end, function(o) - return core.make_entry_preprocess(o) + return make_entry.preprocess(o) end) opts = core.set_header(opts, opts.headers or {"cwd"}) - return core.fzf_files(opts, contents) + return core.fzf_exec(contents, opts) end local function git_cmd(opts) opts = set_git_cwd_args(opts) if not opts.cwd then return end opts = core.set_header(opts, opts.headers or {"cwd"}) - core.fzf_wrap(opts, opts.cmd, function(selected) - if not selected then return end - actions.act(opts.actions, selected, opts) - end)() + core.fzf_exec(opts.cmd, opts) end M.commits = function(opts) diff --git a/lua/fzf-lua/providers/grep.lua b/lua/fzf-lua/providers/grep.lua index e902523..01222aa 100644 --- a/lua/fzf-lua/providers/grep.lua +++ b/lua/fzf-lua/providers/grep.lua @@ -53,7 +53,7 @@ local get_grep_cmd = function(opts, search_query, no_esc) end if opts.rg_glob then - local new_query, glob_args = make_entry.glob_parse(opts, search_query) + local new_query, glob_args = make_entry.glob_parse(search_query, opts) if glob_args then -- since the search string mixes both the query and -- glob separators we cannot used unescaped strings @@ -126,9 +126,6 @@ M.grep = function(opts) opts.search = utils.input(opts.input_prompt) or '' end - -- search query in header line - opts = core.set_header(opts, opts.headers or {"actions","cwd","search"}) - -- get the grep command before saving the last search -- incase the search string is overwritten by 'rg_glob' opts.cmd = get_grep_cmd(opts, opts.search, no_esc) @@ -163,8 +160,10 @@ M.grep = function(opts) end end + -- search query in header line + opts = core.set_header(opts, opts.headers or {"actions","cwd","search"}) opts = core.set_fzf_field_index(opts) - core.fzf_files(opts, contents) + core.fzf_exec(contents, opts) end -- single threaded version @@ -195,10 +194,7 @@ M.live_grep_st = function(opts) set_last_search(opts, opts.query, true) end - -- search query in header line - opts = core.set_header(opts, opts.headers or {"actions","cwd"}) - - opts._reload_command = function(query) + opts.fn_reload = function(query) if query and not (opts.save_last_search == false) then set_last_search(opts, query, true) end @@ -209,9 +205,13 @@ M.live_grep_st = function(opts) end if opts.requires_processing or opts.git_icons or opts.file_icons then - opts._fn_transform = opts._fn_transform - or function(x) - return core.make_entry_file(opts, x) + opts.fn_transform = opts.fn_transform or + function(x) + return make_entry.file(x, opts) + end + opts.fn_preprocess = opts.fn_preprocess or + function(o) + return make_entry.preprocess(o) end end @@ -228,13 +228,10 @@ M.live_grep_st = function(opts) end end - -- disable global resume - -- conflicts with 'change:reload' event - opts.global_resume_query = false - opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() + -- search query in header line + opts = core.set_header(opts, opts.headers or {"actions","cwd"}) opts = core.set_fzf_field_index(opts) - opts = core.set_fzf_interactive_cmd(opts) - core.fzf_files(opts) + core.fzf_exec(nil, opts) end @@ -264,29 +261,26 @@ M.live_grep_mt = function(opts) opts.search, no_esc = get_last_search(opts) end - local query = opts.search or '' + -- interactive interface uses 'query' parameter + opts.query = opts.search or '' if opts.search and #opts.search>0 then -- escape unless the user requested not to if not (no_esc or opts.no_esc) then - query = utils.rg_escape(opts.search) + opts.query = utils.rg_escape(opts.search) end -- save the search query so the use can -- call the same search again - set_last_search(opts, query, true) + set_last_search(opts, opts.query, true) end - -- search query in header line - opts = core.set_header(opts, opts.headers or {"actions","cwd"}) - -- signal to preprocess we are looking to replace {argvz} opts.argv_expr = true - -- fzf already adds single quotes around the placeholder when expanding - -- for skim we surround it with double quotes or single quote searches fail - local placeholder = utils._if(opts._is_skim, '"{}"', '{q}') - opts.cmd = get_grep_cmd(opts , placeholder, 2) - local initial_command = core.mt_cmd_wrapper(opts) - if initial_command ~= opts.cmd then + -- this will be replaced by the approperiate fzf + -- 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 -- this means mt_cmd_wrapper wrapped the command -- since now the `rg` command is wrapped inside -- the shell escaped '--headless .. --cmd' we won't @@ -297,41 +291,14 @@ M.live_grep_mt = function(opts) -- * preprocess then relaces it with vim.fn.argv(1) -- NOTE: since we cannot guarantee the positional index -- of arguments (#291) we use the last argument instead - initial_command = initial_command:gsub(placeholder, "{argvz}") - .. " " .. placeholder - end - -- by redirecting the error stream to stdout - -- we make sure a clear error message is displayed - -- when the user enters bad regex expressions - initial_command = initial_command .. " 2>&1" - local reload_command = initial_command - if not opts.exec_empty_query then - reload_command = ('[ -z %s ] || %s'):format(placeholder, reload_command) - end - if opts._is_skim then - -- skim interactive mode does not need a piped command - opts.fzf_fn = nil - opts.fzf_opts['--prompt'] = opts.prompt:match("[^%*]+") - opts.fzf_opts['--cmd-prompt'] = vim.fn.shellescape(opts.prompt) - opts.prompt = nil - -- since we surrounded the skim placeholder with quotes - -- we need to escape them in the initial query - opts.fzf_opts['--cmd-query'] = libuv.shellescape(utils.sk_escape(query)) - opts._fzf_cli_args = string.format("-i -c %s", - vim.fn.shellescape(reload_command)) - else - opts.fzf_fn = {} - if opts.exec_empty_query or (opts.search and #opts.search > 0) then - opts.fzf_fn = initial_command:gsub(placeholder, - libuv.shellescape(query:gsub("%%", "%%%%"))) - end - opts.fzf_opts['--phony'] = '' - opts.fzf_opts['--query'] = libuv.shellescape(query) - opts._fzf_cli_args = string.format('--bind=%s', - vim.fn.shellescape(("change:reload:%s"):format( - ("%s || true"):format(reload_command)))) + command = command:gsub(core.fzf_query_placeholder, "{argvz}") + .. " " .. core.fzf_query_placeholder end + -- signal 'fzf_exec' to set 'change:reload' parameters + -- or skim's "interactive" mode (AKA "live query") + opts.fn_reload = command + -- when running 'live_grep' with 'exec_empty_query=false' (default) -- an empty typed query will not be saved as the 'neovim --headless' -- command isn't executed resulting in '_last_search.query' never @@ -359,13 +326,10 @@ M.live_grep_mt = function(opts) end end - -- disable global resume - -- conflicts with 'change:reload' event - opts.global_resume_query = false - opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() + -- search query in header line + opts = core.set_header(opts, opts.headers or {"actions","cwd"}) opts = core.set_fzf_field_index(opts) - core.fzf_files(opts) - opts.search = nil + core.fzf_exec(nil, opts) end M.live_grep_glob_st = function(opts) @@ -399,18 +363,12 @@ end M.live_grep_native = function(opts) -- backward compatibility, by setting git|files icons to false - -- we forces mt_cmd_wrapper to pipe the command as is so fzf + -- we force 'mt_cmd_wrapper' to pipe the command as is so fzf -- runs the command directly in the 'change:reload' event opts = opts or {} opts.git_icons = false opts.file_icons = false opts.rg_glob = false - -- disable ctrl-g switch by default - if not opts.actions or not opts.actions["ctrl-g"] then - opts.actions = opts.actions or {} - opts.actions["ctrl-g"] = false - end - opts.__FNCREF__ = utils.__FNCREF__() return M.live_grep_mt(opts) end @@ -418,8 +376,6 @@ M.live_grep = function(opts) opts = config.normalize_opts(opts, config.globals.grep) if not opts then return end - opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() - if opts.multiprocess then return M.live_grep_mt(opts) else @@ -431,8 +387,6 @@ M.live_grep_glob = function(opts) opts = config.normalize_opts(opts, config.globals.grep) if not opts then return end - opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() - if opts.multiprocess then return M.live_grep_glob_mt(opts) else diff --git a/lua/fzf-lua/providers/helptags.lua b/lua/fzf-lua/providers/helptags.lua index 82ef68f..430b839 100644 --- a/lua/fzf-lua/providers/helptags.lua +++ b/lua/fzf-lua/providers/helptags.lua @@ -2,12 +2,11 @@ local path = require "fzf-lua.path" local core = require "fzf-lua.core" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" -local actions = require "fzf-lua.actions" local M = {} -local fzf_function = function (cb) +local fzf_fn = function (cb) local opts = {} opts.lang = config.globals.helptags.lang or vim.o.helplang opts.fallback = utils._if(config.globals.helptags.fallback ~= nil, config.globals.helptags.fallback, true) @@ -82,11 +81,7 @@ local fzf_function = function (cb) end end end - -- done, we can't call utils.delayed_cb here - -- because sleep() messes up the coroutine - -- cb(nil, function() coroutine.resume(co) end) - utils.delayed_cb(cb, function() coroutine.resume(co) end) - coroutine.yield() + cb(nil) end)() end @@ -96,18 +91,10 @@ M.helptags = function(opts) opts = config.normalize_opts(opts, config.globals.helptags) if not opts then return end - -- local prev_act = action(function (args) end) - opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview-window'] = 'hidden:right:0' - core.fzf_wrap(opts, fzf_function, function(selected) - - if not selected then return end - - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(fzf_fn, opts) end diff --git a/lua/fzf-lua/providers/lsp.lua b/lua/fzf-lua/providers/lsp.lua index 8814ead..3f8a9dd 100644 --- a/lua/fzf-lua/providers/lsp.lua +++ b/lua/fzf-lua/providers/lsp.lua @@ -1,7 +1,7 @@ local core = require "fzf-lua.core" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" -local actions = require "fzf-lua.actions" +local make_entry = require "fzf-lua.make_entry" local M = {} @@ -39,8 +39,8 @@ local function location_handler(opts, cb, _, result, ctx, _) for _, entry in ipairs(items) do if not opts.current_buffer_only or vim.api.nvim_buf_get_name(opts.bufnr) == entry.filename then - entry = core.make_entry_lcol(opts, entry) - entry = core.make_entry_file(opts, entry) + entry = make_entry.lcol(entry, opts) + entry = make_entry.file(entry, opts) if entry then cb(entry, function(err) if err then return end @@ -61,8 +61,8 @@ local function call_hierarchy_handler(opts, cb, _, result, _, _) lnum = range.start.line + 1, col = range.start.character + 1, } - entry = core.make_entry_lcol(opts, entry) - entry = core.make_entry_file(opts, entry) + entry = make_entry.lcol(entry, opts) + entry = make_entry.file(entry, opts) if entry then cb(entry, function(err) if err then return end @@ -88,8 +88,8 @@ local function symbol_handler(opts, cb, _, result, _, _) entry.text = entry.text:gsub("%[.-%]", M._sym2style[kind]) end end - entry = core.make_entry_lcol(opts, entry) - entry = core.make_entry_file(opts, entry) + entry = make_entry.lcol(entry, opts) + entry = make_entry.file(entry, opts) if entry then cb(entry, function(err) if err then return end @@ -121,10 +121,10 @@ local function code_action_handler(opts, cb, _, code_actions, context, _) end end -local function diagnostics_handler(opts, cb, _, entry) +local function diagnostics_handler(opts, cb, co, entry) local type = entry.type - entry = core.make_entry_lcol(opts, entry) - entry = core.make_entry_file(opts, entry) + entry = make_entry.lcol(entry, opts) + entry = make_entry.file(entry, opts) if not entry then return end if opts.lsp_icons and opts._severity_icons[type] then local severity = opts._severity_icons[type] @@ -134,9 +134,7 @@ local function diagnostics_handler(opts, cb, _, entry) end entry = icon .. utils.nbsp .. utils.nbsp .. entry end - cb(entry, function(err) - if err then return end - end) + cb(entry, function() coroutine.resume(co) end) end -- see neovim #15504 @@ -181,7 +179,7 @@ local function wrap_handler(handler, opts, cb, co) if opts.num_callbacks == opts.num_clients then -- close the pipe to fzf, this -- removes the loading indicator in fzf - utils.delayed_cb(cb) + cb(nil) end end return ret @@ -275,15 +273,13 @@ local function set_lsp_fzf_fn(opts) -- cancel all remaining LSP requests -- once the user made their selection -- or closed the fzf popup - opts.post_select_cb = function() + opts._fn_post_fzf = function() if opts._cancel_all then opts._cancel_all() opts._cancel_all = nil end end - -- coroutine.yield() - end)() end @@ -303,7 +299,6 @@ local normalize_lsp_opts = function(opts, cfg) opts = config.normalize_opts(opts, cfg) if not opts then return end - if not opts.cwd then opts.cwd = vim.loop.cwd() end if not opts.prompt and opts.prompt_postfix then opts.prompt = opts.lsp_handler.label .. (opts.prompt_postfix or '') end @@ -327,7 +322,7 @@ local function fzf_lsp_locations(opts) if opts.force_uri == nil then opts.force_uri = true end opts = set_lsp_fzf_fn(opts) if not opts.fzf_fn then return end - return core.fzf_files(opts) + return core.fzf_exec(opts.fzf_fn, opts) end -- define the functions for wrap_module_fncs @@ -441,7 +436,7 @@ M.document_symbols = function(opts) opts.fn_pre_fzf = function() gen_sym2style_map(opts) end opts.fn_post_fzf = function() M._sym2style = nil end end - return core.fzf_files(opts) + return core.fzf_exec(opts.fzf_fn, opts) end M.workspace_symbols = function(opts) @@ -458,7 +453,7 @@ M.workspace_symbols = function(opts) opts.fn_pre_fzf = function() gen_sym2style_map(opts) end opts.fn_post_fzf = function() M._sym2style = nil end end - return core.fzf_files(opts) + return core.fzf_exec(opts.fzf_fn, opts) end -- Converts 'vim.diagnostic.get' to legacy style 'get_line_diagnostics()' @@ -627,17 +622,7 @@ M.code_actions = function(opts) opts.fzf_opts["--no-multi"] = '' opts.fzf_opts["--preview-window"] = 'right:0' - core.fzf_wrap(opts, opts.fzf_fn, function(selected) - - if opts.post_select_cb then - opts.post_select_cb() - end - - if not selected then return end - - actions.act(opts.actions, selected, opts) - - end)() + core.fzf_exec(opts.fzf_fn, opts) end @@ -761,7 +746,7 @@ M.diagnostics = function(opts) return buffer_diag end - opts.fzf_fn = function (cb) + opts.fzf_fn = function (fzf_cb) coroutine.wrap(function () local co = coroutine.running() @@ -771,8 +756,13 @@ M.diagnostics = function(opts) -- empty tables for unused buffers if not vim.tbl_isempty(diag) then if filter_diag_severity(opts, diag.severity) then - diagnostics_handler(opts, cb, co, - preprocess_diag(diag, bufnr)) + -- wrap with 'vim.scheudle' or calls to vim.{fn|api} fail: + -- E5560: vimL function must not be called in a lua loop callback + vim.schedule(function() + diagnostics_handler(opts, fzf_cb, co, preprocess_diag(diag, bufnr)) + end) + -- wait here for 'diagnostics_handler' to return + coroutine.yield() end end end @@ -787,14 +777,14 @@ M.diagnostics = function(opts) end -- close the pipe to fzf, this -- removes the loading indicator - cb(nil) + fzf_cb(nil) end)() end opts = core.set_header(opts, opts.headers or {"cwd"}) opts = core.set_fzf_field_index(opts) if opts.force_uri == nil then opts.force_uri = true end - return core.fzf_files(opts) + return core.fzf_exec(opts.fzf_fn, opts) end M.workspace_diagnostics = function(opts) @@ -826,7 +816,7 @@ M.live_workspace_symbols = function(opts) opts.bufnr = vim.api.nvim_get_current_buf() opts.winid = vim.api.nvim_get_current_win() - opts._reload_action = function(query) + opts.fn_reload = function(query) if query and not (opts.save_last_query == false) then last_search = { query = query } config.__resume_data.last_query = query @@ -838,19 +828,13 @@ M.live_workspace_symbols = function(opts) return opts.fzf_fn end - -- disable global resume - -- conflicts with 'change:reload' event - opts.global_resume_query = false - opts.__FNCREF__ = M.live_workspace_symbols - opts = core.set_fzf_interactive_cb(opts) opts = core.set_fzf_field_index(opts) if opts.force_uri == nil then opts.force_uri = true end if opts.symbol_style or opts.symbol_fmt then opts.fn_pre_fzf = function() gen_sym2style_map(opts) end opts.fn_post_fzf = function() M._sym2style = nil end end - core.fzf_files(opts) - opts.search = nil + core.fzf_exec(nil, opts) end local function check_capabilities(feature) diff --git a/lua/fzf-lua/providers/manpages.lua b/lua/fzf-lua/providers/manpages.lua index 399863e..43c5e14 100644 --- a/lua/fzf-lua/providers/manpages.lua +++ b/lua/fzf-lua/providers/manpages.lua @@ -1,60 +1,25 @@ local core = require "fzf-lua.core" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" -local actions = require "fzf-lua.actions" -local libuv = require "fzf-lua.libuv" - local M = {} -M.getmanpage = function(line) - --[[ -- extract section from the last pair of parentheses - local name, section = line:match("^(.*)%((.-)%)[^()]-$") - if name:sub(-1) == " " then - -- man-db - name = name:sub(1, -2) - else - -- mandoc - name = name:match("^[^, ]+") - section = section:match("^[^, ]+") - end - return name .. "(" .. section .. ")" ]] - return line:match("[^[,( ]+") -end - M.manpages = function(opts) opts = config.normalize_opts(opts, config.globals.manpages) if not opts then return end - local fzf_fn = libuv.spawn_nvim_fzf_cmd( - { cmd = opts.cmd, cwd = opts.cwd, pid_cb = opts._pid_cb }, - function(x) - -- split by first occurence of ' - ' (spaced hyphen) - local man, desc = x:match("^(.-) %- (.*)$") - return string.format("%-45s %s", - utils.ansi_codes.magenta(man), desc) - end) + opts.fn_transform = function(x) + -- split by first occurence of ' - ' (spaced hyphen) + local man, desc = x:match("^(.-) %- (.*)$") + return string.format("%-45s %s", + utils.ansi_codes.magenta(man), desc) + end opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview-window'] = 'hidden:right:0' - opts.fzf_opts['--tiebreak'] = 'begin' - opts.fzf_opts['--nth'] = '1,2' - - core.fzf_wrap(opts, fzf_fn, function(selected) - - if not selected then return end - - if #selected > 1 then - for i = 2, #selected do - selected[i] = M.getmanpage(selected[i]) - end - end - - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(opts.cmd, opts) end return M diff --git a/lua/fzf-lua/providers/module.lua b/lua/fzf-lua/providers/module.lua index de9d45f..1fa6e40 100644 --- a/lua/fzf-lua/providers/module.lua +++ b/lua/fzf-lua/providers/module.lua @@ -1,7 +1,6 @@ local core = require "fzf-lua.core" local shell = require "fzf-lua.shell" local config = require "fzf-lua.config" -local actions = require "fzf-lua.actions" local M = {} @@ -35,13 +34,7 @@ M.metatable = function(opts) -- as the behavior might confuse users (#267) opts.global_resume = false - core.fzf_wrap(opts, methods, function(selected) - - if not selected then return end - - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(methods, opts) end diff --git a/lua/fzf-lua/providers/nvim.lua b/lua/fzf-lua/providers/nvim.lua index a8e1ec0..9d3a1f2 100644 --- a/lua/fzf-lua/providers/nvim.lua +++ b/lua/fzf-lua/providers/nvim.lua @@ -3,7 +3,7 @@ local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" local shell = require "fzf-lua.shell" local config = require "fzf-lua.config" -local actions = require "fzf-lua.actions" +local make_entry = require "fzf-lua.make_entry" local M = {} @@ -40,12 +40,7 @@ M.commands = function(opts) opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview'] = prev_act - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end @@ -64,12 +59,7 @@ local history = function(opts, str) opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview-window'] = 'hidden:right:0' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end local arg_header = function(sel_key, edit_key, text) @@ -119,12 +109,7 @@ M.jumps = function(opts) opts.fzf_opts['--no-multi'] = '' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected, opts) - - end)() + core.fzf_exec(entries, opts) end M.tagstack = function(opts) @@ -154,12 +139,13 @@ M.tagstack = function(opts) local entries = {} for i, tag in ipairs(tags) do - local bufname = tag.filename + local bufname = path.HOME_to_tilde( + path.relative(tag.filename, vim.loop.cwd())) local buficon, hl if opts.file_icons then local filename = path.tail(bufname) local extension = path.extension(filename) - buficon, hl = core.get_devicon(filename, extension) + buficon, hl = make_entry.get_devicon(filename, extension) if opts.color_icons then buficon = utils.ansi_codes[hl](buficon) end @@ -181,12 +167,7 @@ M.tagstack = function(opts) opts.fzf_opts['--no-multi'] = '' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected, opts) - - end)() + core.fzf_exec(entries, opts) end @@ -226,12 +207,7 @@ M.marks = function(opts) -- opts.fzf_opts['--preview'] = prev_act opts.fzf_opts['--no-multi'] = '' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end M.registers = function(opts) @@ -285,12 +261,7 @@ M.registers = function(opts) opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview'] = prev_act - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end M.keymaps = function(opts) @@ -343,12 +314,7 @@ M.keymaps = function(opts) opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview'] = prev_act - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end M.spell_suggest = function(opts) @@ -365,12 +331,7 @@ M.spell_suggest = function(opts) opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview-window'] = 'hidden:right:0' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end @@ -385,12 +346,7 @@ M.filetypes = function(opts) opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview-window'] = 'hidden:right:0' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end @@ -406,12 +362,7 @@ M.packadd = function(opts) opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview-window'] = 'hidden:right:0' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end @@ -448,12 +399,7 @@ M.menus = function(opts) opts.fzf_opts['--no-multi'] = '' opts.fzf_opts['--preview-window'] = 'hidden:right:0' - core.fzf_wrap(opts, entries, function(selected) - - if not selected then return end - actions.act(opts.actions, selected) - - end)() + core.fzf_exec(entries, opts) end diff --git a/lua/fzf-lua/providers/oldfiles.lua b/lua/fzf-lua/providers/oldfiles.lua index 6b4f439..d09b4fa 100644 --- a/lua/fzf-lua/providers/oldfiles.lua +++ b/lua/fzf-lua/providers/oldfiles.lua @@ -1,5 +1,6 @@ local core = require "fzf-lua.core" local config = require "fzf-lua.config" +local make_entry = require "fzf-lua.make_entry" local M = {} @@ -29,14 +30,14 @@ M.oldfiles = function(opts) local contents = function (cb) local function add_entry(x, co) - x = core.make_entry_file(opts, x) + x = make_entry.file(x, opts) if not x then return end cb(x, function(err) coroutine.resume(co) if err then -- close the pipe to fzf, this -- removes the loading indicator in fzf - cb(nil, function() end) + cb(nil) end end) coroutine.yield() @@ -60,14 +61,13 @@ M.oldfiles = function(opts) -- end; print("took", os.time()-start, "seconds.") -- done - cb(nil, function() coroutine.resume(co) end) - coroutine.yield() + cb(nil) end)() end opts = core.set_header(opts, opts.headers or {"cwd"}) - return core.fzf_files(opts, contents) + return core.fzf_exec(contents, opts) end return M diff --git a/lua/fzf-lua/providers/quickfix.lua b/lua/fzf-lua/providers/quickfix.lua index 8aa2a93..02f8769 100644 --- a/lua/fzf-lua/providers/quickfix.lua +++ b/lua/fzf-lua/providers/quickfix.lua @@ -1,6 +1,7 @@ local core = require "fzf-lua.core" local utils = require "fzf-lua.utils" local config = require "fzf-lua.config" +local make_entry = require "fzf-lua.make_entry" local M = {} @@ -14,12 +15,12 @@ local quickfix_run = function(opts, cfg, locations) if not opts.cwd then opts.cwd = vim.loop.cwd() end for _, entry in ipairs(locations) do - table.insert(results, core.make_entry_lcol(opts, entry)) + table.insert(results, make_entry.lcol(entry, opts)) end local contents = function(cb) for _, x in ipairs(results) do - x = core.make_entry_file(opts, x) + x = make_entry.file(x, opts) if x then cb(x, function(err) if err then return end @@ -29,11 +30,11 @@ local quickfix_run = function(opts, cfg, locations) end) end end - utils.delayed_cb(cb) + cb(nil) end opts = core.set_fzf_field_index(opts) - return core.fzf_files(opts, contents) + return core.fzf_exec(contents, opts) end M.quickfix = function(opts) diff --git a/lua/fzf-lua/providers/tags.lua b/lua/fzf-lua/providers/tags.lua index e07c6be..91a0022 100644 --- a/lua/fzf-lua/providers/tags.lua +++ b/lua/fzf-lua/providers/tags.lua @@ -76,7 +76,7 @@ local function tags(opts) }) local ok, lines, err = pcall(utils.io_systemlist, cmd) if ok and err == 0 and lines and not vim.tbl_isempty(lines) then - local tag, line = make_entry.tag(opts, lines[1]) + local tag, line = make_entry.tag(lines[1], opts) if tag and not line then -- tags file does not contain lines -- remove preview offset field index @@ -87,8 +87,11 @@ local function tags(opts) -- prevents 'file|git_icons=false' from overriding processing opts.requires_processing = true - opts._fn_transform = make_entry.tag -- multiprocess=false - opts._fn_transform_str = [[return require("make_entry").tag]] -- multiprocess=true + if opts.multiprocess then + opts.__mt_transform = [[return require("make_entry").tag]] + else + opts.__mt_transform = make_entry.tag + end if opts.lgrep then -- live_grep requested by caller ('tags_live_grep') @@ -101,9 +104,9 @@ local function tags(opts) if opts.multiprocess then return require'fzf-lua.providers.grep'.live_grep_mt(opts) else - -- 'live_grep_st' uses different signature '_fn_transform' - opts._fn_transform = function(x) - return make_entry.tag(opts, x) + -- 'live_grep_st' uses different signature 'fn_transform' + opts.fn_transform = function(x) + return make_entry.tag(x, opts) end return require'fzf-lua.providers.grep'.live_grep_st(opts) end @@ -159,7 +162,6 @@ M.live_grep = function(opts) opts = config.normalize_opts(opts, config.globals.tags) if not opts then return end opts.lgrep = true - opts.__FNCREF__ = utils.__FNCREF__() return tags(opts) end diff --git a/lua/fzf-lua/shell.lua b/lua/fzf-lua/shell.lua index 2917d5e..e2b7347 100644 --- a/lua/fzf-lua/shell.lua +++ b/lua/fzf-lua/shell.lua @@ -37,9 +37,14 @@ function M.raw_async_action(fn, fzf_field_expression, debug) local pipe = uv.new_pipe(false) local args = {...} uv.pipe_connect(pipe, pipe_path, function(err) - vim.schedule(function () - fn(pipe, unpack(args)) - end) + if err then + error(string.format("pipe_connect(%s) failed with error: %s", + pipe_path, err)) + else + vim.schedule(function () + fn(pipe, unpack(args)) + end) + end end) end @@ -131,45 +136,134 @@ end M.reload_action_cmd = function(opts, fzf_field_expression) - local _pid = nil + if opts.fn_preprocess and type(opts.fn_preprocess) == 'function' then + -- run the preprocessing fn + opts = vim.tbl_deep_extend("keep", opts, opts.fn_preprocess(opts)) + end return M.raw_async_action(function(pipe, args) - local function on_pid(pid) - _pid = pid - if opts.pid_cb then - opts.pid_cb(pid) - end - end - - local function on_finish(_, _) - if pipe and not uv.is_closing(pipe) then + -- get the type of contents from the caller + local reload_contents = opts.__fn_reload(args[1]) + local write_cb_count = 0 + local pipe_want_close = false + + -- local on_finish = function(code, sig, from, pid) + -- print("finish", pipe, pipe_want_close, code, sig, from, pid) + local on_finish = function(_, _, _, _) + if not pipe then return end + pipe_want_close = true + if write_cb_count==0 then + -- only close if all our uv.write calls are completed uv.close(pipe) pipe = nil end end - local function on_write(data, cb) - if not pipe then - cb(true) + local on_write = function(data, cb, co) + -- pipe can be nil when using a shell command with spawn + -- and typing quickly, the process will terminate and + assert(not co or (co and pipe and not uv.is_closing(pipe))) + if not pipe then return end + if not data then + on_finish(nil, nil, 5) + if cb then cb(nil) end else - uv.write(pipe, data, cb) + write_cb_count = write_cb_count + 1 + uv.write(pipe, tostring(data), function(err) + write_cb_count = write_cb_count - 1 + if co then coroutine.resume(co) end + if cb then cb(err) end + if err then + -- force close on error + write_cb_count = 0 + on_finish(nil, nil, 2) + end + if write_cb_count == 0 and pipe_want_close then + on_finish(nil, nil, 3) + end + end) + -- yield returns when uv.write compeletes + -- or when a new coroutine calls resume(1) + if co and coroutine.yield() == 1 then + -- we have a new routine in opts.__co, this + -- routine is no longer relevant so kill it + write_cb_count = 0 + on_finish(nil, nil, 4) + end end end - -- terminate previously running commands - libuv.process_kill(_pid) + if type(reload_contents) == 'string' then + -- string will be used as a shell command + -- terminate previously running commands + libuv.process_kill(opts.__pid) + opts.__pid = nil - -- return libuv.spawn({ - return libuv.async_spawn({ + -- spawn/async_spawn already async, no need to send opts.__co + -- also, we can't call coroutine.yield inside a libuv callback + -- due to: "attempt to yield across C-call boundary" + libuv.async_spawn({ cwd = opts.cwd, - cmd = opts._reload_command(args[1]), + cmd = reload_contents, cb_finish = on_finish, cb_write = on_write, - cb_pid = on_pid, + cb_pid = function(pid) opts.__pid = pid end, -- must send false, 'coroutinify' adds callback as last argument -- which will conflict with the 'fn_transform' argument - }, opts._fn_transform or false) + }, opts.__fn_transform or false) + else + -- table or function runs in a coroutine + -- which isn't required for 'libuv.spawn' + coroutine.wrap(function() + + if opts.__co then + local costatus = coroutine.status(opts.__co) + if costatus ~= 'dead' then + -- the previous routine is either 'running' or 'suspended' + -- return 1 from yield to signal abort to 'on_write' + coroutine.resume(opts.__co, 1) + end + assert(coroutine.status(opts.__co) == 'dead') + end + -- reset var to current running routine + opts.__co = coroutine.running() + + -- callback with newline + local on_write_nl = function(data, cb) + data = data and tostring(data) .. "\n" or nil + return on_write(data, cb) + end + + -- callback with newline and coroutine + local on_write_nl_co = function(data, cb) + data = data and tostring(data) .. "\n" or nil + return on_write(data, cb, opts.__co) + end + + -- callback with coroutine (no NL) + local on_write_co = function(data, cb) + return on_write(data, cb, opts.__co) + end + + + if type(reload_contents) == 'table' then + for _, l in ipairs(reload_contents) do + on_write_nl_co(l) + end + on_finish() + elseif type(reload_contents) == 'function' then + -- by default we use the async callbacks + if opts.func_async_callback ~= false then + reload_contents(on_write_nl_co, on_write_co) + else + reload_contents(on_write_nl, on_write) + end + else + end + + end)() + end end, fzf_field_expression, opts.debug) end diff --git a/lua/fzf-lua/utils.lua b/lua/fzf-lua/utils.lua index 6f9c91c..e51d165 100644 --- a/lua/fzf-lua/utils.lua +++ b/lua/fzf-lua/utils.lua @@ -413,17 +413,6 @@ function M.feed_keys_termcodes(key) vim.api.nvim_replace_termcodes(key, true, false, true), 'n', true) end -function M.delayed_cb(cb, fn) - -- HACK: slight delay to prevent missing results - -- otherwise the input stream closes too fast - -- sleep was causing all sorts of issues - -- vim.cmd("sleep! 10m") - if fn == nil then fn = function() end end - vim.defer_fn(function() - cb(nil, fn) - end, 20) -end - function M.is_term_bufname(bufname) if bufname and bufname:match("term://") then return true end return false @@ -590,8 +579,8 @@ function M.io_systemlist(cmd, use_lua_io) -- last line contains the exit status rc = tonumber(stdout[#stdout]) stdout[#stdout] = nil + handle:close() end - handle:close() return stdout, rc else return vim.fn.systemlist(cmd), vim.v.shell_error diff --git a/lua/fzf-lua/win.lua b/lua/fzf-lua/win.lua index 5247891..8e555c7 100644 --- a/lua/fzf-lua/win.lua +++ b/lua/fzf-lua/win.lua @@ -29,7 +29,9 @@ function FzfWin.save_query(key) local query = lines[1]:gsub("^%*+", "") :gsub("^"..utils.lua_escape(self.prompt:match("[^%*]+")), "") -- remove '--info=inline' - query = query and query:gsub("<%s%d+/%d+.*$", "") + query = query and query:gsub("[<%-]%s%d+/%d+.*$", "") + -- remove '< [Command failed: ...] + query = query and query:gsub("<%s%[Command failed:.*$", "") -- trim whitespaces at the end query = query and query:gsub("%s*$", "") if self.fn_save_query then