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 function get_last_search() local last_search = config.globals.grep._last_search or {} return last_search.query, last_search.no_esc end local function set_last_search(query, no_esc) config.globals.grep._last_search = { query = query, no_esc = no_esc } if config.__resume_data then config.__resume_data.last_query = query end end local M = {} local get_grep_cmd = function(opts, search_query, no_esc) if opts.cmd_fn and type(opts.cmd_fn) == 'function' then return opts.cmd_fn(opts, search_query, no_esc) end if opts.raw_cmd and #opts.raw_cmd>0 then return opts.raw_cmd end local command = nil if opts.cmd and #opts.cmd > 0 then command = opts.cmd elseif vim.fn.executable("rg") == 1 then command = string.format("rg %s", opts.rg_opts) else command = string.format("grep %s", opts.grep_opts) end -- filename takes precedence over directory -- filespec takes precedence over all and doesn't shellescape -- this is so user can send a file populating command instead local search_path = '' if opts.filespec and #opts.filespec>0 then search_path = opts.filespec elseif opts.filename and #opts.filename>0 then search_path = vim.fn.shellescape(opts.filename) end search_query = search_query or '' if not (no_esc or opts.no_esc) then search_query = utils.rg_escape(search_query) end -- remove column numbers when search term is empty if not opts.no_column_hide and #search_query==0 then command = command:gsub("%s%-%-column", "") end -- do not escape at all if not (no_esc == 2 or opts.no_esc == 2) then search_query = vim.fn.shellescape(search_query) end return string.format('%s %s %s', command, search_query, search_path) end M.grep = function(opts) opts = config.normalize_opts(opts, config.globals.grep) if not opts then return end local no_esc = false if opts.continue_last_search or opts.repeat_last_search then opts.search, no_esc = get_last_search() end -- if user did not provide a search term -- provide an input prompt if not opts.search then opts.search = vim.fn.input(opts.input_prompt) or '' end --[[ if not opts.search or #opts.search == 0 then utils.info("Please provide a valid search string") return end ]] -- search query in header line opts = core.set_header(opts) -- save the search query so the use can -- call the same search again set_last_search(opts.search, no_esc or opts.no_esc) opts.cmd = get_grep_cmd(opts, opts.search, no_esc) local contents = core.mt_cmd_wrapper(opts) -- by redirecting the error stream to stdout -- we make sure a clear error message is displayed -- when the user enters bad regex expressions if type(contents) == 'string' then contents = contents .. " 2>&1" end opts = core.set_fzf_line_args(opts) core.fzf_files(opts, contents) opts.search = nil end -- single threaded version M.live_grep_st = function(opts) opts = config.normalize_opts(opts, config.globals.grep) if not opts then return end local no_esc = false if opts.continue_last_search or opts.repeat_last_search then opts.search, no_esc = get_last_search() end opts.query = opts.search or '' if opts.search and #opts.search>0 then -- save the search query so the use can -- call the same search again set_last_search(opts.search, true) -- escape unless the user requested not to if not (no_esc or opts.no_esc) then opts.query = utils.rg_escape(opts.search) end end -- search query in header line opts = core.set_header(opts, 2) opts._reload_command = function(query) if query and not (opts.save_last_search == false) then set_last_search(query, true) end -- can be nill when called as fzf initial command query = query or '' -- TODO: need to empty filespec -- fix this collision, rename to _filespec opts.no_esc = nil opts.filespec = nil return get_grep_cmd(opts, query, true) end if opts.experimental and (opts.git_icons or opts.file_icons) then opts._fn_transform = function(x) return core.make_entry_file(opts, x) end end -- disable global resume -- conflicts with 'change:reload' event opts.global_resume_query = false opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() opts = core.set_fzf_line_args(opts) opts = core.set_fzf_interactive_cmd(opts) core.fzf_files(opts) 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 -- runs the command directly in the 'change:reload' event opts = opts or {} opts.git_icons = false opts.file_icons = false opts.__FNCREF__ = utils.__FNCREF__() return M.live_grep_mt(opts) end M.live_grep_glob_mt = function(opts) if vim.fn.executable("rg") ~= 1 then utils.warn("'--glob|iglob' flags requires 'rg' (https://github.com/BurntSushi/ripgrep)") return end -- 'rg_glob = true' enables the glob processsing in -- 'make_entry.preprocess', only supported with multiprocess opts = opts or {} opts.rg_glob = true opts.force_multiprocess = true opts.__FNCREF__ = utils.__FNCREF__() return M.live_grep_mt(opts) end -- multi threaded (multi-process actually) version M.live_grep_mt = function(opts) opts = config.normalize_opts(opts, config.globals.grep) if not opts then return end local no_esc = false if opts.continue_last_search or opts.repeat_last_search then opts.search, no_esc = get_last_search() end local query = opts.search or '' if opts.search and #opts.search>0 then -- save the search query so the use can -- call the same search again set_last_search(opts.search, no_esc or opts.no_esc) -- escape unless the user requested not to if not (no_esc or opts.no_esc) then query = utils.rg_escape(opts.search) end end -- search query in header line opts = core.set_header(opts, 2) -- since the introduction of 'libuv.spawn_stdio' with '--headless' -- we can now run the command externally with minimal overhead if not opts.multiprocess and not opts.force_multiprocess then opts.git_icons = false opts.file_icons = false end -- 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 means mt_cmd_wrapper wrapped the command -- since now the `rg` command is wrapped inside -- the shell escaped '--headless .. --cmd' we won't -- be able to search single quotes as it will break -- the escape sequence so we use a nifty trick -- * replace the placeholder with {argv1} -- * re-add the placeholder at the end of the command -- * preprocess then 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 opts.fzf_opts['--cmd-prompt'] = vim.fn.shellescape(opts.prompt) -- since we surrounded the skim placeholder with quotes -- we need to escape them in the initial query opts.fzf_opts['--cmd-query'] = vim.fn.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, vim.fn.shellescape(query)) end opts.fzf_opts['--phony'] = '' opts.fzf_opts['--query'] = vim.fn.shellescape(query) opts._fzf_cli_args = string.format('--bind=%s', vim.fn.shellescape(("change:reload:%s"):format( ("%s || true"):format(reload_command)))) end -- disable global resume -- conflicts with 'change:reload' event opts.global_resume_query = false opts.__FNCREF__ = opts.__FNCREF__ or utils.__FNCREF__() opts = core.set_fzf_line_args(opts) core.fzf_files(opts) opts.search = nil end M.live_grep_resume = function(opts) if not opts then opts = {} end if not opts.search then opts.continue_last_search = (opts.continue_last_search == nil and opts.repeat_last_search == nil and true) or (opts.continue_last_search or opts.repeat_last_search) end return M.live_grep_mt(opts) end M.live_grep_glob = function(opts) if not opts then opts = {} end if vim.fn.executable("rg") ~= 1 then utils.warn("'--glob|iglob' flags requires 'rg' (https://github.com/BurntSushi/ripgrep)") return end opts.cmd_fn = function(o, query, no_esc) local glob_arg, glob_str = "", "" local search_query = query or "" if query:find(o.glob_separator) then search_query, glob_str = query:match("(.*)"..o.glob_separator.."(.*)") for _, s in ipairs(utils.strsplit(glob_str, "%s")) do glob_arg = glob_arg .. (" %s %s") :format(o.glob_flag, vim.fn.shellescape(s)) end end -- copied over from get_grep_cmd local search_path = '' if o.filespec and #o.filespec>0 then search_path = o.filespec elseif o.filename and #o.filename>0 then search_path = vim.fn.shellescape(o.filename) end if not (no_esc or o.no_esc) then search_query = utils.rg_escape(search_query) end -- do not escape at all if not (no_esc == 2 or o.no_esc == 2) then search_query = vim.fn.shellescape(search_query) end local cmd = ("rg %s %s -- %s %s") :format(o.rg_opts, glob_arg, search_query, search_path) return cmd end opts.__FNCREF__ = utils.__FNCREF__() return M.live_grep_st(opts) end M.grep_last = function(opts) if not opts then opts = {} end opts.continue_last_search = true return M.grep(opts) end M.grep_cword = function(opts) if not opts then opts = {} end opts.search = vim.fn.expand("") return M.grep(opts) end M.grep_cWORD = function(opts) if not opts then opts = {} end opts.search = vim.fn.expand("") return M.grep(opts) end M.grep_visual = function(opts) if not opts then opts = {} end opts.search = utils.get_visual_selection() return M.grep(opts) end M.grep_project = function(opts) if not opts then opts = {} end if not opts.search then opts.search = '' end -- by default, do not include filename in search if not opts.fzf_opts or opts.fzf_opts["--nth"] == nil then opts.fzf_opts = opts.fzf_opts or {} opts.fzf_opts["--nth"] = '2..' end return M.grep(opts) end M.grep_curbuf = function(opts) if not opts then opts = {} end opts.rg_opts = config.globals.grep.rg_opts .. " --with-filename" opts.grep_opts = config.globals.grep.grep_opts .. " --with-filename" if opts.exec_empty_query == nil then opts.exec_empty_query = true end opts.fzf_opts = vim.tbl_extend("keep", opts.fzf_opts or {}, config.globals.blines.fzf_opts) opts.filename = vim.api.nvim_buf_get_name(0) if #opts.filename > 0 and vim.loop.fs_stat(opts.filename) then opts.filename = path.relative(opts.filename, vim.loop.cwd()) if opts.lgrep then return M.live_grep_mt(opts) else opts.search = '' return M.grep(opts) end else utils.info("Rg current buffer requires file on disk") return end end M.lgrep_curbuf = function(opts) if not opts then opts = {} end opts.lgrep = true opts.__FNCREF__ = utils.__FNCREF__() return M.grep_curbuf(opts) end return M