You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
15 KiB
Lua
478 lines
15 KiB
Lua
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 libuv = require "fzf-lua.libuv"
|
|
local make_entry = require "fzf-lua.make_entry"
|
|
|
|
local function get_last_search(opts)
|
|
if opts.__MODULE__ and opts.__MODULE__.get_last_search then
|
|
return opts.__MODULE__.get_last_search(opts)
|
|
end
|
|
local last_search = config.globals.grep._last_search or {}
|
|
return last_search.query, last_search.no_esc
|
|
end
|
|
|
|
local function set_last_search(opts, query, no_esc)
|
|
if opts.__MODULE__ and opts.__MODULE__.set_last_search then
|
|
opts.__MODULE__.set_last_search(opts, query, no_esc)
|
|
return
|
|
end
|
|
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 function set_live_grep_prompt(prompt)
|
|
-- prefix all live_grep prompts with an asterisk
|
|
return prompt:match("^%*") and prompt or '*'..prompt
|
|
end
|
|
|
|
local M = {}
|
|
|
|
local get_grep_cmd = function(opts, search_query, no_esc)
|
|
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
|
|
|
|
if opts.rg_glob and not command:match("^rg") then
|
|
opts.rg_glob = false
|
|
utils.warn("'--glob|iglob' flags require 'rg', ignoring 'rg_glob' option.")
|
|
end
|
|
|
|
if opts.rg_glob then
|
|
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
|
|
if not (no_esc or opts.no_esc) then
|
|
new_query = utils.rg_escape(new_query)
|
|
opts.no_esc = true
|
|
opts.search = ("%s%s"):format(new_query,
|
|
search_query:match(opts.glob_separator..".*"))
|
|
end
|
|
search_query = new_query
|
|
command = ("%s %s"):format(command, glob_args)
|
|
end
|
|
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 = libuv.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
|
|
-- we need to use our own version of 'shellescape'
|
|
-- that doesn't escape '\' on fish shell (#340)
|
|
search_query = libuv.shellescape(search_query)
|
|
end
|
|
|
|
-- construct the final command
|
|
command = ('%s %s %s'):format(command, search_query, search_path)
|
|
|
|
-- piped command filter, used for filtering ctags
|
|
if opts.filter and #opts.filter>0 then
|
|
command = ("%s | %s"):format(command, opts.filter)
|
|
end
|
|
|
|
return command
|
|
end
|
|
|
|
M.grep = function(opts)
|
|
|
|
opts = config.normalize_opts(opts, config.globals.grep)
|
|
if not opts then return end
|
|
|
|
-- we need this for 'actions.grep_lgrep'
|
|
opts.__MODULE__ = opts.__MODULE__ or M
|
|
|
|
local no_esc = false
|
|
if not opts.search and opts.resume then
|
|
opts.search, no_esc = get_last_search(opts)
|
|
opts.search = opts.search or opts.resume_search_default
|
|
end
|
|
|
|
-- if user did not provide a search term
|
|
-- provide an input prompt
|
|
if not opts.search and not opts.raw_cmd then
|
|
opts.search = utils.input(opts.input_prompt) or ''
|
|
end
|
|
|
|
-- 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)
|
|
|
|
-- save the search query so we
|
|
-- can call the same search again
|
|
set_last_search(opts, opts.search, no_esc or opts.no_esc)
|
|
|
|
local contents = core.mt_cmd_wrapper(vim.tbl_deep_extend("force", opts,
|
|
-- query was already parsed for globs inside 'get_grep_cmd'
|
|
-- no need for our external headless instance to parse again
|
|
{ rg_glob = false }))
|
|
|
|
-- 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
|
|
|
|
-- when using an empty string grep (as in 'grep_project') or
|
|
-- when switching from grep to live_grep using 'ctrl-g' users
|
|
-- may find it confusing why is the last typed query not
|
|
-- considered the last search so we find out if that's the
|
|
-- case and use the last typed prompt as the grep string
|
|
opts.fn_post_fzf = function(o, _)
|
|
local last_search, _ = get_last_search(o)
|
|
local last_query = config.__resume_data and config.__resume_data.last_query
|
|
if not last_search or #last_search==0
|
|
and (last_query and #last_query>0) then
|
|
set_last_search(opts, last_query)
|
|
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_exec(contents, opts)
|
|
end
|
|
|
|
-- single threaded version
|
|
M.live_grep_st = function(opts)
|
|
|
|
opts = config.normalize_opts(opts, config.globals.grep)
|
|
if not opts then return end
|
|
|
|
-- we need this for 'actions.grep_lgrep'
|
|
opts.__MODULE__ = opts.__MODULE__ or M
|
|
opts.prompt = set_live_grep_prompt(opts.prompt)
|
|
|
|
assert(not opts.multiprocess)
|
|
|
|
local no_esc = false
|
|
if not opts.search and opts.resume then
|
|
opts.search, no_esc = get_last_search(opts)
|
|
end
|
|
|
|
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
|
|
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, opts.query, true)
|
|
end
|
|
|
|
opts.fn_reload = function(query)
|
|
if query and not (opts.save_last_search == false) then
|
|
set_last_search(opts, query, true)
|
|
end
|
|
-- can be nil when called as fzf initial command
|
|
query = query or ''
|
|
opts.no_esc = nil
|
|
return get_grep_cmd(opts, query, true)
|
|
end
|
|
|
|
if opts.requires_processing or opts.git_icons or opts.file_icons then
|
|
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
|
|
|
|
-- see notes for this section in 'live_grep_mt'
|
|
if not opts._is_skim then
|
|
opts.save_query = true
|
|
opts.fn_post_fzf = function(o, _)
|
|
local last_search, _ = get_last_search(o)
|
|
local last_query = config.__resume_data and config.__resume_data.last_query
|
|
if not opts.exec_empty_query
|
|
and last_search ~= last_query then
|
|
set_last_search(opts, last_query or '')
|
|
end
|
|
end
|
|
end
|
|
|
|
-- search query in header line
|
|
opts = core.set_header(opts, opts.headers or {"actions","cwd"})
|
|
opts = core.set_fzf_field_index(opts)
|
|
core.fzf_exec(nil, 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
|
|
|
|
-- we need this for 'actions.grep_lgrep'
|
|
opts.__MODULE__ = opts.__MODULE__ or M
|
|
opts.__module__ = opts.__module__ or 'grep'
|
|
opts.prompt = set_live_grep_prompt(opts.prompt)
|
|
|
|
-- when using glob parsing we must use the external
|
|
-- headless instance for processing the query, this
|
|
-- prevents 'file|git_icons=false' from overriding
|
|
-- processing inside 'core.mt_cmd_wrapper'
|
|
if opts.rg_glob then
|
|
opts.requires_processing = true
|
|
end
|
|
|
|
assert(opts.multiprocess)
|
|
|
|
local no_esc = false
|
|
if not opts.search and opts.resume then
|
|
opts.search, no_esc = get_last_search(opts)
|
|
end
|
|
|
|
-- 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
|
|
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, opts.query, true)
|
|
end
|
|
|
|
-- signal to preprocess we are looking to replace {argvz}
|
|
opts.argv_expr = true
|
|
|
|
-- 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
|
|
-- 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
|
|
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
|
|
-- cleared, always having a minimum of one characer.
|
|
-- this signals 'core.fzf' to add the '--print-query' flag and
|
|
-- handle the typed query on process exit using 'opts.fn_save_query'
|
|
-- due to a skim bug this doesn't work when used in conjucntion with
|
|
-- the '--interactive' flag, the line with the typed query is printed
|
|
-- to stdout but is always empty
|
|
-- to understand this issue, run 'live_grep', type a query and then
|
|
-- delete it and press <C-i> to switch to 'grep', instead of an empty
|
|
-- search the last typed character will be used as the search string
|
|
if not opts._is_skim then
|
|
opts.save_query = true
|
|
opts.fn_post_fzf = function(o, _)
|
|
local last_search, _ = get_last_search(o)
|
|
local last_query = config.__resume_data and config.__resume_data.last_query
|
|
if not opts.exec_empty_query and last_search ~= last_query or
|
|
-- we should also save the query when we are piping the command
|
|
-- directly without our headless wrapper, i.e. 'live_grep_native'
|
|
(not opts.requires_processing and
|
|
not opts.git_icons and not opts.file_icons) then
|
|
set_last_search(opts, last_query or '', true)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- search query in header line
|
|
opts = core.set_header(opts, opts.headers or {"actions","cwd"})
|
|
opts = core.set_fzf_field_index(opts)
|
|
core.fzf_exec(nil, opts)
|
|
end
|
|
|
|
M.live_grep_glob_st = 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 glob
|
|
-- processsing in 'get_grep_cmd'
|
|
opts = opts or {}
|
|
opts.rg_glob = true
|
|
return M.live_grep_st(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
|
|
return M.live_grep_mt(opts)
|
|
end
|
|
|
|
M.live_grep_native = function(opts)
|
|
|
|
-- backward compatibility, by setting git|files icons to false
|
|
-- 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
|
|
return M.live_grep_mt(opts)
|
|
end
|
|
|
|
M.live_grep = function(opts)
|
|
opts = config.normalize_opts(opts, config.globals.grep)
|
|
if not opts then return end
|
|
|
|
if opts.multiprocess then
|
|
return M.live_grep_mt(opts)
|
|
else
|
|
return M.live_grep_st(opts)
|
|
end
|
|
end
|
|
|
|
M.live_grep_glob = function(opts)
|
|
opts = config.normalize_opts(opts, config.globals.grep)
|
|
if not opts then return end
|
|
|
|
if opts.multiprocess then
|
|
return M.live_grep_glob_mt(opts)
|
|
else
|
|
return M.live_grep_glob_st(opts)
|
|
end
|
|
end
|
|
|
|
M.live_grep_resume = function(opts)
|
|
if not opts then opts = {} end
|
|
opts.resume = true
|
|
return M.live_grep(opts)
|
|
end
|
|
|
|
M.grep_last = function(opts)
|
|
if not opts then opts = {} end
|
|
opts.resume = true
|
|
return M.grep(opts)
|
|
end
|
|
|
|
M.grep_cword = function(opts)
|
|
if not opts then opts = {} end
|
|
opts.search = vim.fn.expand("<cword>")
|
|
return M.grep(opts)
|
|
end
|
|
|
|
M.grep_cWORD = function(opts)
|
|
if not opts then opts = {} end
|
|
opts.search = vim.fn.expand("<cWORD>")
|
|
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)
|
|
-- we can't call 'normalize_opts' here because it will override
|
|
-- 'opts.__call_opts' which will confuse 'actions.grep_lgrep'
|
|
if type(opts) == 'function' then
|
|
opts = opts()
|
|
elseif not opts then
|
|
opts = {}
|
|
end
|
|
-- rg globs are meaningless here since we searching
|
|
-- a single file
|
|
opts.rg_glob = false
|
|
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(opts)
|
|
else
|
|
opts.search = opts.search or ''
|
|
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
|
|
return M.grep_curbuf(opts)
|
|
end
|
|
|
|
return M
|