---@diagnostic disable-next-line: deprecated
local uv = vim.uv or vim.loop
local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
local libuv = require "fzf-lua.libuv"
local actions = require "fzf-lua.actions"
local devicons = require "fzf-lua.devicons"
---@class fzf-lua.config
---@field globals fzf-lua.config.Defaults
---@field __HLS_STATE { colorscheme: string, bg: string }
local M = {}
-- set this so that make_entry won't get nil err when setting remotely
M.__resume_data = {}
M.resume_get = function(what, opts)
assert(opts.__resume_key)
if type(opts.__resume_get) == "function" then
-- override was set (live_grep, live_workspace_symbols)
return opts.__resume_get(what, opts)
end
local fn_key = tostring(opts.__resume_key):match("[^%s]+$")
what = string.format("__resume_map.%s%s", fn_key,
type(what) == "string" and ("." .. what) or "")
-- _G.dump("resume_get", what, utils.map_get(M, what))
return utils.map_get(M, what)
end
-- store an option in both the resume options and provider specific __call_opts
M.resume_set = function(what, val, opts)
assert(opts.__resume_key)
if type(opts.__resume_set) == "function" then
-- override was set (live_grep, live_workspace_symbols)
return opts.__resume_set(what, val, opts)
end
local fn_key = tostring(opts.__resume_key):match("[^%s]+$")
local key1 = string.format("__resume_map.%s%s", fn_key,
type(what) == "string" and ("." .. what) or "")
utils.map_set(M, key1, val)
if type(what) == "string" then
local key2 = string.format("__resume_data.opts.%s", what)
utils.map_set(M, key2, val)
end
-- backward compatibility for users using `get_last_query`
if what == "query" then
utils.map_set(M, "__resume_data.last_query", val)
-- store in opts for convenience in action callbacks
opts.last_query = val
end
-- _G.dump("resume_set", key1, utils.map_get(M, key1))
end
---@param opts {resume: boolean, __call_opts: table}
---@return table
function M.resume_opts(opts)
assert(opts.resume and opts.__call_opts)
local __call_opts = M.resume_get(nil, opts)
opts = vim.tbl_deep_extend("keep", opts, __call_opts or {}) --[[@as { resume: boolean, __call_opts: table }]]
opts.__call_opts = vim.tbl_deep_extend("keep", opts.__call_opts, __call_opts or {}) --[[@as table]]
return opts
end
-- proxy table (with logic) for accessing the global config
---@type fzf-lua.Config|{}
M.setup_opts = {}
---@type fzf-lua.config.Defaults
M.globals = setmetatable({}, {
__index = function(_, index)
local function setup_opts()
return M._profile_opts
and vim.tbl_deep_extend("keep", {}, M._profile_opts, M.setup_opts)
or M.setup_opts
end
local function setup_defaults()
return M._profile_opts and (M._profile_opts.defaults or {}) or M.setup_opts.defaults or {}
end
-- build normalized globals, option priority below:
-- (1) provider specific globals (post-setup)
-- (2) generic global-defaults (post-setup), i.e. `setup({ defaults = { ... } })`
-- (3) fzf-lua's true defaults (pre-setup, static)
local fzflua_default = utils.map_get(M.defaults, index)
local setup_default = utils.map_get(setup_defaults(), index)
local setup_value = utils.map_get(setup_opts(), index)
local function build_bind_tables(keys)
assert(fzflua_default)
-- bind tables are logical exception, do not merge with defaults unless `[1] == true`
-- normalize all binds as lowercase to prevent duplicate keys (#654)
local ret = {}
-- exclude case-sensitive alt-binds from being lowercased
local exclude_case_sensitive_alt = { "^alt%-%a$" }
for _, k in ipairs(keys) do
if type(setup_value) == "function" then setup_value = setup_value() end
ret[k] = setup_value and type(setup_value[k]) == "table"
and vim.tbl_deep_extend("keep",
utils.map_tolower(utils.tbl_deep_clone(setup_value[k]), exclude_case_sensitive_alt) or
{},
setup_value[k][1] == true and
utils.map_tolower(fzflua_default[k], exclude_case_sensitive_alt) or {})
or utils.map_tolower(utils.tbl_deep_clone(fzflua_default[k]), exclude_case_sensitive_alt)
if ret[k] and ret[k][1] ~= nil then
-- Remove the [1] indicating inheritance from defaults and
ret[k][1] = nil
end
end
return ret
end
if index == "actions" then
return build_bind_tables({ "files", "buffers" })
elseif index == "keymap" then
return build_bind_tables({ "fzf", "builtin" })
end
-- values that aren't tables can't get merged, return highest priority
if setup_value ~= nil and type(setup_value) ~= "table" then
return setup_value
elseif setup_default ~= nil and type(setup_default) ~= "table" then
return setup_default
elseif fzflua_default ~= nil and type(fzflua_default) ~= "table" then
return fzflua_default
elseif fzflua_default == nil and setup_value == nil then
return
end
-- (1) use fzf-lua's true defaults (pre-setup) as our options base
local ret = utils.tbl_deep_clone(fzflua_default) or {}
if (fzflua_default and (fzflua_default.actions or fzflua_default._actions))
or (setup_value and (setup_value.actions or setup_value._actions)) then
-- (2) the existence of the `actions` key implies we're dealing with a picker
-- override global provider defaults supplied by the user's setup `defaults` table
ret = (vim.tbl_deep_extend("force", ret, setup_defaults()) --[[@as fzf-lua.config.Defaults]])
end
-- (3) override with the specific provider options from the users's `setup` option
ret = (vim.tbl_deep_extend("force", ret, utils.map_get(setup_opts(), index) or {})
--[[@as fzf-lua.config.Defaults]])
return ret
end,
__newindex = function(_, index, _)
error(string.format("modifying globals directly isn't allowed [index: %s]", index))
end
})
do
-- store circular refs for globals/defaults
-- used by M.globals.__index and M.defaults.<provider>._actions
local m = require("fzf-lua.defaults")
M.defaults = m.defaults
---@diagnostic disable-next-line: inject-field
m.globals = M.globals
end
local eval = function(v, ...)
if vim.is_callable(v) then return v(...) end
return v
end
---expand opts that were specified with a dot
---@param opts table
local normalize_tbl = function(opts)
-- convert keys only after full iteration or we will
-- miss keys due to messing with map ordering
local to_convert = {}
for k, _ in pairs(opts) do
if type(k) == "string" and k:match("%.") then
table.insert(to_convert, k)
end
end
for _, k in ipairs(to_convert) do
utils.map_set(opts, k, opts[k])
opts[k] = nil
end
end
---@generic T table
---@param opts T?|{}|(fun():T?)
---@param globals string|(fzf-lua.Config|{})?
---@param __resume_key string?
---@return T
function M.normalize_opts(opts, globals, __resume_key) ---@diagnostic disable
-- opts can also be a function that returns an opts table
---@type fzf-lua.config.Resolved
opts = eval(opts) or {}
if opts._normalized then
return opts
end
-- e.g. `:FzfLua files winopts.border=single`
normalize_tbl(opts)
local profile = opts.profile or opts[1] or (function()
if type(globals) == "string" then
local picker_opts = M.globals[globals]
return picker_opts.profile or picker_opts[1]
end
end)()
if type(profile) == "table" or type(profile) == "string" then
-- TODO: we should probably cache the profiles
M._profile_opts = utils.load_profiles(profile, 1)
end
-- save the user's original call params separately
opts.__call_opts = opts.__call_opts or utils.deepcopy(opts)
opts.__call_fn = utils.__FNCREF2__()
-- resume storage data lookup key, default to the calling function ref
-- __FNCREF2__ will use the 2nd function ref in the stack (calling fn)
opts.__resume_key = __resume_key
or opts.__resume_key
or (type(globals) == "string" and globals)
or (type(globals) == "table" and globals.__resume_key)
or utils.__FNCREF2__()
if type(globals) == "string" then
-- globals is a string, generate provider globals
globals = M.globals[globals]
assert(type(globals) == "table")
else
assert(globals)
-- backward compat: globals sent directly as table
-- merge with setup options "defaults" table
globals = vim.tbl_deep_extend("keep", globals, M.setup_opts.defaults or {})
end
---@cast globals fzf-lua.config.Base
-- merge current opts with revious __call_opts on resume
if opts.resume then
opts = M.resume_opts(opts)
end
local executable = function(binary, fncerr, strerr)
if binary and vim.fn.executable(binary) ~= 1 then
fncerr(("'%s' is not a valid executable, %s"):format(binary, strerr))
return false
end
return true
end
-- Validate the fzf binary & version dependency
opts.fzf_bin = opts.fzf_bin or M.globals.fzf_bin
opts.fzf_bin = opts.fzf_bin and libuv.expand(opts.fzf_bin) or nil
if not opts.fzf_bin or
not executable(opts.fzf_bin, utils.warn, "fallback to 'fzf'.") then
-- default|fallback to fzf
opts.fzf_bin = "fzf"
-- try fzf plugin if fzf is not installed globally
if vim.fn.executable(opts.fzf_bin) ~= 1 then
local ok, fzf_plug = pcall(vim.api.nvim_call_function, "fzf#exec", {})
if ok and fzf_plug then
opts.fzf_bin = fzf_plug
end
end
if not executable(opts.fzf_bin, utils.error,
"aborting. Please make sure 'fzf' is in installed.") then
return nil
end
end
-- enforce fzf minimum requirements
vim.g.fzf_lua_fzf_version = nil
if vim.fs.basename(opts.fzf_bin):match("^sk") then
local SK_VERSION, rc, err = utils.sk_version(opts)
opts.__SK_VERSION = SK_VERSION
if not opts.__SK_VERSION then
utils.error("'sk --version' failed with error %s: %s", rc, err)
return nil
elseif utils.__IS_WINDOWS and not utils.has(opts, "sk", { 5, 8, 0 }) then
-- Windows minimum: older skim releases are unsupported on this platform
utils.error("skim version %s is lower than minimum (5.8.0) on Windows, aborting.",
utils.ver2str(opts.__SK_VERSION))
return nil
end
else
local FZF_VERSION, rc, err = utils.fzf_version(opts)
opts.__FZF_VERSION = FZF_VERSION
vim.g.fzf_lua_fzf_version = FZF_VERSION
if not opts.__FZF_VERSION then
utils.error("'fzf --version' failed with error %s: %s", rc, err)
return nil
elseif not utils.has(opts, "fzf", { 0, 36 }) then
utils.error("fzf version %s is lower than minimum (0.36), aborting.",
utils.ver2str(opts.__FZF_VERSION))
return nil
end
end
local function convert_bool_opts()
-- Enforce conversion of boolean options that are tables with `enabled`
-- property, i.e. `winopts.treesitter = true` will be converted to
-- `winopts = { treesitter = { enabled = true, <defaults> } }`
-- Running a command and setting sub-values implies `enabled=true`, e.g:
-- `:FzfLua blines winopts.treesitter.fzf_colors=false`
-- `:FzfLua blines winopts.treesitter.fzf_colors={hl="-1:underline"}`
-- So the above will be converted to:
-- `winopts = { treesitter = { enabled = true, fzf_colors = false, <defaults> } }`
-- NOTE: this function runs once before merging with globals and once
-- later down the line (after merges with globals/defaults), this is done
-- so that commands as the example above won't inherit `{ enabled = false }`
-- from the defaults (e.g. the default is `winopts.treesitter.enabled = false`)
for k, vfrom in pairs({
["winopts.treesitter"] = "winopts.treesitter",
["previewer.treesitter"] = "previewers.builtin.treesitter",
["previewer.render_markdown"] = "previewers.builtin.render_markdown",
})
do
local v = utils.map_get(opts, k)
if v == false then
utils.map_set(opts, k, { enabled = false })
elseif v == true or type(v) == "table" then
local newv = vim.tbl_deep_extend("keep", type(v) == "table" and v or {},
{ enabled = true }, utils.map_get(M.defaults, vfrom) or {})
utils.map_set(opts, k, newv)
end
end
end
convert_bool_opts()
-- normalize all binds as lowercase or we can have duplicate keys (#654)
---@param m fzf-lua.config.Keymap?
---@param exclude_patterns string[]
---@return fzf-lua.config.Keymap?
local keymap_tolower = function(m, exclude_patterns)
return m and {
fzf = utils.map_tolower(m.fzf, exclude_patterns),
builtin = utils.map_tolower(m.builtin, exclude_patterns),
} or nil
end
local exclude_case_sensitive_alt = { "^alt%-%a$" }
opts.keymap = keymap_tolower(eval(opts.keymap, opts), exclude_case_sensitive_alt)
opts.actions = utils.map_tolower(eval(opts.actions, opts), exclude_case_sensitive_alt)
globals.keymap = keymap_tolower(eval(globals.keymap, opts), exclude_case_sensitive_alt)
globals.actions = utils.map_tolower(eval(globals.actions, opts), exclude_case_sensitive_alt)
-- inherit from globals.actions?
if type(globals._actions) == "function" then
globals.actions = vim.tbl_extend("keep", globals.actions or {}, globals._actions())
end
-- merge with provider defaults from globals (defaults + setup options)
opts = vim.tbl_deep_extend("keep", opts, utils.tbl_deep_clone(globals))
-- Backward compat: merge `winopts` with outputs from `winopts_fn`
local winopts_fn = opts.winopts_fn or M.globals.winopts_fn
if type(winopts_fn) == "function" then
vim.deprecate("winopts_fn", "winopts", "Jan 2026", "FzfLua")
local ret = winopts_fn(opts) or {}
if not utils.tbl_isempty(ret) and (not opts.winopts or type(opts.winopts) == "table") then
opts.winopts = vim.tbl_deep_extend("force", opts.winopts or {}, ret)
end
end
local extend_opts = function(m, k)
local setup_val = m[k]
if type(setup_val) == "function" then
setup_val = setup_val(opts)
if type(setup_val) == "table" then
local default_val = utils.map_get(M.defaults, k)
if type(default_val) == "table" then
setup_val = vim.tbl_deep_extend("force", {}, default_val, setup_val)
end
end
end
if type(setup_val) == "table" then
-- must clone or map will be saved as reference
-- and then overwritten if found in 'backward_compat'
setup_val = utils.tbl_deep_clone(setup_val)
end
if opts[k] == nil then
opts[k] = setup_val
else
if type(opts[k]) == "function" then
opts[k] = opts[k](opts)
end
if type(opts[k]) == "table" then
opts[k] = vim.tbl_deep_extend("keep",
opts[k], type(setup_val) == "table" and setup_val or {})
end
end
end
-- Merge values from globals
for _, k in ipairs({
"winopts", "keymap", "fzf_opts", "fzf_colors", "fzf_tmux_opts", "hls"
}) do
extend_opts(globals, k)
extend_opts(M.globals, k)
end
-- backward compat: no-value flags should be set to `true`, in the past these
-- would be set to an empty string which would now translate into a shell escaped
-- string as we automatically shell escape all fzf_opts
for k, v in pairs(opts.fzf_opts) do
if v == "" then opts.fzf_opts[k] = true end
end
-- backward compat for `winopts.preview.{wrap|hidden}`
for k, v in pairs({ wrap = "nowrap", hidden = "nohidden" }) do
local val = utils.map_get(opts, "winopts.preview." .. k)
if type(val) == "string" then
utils.map_set(opts, "winopts.preview." .. k, not val:match(v))
end
end
-- fzf.vim's `g:fzf_history_dir` (#1127)
if vim.g.fzf_history_dir and opts.fzf_opts["--history"] == nil then
local histdir = libuv.expand(vim.g.fzf_history_dir)
if vim.fn.isdirectory(histdir) == 0 then
pcall(vim.fn.mkdir, histdir)
end
if vim.fn.isdirectory(histdir) == 1 and type(opts.__resume_key) == "string" then
opts.fzf_opts["--history"] = path.join({ histdir, opts.__resume_key })
end
end
if opts.fzf_opts["--history"] == nil then
opts.fzf_opts["--history"] = require("fzf-lua.config.parse").get()["--history"]
end
-- Merge arrays from globals|defaults, can't use 'vim.tbl_xxx'
-- for these as they only work for maps, ie. '{ key = value }'
for _, k in ipairs({ "file_ignore_patterns" }) do
for _, m in ipairs({ globals, M.globals }) do
if m[k] and opts[k] ~= false then
for _, item in ipairs(m[k]) do
if not opts[k] then opts[k] = {} end
table.insert(opts[k], item)
end
end
end
end
-- these options are copied from globals unless specifically set
-- also check if we need to override 'opts.prompt' from cli args
-- if we don't override 'opts.prompt' 'FzfWin.save_query' will
-- fail to remove the prompt part from resume saved query (#434)
for _, s in ipairs({
"fzf_args",
"fzf_cli_args",
"fzf_raw_args",
"file_icon_padding",
"dir_icon",
"help_open_win",
}) do
if opts[s] == nil then
opts[s] = M.globals[s]
end
local pattern_prefix = "%-%-prompt="
local pattern_prompt = ".-"
---@type string?
local surround = type(opts[s]) == "string" and opts[s]:match(pattern_prefix .. "(.)") or nil
-- prompt was set without surrounding quotes
-- technically an error but we can handle it gracefully instead
if surround and surround ~= [[']] and surround ~= [["]] then
surround = ""
pattern_prompt = "[^%s]+"
end
if surround then
local pattern_capture = pattern_prefix ..
("%s(%s)%s"):format(surround, pattern_prompt, surround)
local pattern_gsub = pattern_prefix ..
("%s%s%s"):format(surround, pattern_prompt, surround)
if opts[s]:match(pattern_gsub) then
opts.prompt = opts[s]:match(pattern_capture)
opts[s] = opts[s]:gsub(pattern_gsub, "")
end
end
end
-- `fzf_cli_args` is string, `_fzf_cli_args` is a table used internally
opts._fzf_cli_args = type(opts._fzf_cli_args) == "table" and opts._fzf_cli_args or {}
-- backward compatibility, rhs overrides lhs
-- (rhs being the "old" option)
local backward_compat = {
{ "winopts.row", "winopts.win_row" },
{ "winopts.col", "winopts.win_col" },
{ "winopts.width", "winopts.win_width" },
{ "winopts.height", "winopts.win_height" },
{ "winopts.border", "winopts.win_border" },
{ "winopts.on_create", "winopts.window_on_create" },
{ "winopts.preview.wrap", "preview_wrap" },
{ "winopts.preview.border", "preview_border" },
{ "winopts.preview.hidden", "preview_opts" },
{ "winopts.preview.vertical", "preview_vertical" },
{ "winopts.preview.horizontal", "preview_horizontal" },
{ "winopts.preview.layout", "preview_layout" },
{ "winopts.preview.flip_columns", "flip_columns" },
{ "winopts.preview.default", "default_previewer" },
{ "winopts.preview.delay", "previewers.builtin.delay" },
{ "winopts.preview.title", "previewers.builtin.title" },
{ "winopts.preview.title_pos", "winopts.preview.title_align" },
{ "winopts.preview.scrollbar", "previewers.builtin.scrollbar" },
{ "winopts.preview.scrollchar", "previewers.builtin.scrollchar" },
{ "cwd_header", "show_cwd_header" },
{ "cwd_prompt", "show_cwd_prompt" },
{ "resume", "continue_last_search" },
{ "resume", "repeat_last_search" },
{ "jump1", "jump_to_single_result" },
{ "jump1_action", "jump_to_single_result_action" },
{ "hls.normal", "winopts.hl_normal" },
{ "hls.border", "winopts.hl_border" },
{ "hls.cursor", "previewers.builtin.hl_cursor" },
{ "hls.cursorline", "previewers.builtin.hl_cursorline" },
{ "hls", "winopts.hl" },
{ "previewer.treesitter.enabled", "previewer.treesitter.enable" },
{ "previewer.treesitter.disabled", "previewer.treesitter.disable" },
{ "previewers.builtin.treesitter.enabled", "previewers.builtin.treesitter.enable" },
{ "previewers.builtin.treesitter.disabled", "previewers.builtin.treesitter.disable" },
}
-- iterate backward compat map, retrieve values from opts or globals
for _, t in ipairs(backward_compat) do
local new_key, old_key = t[1], t[2]
local v_opts = utils.map_get(opts, old_key)
local old_val = v_opts == nil and utils.map_get(M.globals, old_key) or v_opts
local new_val = utils.map_get(opts, new_key)
if old_val ~= nil then
if type(old_val) == "table" and type(new_val) == "table" then
utils.map_set(opts, new_key, vim.tbl_deep_extend("keep", new_val, old_val))
else
utils.map_set(opts, new_key, old_val)
end
utils.map_set(opts, old_key, nil)
vim.deprecate(old_key, new_key, "Jan 2026", "FzfLua")
end
end
-- Backward compat, "default" action is "enter"
if opts.actions then
opts.actions.enter = opts.actions.default or opts.actions.enter
opts.actions.default = nil
end
-- Setup completion options
if opts.complete then
---@diagnostic disable-next-line: assign-type-mismatch
opts.actions = opts.actions or {}
opts.actions.enter = actions.complete
opts.actions["ctrl-c"] = function() end
end
-- Merge highlight overrides with defaults, we only do this after the
-- backward compat copy due to the migration of `winopts.hl` -> `hls`
opts.hls = vim.tbl_deep_extend("keep", opts.hls or {}, M.globals.__HLS)
-- Setup formatter options
if opts.formatter then
local _fmt_ver = 1
if type(opts.formatter) == "table" then
_fmt_ver = opts.formatter.v or opts.formatter[2] or _fmt_ver
opts.formatter = opts.formatter.name or opts.formatter[1]
end
local _fmt = M.globals["formatters." .. opts.formatter]
if _fmt then
opts._fmt = opts._fmt or {}
if opts._fmt.to == nil then
opts._fmt.to = _fmt.to or _fmt._to and _fmt._to(opts, _fmt_ver) or nil
end
if opts._fmt.from == nil then
opts._fmt.from = _fmt.from
end
if type(opts._fmt.to) == "string" then
-- store the string function as backup for `make_entry.preprocess`
opts._fmt._to = opts._fmt.to
opts._fmt.to = loadstring(tostring(opts._fmt.to))()
end
if type(_fmt.enrich) == "function" then
-- formatter requires enriching the config (fzf_opts, etc)
opts = _fmt.enrich(opts, _fmt_ver) or opts
end
else
utils.warn(("Invalid formatter '%s', ignoring."):format(opts.formatter))
end
end
-- Exclude file icons from the fuzzy matching (#1080)
if (opts.file_icons or opts.git_icons)
and opts._fzf_nth_devicons
and not opts.fzf_opts["--delimiter"]
-- Can't work due to : delimiter (#2112)
and opts.previewer ~= "bat"
and opts.previewer ~= "bat_native"
then
opts.fzf_opts["--nth"] = opts.fzf_opts["--nth"] or "-1.."
opts.fzf_opts["--delimiter"] = string.format("[%s]", utils.nbsp)
end
if type(opts.previewer) == "function" then
-- we use a function so the user can override
-- globals.winopts.preview.default
opts.previewer = opts.previewer()
end
-- "Shortcut" values to the builtin previewer
-- merge with builtin previewer defaults
if type(opts.previewer) == "table"
or opts.previewer == true
or opts.previewer == "hidden"
or opts.previewer == "nohidden"
then
-- of type string, can only be "hidden|nohidden"
if type(opts.previewer) == "string" then
assert(opts.previewer == "hidden" or opts.previewer == "nohidden")
utils.map_set(opts, "winopts.preview.hidden", opts.previewer ~= "nohidden")
end
opts.previewer = vim.tbl_deep_extend("keep",
type(opts.previewer) == "table" and opts.previewer or {},
M.globals.previewers.builtin)
end
-- Convert again in case the bool option came from global opts
convert_bool_opts()
-- Auto-generate fzf's colorscheme
opts.fzf_colors = type(opts.fzf_colors) == "table" and opts.fzf_colors
or opts.fzf_colors == true and { true } or {}
-- Inerherit from fzf.vim's g:fzf_colors
-- fzf.vim:
-- vim.g.fzf_colors = {
-- ["fg"] = { "fg" , "Comment", "Normal" }
-- }
-- fzf-lua:
-- fzf_colors = {
-- ["fg"] = { "fg" , { "Comment", "Normal" } }
-- }
opts.fzf_colors = vim.tbl_extend("keep", opts.fzf_colors,
vim.tbl_map(function(v)
-- Value isn't guaranteed a table, e.g:
-- vim.g.fzf_colors = { ["gutter"] = "-1" }
if type(v) ~= "table" then return tostring(v) end
-- We accept both fzf.vim and fzf-lua style values
if type(v[2]) == "table" then return v end
local new_v = { v[1], { v[2] } }
for i = 3, #v do
table.insert(new_v[2], v[i])
end
return new_v
end, type(vim.g.fzf_colors) == "table" and vim.g.fzf_colors or {}))
if opts.fzf_colors[1] == true then
opts.fzf_colors[1] = nil
opts.fzf_colors = vim.tbl_deep_extend("keep", opts.fzf_colors, {
["fg"] = { "fg", opts.hls.fzf.normal },
["bg"] = { "bg", opts.hls.fzf.normal },
["hl"] = { "fg", opts.hls.fzf.match },
["fg+"] = { "fg", { opts.hls.fzf.cursorline, opts.hls.fzf.normal } },
["bg+"] = { "bg", opts.hls.fzf.cursorline },
["hl+"] = { "fg", opts.hls.fzf.match },
["info"] = { "fg", opts.hls.fzf.info },
["border"] = { "fg", opts.hls.fzf.border },
["gutter"] = { "bg", opts.hls.fzf.gutter },
["query"] = { "fg", opts.hls.fzf.query, "regular" },
["prompt"] = { "fg", opts.hls.fzf.prompt },
["pointer"] = { "fg", opts.hls.fzf.pointer },
["marker"] = { "fg", opts.hls.fzf.marker },
["spinner"] = { "fg", opts.hls.fzf.spinner },
["header"] = { "fg", opts.hls.fzf.header },
["separator"] = { "fg", opts.hls.fzf.separator },
["scrollbar"] = { "fg", opts.hls.fzf.scrollbar }
})
end
-- Adjust main fzf window treesitter settings
-- Disabled unless the picker is TS enabled with `_treesitter=true`
-- Unless `enabled=false` is specifically set `true` is asssumed
if not opts._treesitter then opts.winopts.treesitter = nil end
if not opts.winopts.treesitter or opts.winopts.treesitter.enabled == false then
opts.winopts.treesitter = nil
else
assert(type(opts.winopts.treesitter) == "table")
assert(not opts.fzf_colors or type(opts.fzf_colors) == "table")
-- Unless the caller specifically disables `fzf_colors` fuzzy matching
-- colors "hl,hl+" will be set to "-1:reverse" which sets the background
-- color for matches to the corresponding original foreground color
-- NOTE: `fzf_colors` inherited from `defaults.winopts.treesitter`
if opts.winopts.treesitter.fzf_colors ~= false then
opts.fzf_colors = vim.tbl_deep_extend("force",
type(opts.fzf_colors) == "table" and opts.fzf_colors or {},
M.defaults.winopts.treesitter.fzf_colors,
type(opts.winopts.treesitter.fzf_colors) == "table"
and opts.winopts.treesitter.fzf_colors or {})
end
end
-- we need the original `cwd` with `autochdir=true` (#882)
-- use `_cwd` to not interfere with supplied users' options
-- as this can have unintended effects (e.g. in "buffers")
-- NOTE: we now always get the original cwd as there are
-- other user scenarios which need to use `opts._cwd`, for
-- exmaple, using the "hide" profile and resuming fzf-lua
-- from another tab after a `:tcd <dir>` (#1854)
opts._cwd = utils.cwd()
if opts.cwd and #opts.cwd > 0 then
-- NOTE: on Windows, `expand` will replace all backslashes with forward slashes
-- i.e. C:/Users -> c:\Users
-- Also reduces double backslashes to a single backslash, we therefore double
-- the backslashes prior to expanding (#1429)
opts.cwd = libuv.expand(opts.cwd)
if not uv.fs_stat(opts.cwd) then
utils.warn(("Unable to access '%s', removing 'cwd' option."):format(opts.cwd))
opts.cwd = nil
else
if not path.is_absolute(opts.cwd) then
-- relative paths in cwd are inaccessible when using multiprocess
-- as the external process have no awareness of our current working
-- directory so we must convert to full path (#375)
opts.cwd = path.join({ utils.cwd(), opts.cwd })
elseif utils.__IS_WINDOWS and opts.cwd:sub(2) == ":" then
-- TODO: upstream bug? on Windows: starting jobs with `cwd = C:` (without separator)
-- ignores the cwd argument and starts the job in the current working directory
opts.cwd = path.add_trailing(opts.cwd)
end
end
end
-- test for valid git_repo
opts.git_icons = opts.git_icons and path.is_git_repo(opts, true)
if utils.has(opts, "fzf", { 0, 53 })
-- `_multiline` is used to override `multiline` inherited from `defaults = {}`
and opts.multiline and opts._multiline ~= false then
-- If `multiline` was specified we add both "read0" & "print0" flags
opts.fzf_opts["--read0"] = true
opts.fzf_opts["--print0"] = true
local gap = (tonumber(opts.multiline) or 1) - 1
if gap > 0 then opts.fzf_opts["--gap"] = gap end
else
-- If not possible (fzf v<0.53|skim), nullify the option
opts.multiline = nil
end
-- Support filenames with CRLF (#2367), idea borrowed from fzf v0.39 changelog:
-- carriage return and a line feed characters will be rendered as dim ␍ and ␊ respectively
if opts.render_crlf then
opts.fzf_opts["--read0"] = true
if opts.fd_opts then
-- adding "-0" to fd prepends entries with "./", since we cannot guarantee fd v8.3
-- we remove the prefix in `make_entry.file` (instead of adding "--strip-cwd-prefix")
opts.strip_cwd_prefix = true
opts.fd_opts = "-0 " .. opts.fd_opts
end
if opts.rg_opts then opts.rg_opts = "-0 " .. opts.rg_opts end
if opts.grep_opts then opts.grep_opts = "-Z " .. opts.grep_opts end
if opts.find_opts then opts.find_opts = "-print0 " .. opts.find_opts end
end
do
-- Remove incompatible flags / values
-- (1) `true` flags are removed entirely (regardless of value)
-- (2) `string` flags are removed only if the values match
-- (3) `table` flags are removed if the value is contained
local bin, version, changelog = (function()
if opts.__SK_VERSION then
return "sk", opts.__SK_VERSION, {
-- All fzf flags values not support by skim (https://github.com/skim-rs/skim/issues/628)
["99.9.9"] = {
fzf_opts = {
["--border"] = { "none" },
["--info"] = { "inline-right" },
["--border-label-pos"] = true,
["--info-command"] = true,
}
},
["1.5.3"] = {
fzf_opts = {
["--border"] = { "plain", "rounded", "double", "thick", "light-double-dashed",
"heavy-double-dashed", "light-triple-dashed", "heavy-triple-dashed",
"light-quadruple-dashed", "heavy-quadruple-dashed", "quadrant-inside",
"quadrant-outside" },
["--algo"] = "frizbee"
}
},
["0.15.5"] = { fzf_opts = { ["--tmux"] = true } },
["0.53"] = {
fzf_opts = {
["--inline-info"] = true,
["--algo"] = { "skim_v1", "skim_v2" },
}
},
-- All fzf flags not existing in skim
["all"] = {
fzf_opts = {
["--scheme"] = false,
["--gap"] = false,
["--scrollbar"] = false,
["--no-scrollbar"] = false,
["--wrap"] = true,
["--wrap-sign"] = true,
["--highlight-line"] = false,
["--gutter"] = false,
["--border-label"] = false,
["--ellipsis"] = true,
}
},
}
else
return "fzf", opts.__FZF_VERSION, {
["0.66"] = { fzf_opts = { ["--raw"] = true, ["--gutter"] = true, ["--gutter-raw"] = true } },
["0.63"] = { fzf_opts = { ["--footer"] = true } },
["0.59"] = { fzf_opts = { ["--scheme"] = "path" } },
["0.56"] = { fzf_opts = { ["--gap"] = true } },
["0.54"] = {
fzf_opts = {
["--wrap"] = true,
["--wrap-sign"] = true,
["--highlight-line"] = true,
}
},
["0.53"] = { fzf_opts = { ["--tmux"] = true } },
["0.52"] = { fzf_opts = { ["--highlight-line"] = true } },
["0.42"] = {
fzf_opts = {
["--info"] = { "right", "inline-right" },
}
},
["0.39"] = { fzf_opts = { ["--track"] = true } },
["0.36"] = {
fzf_opts = {
["--listen"] = true,
["--scrollbar"] = true,
["--no-scrollbar"] = true,
}
},
["0.35"] = {
fzf_opts = {
["--border"] = { "bold", "double" },
["--border-label"] = true,
["--border-label-pos"] = true,
["--preview-label"] = true,
["--preview-label-pos"] = true,
}
},
["0.33"] = { fzf_opts = { ["--scheme"] = true } },
["0.30"] = { fzf_opts = { ["--ellipsis"] = true } },
["0.28"] = {
fzf_opts = {
["--header-first"] = true,
["--scroll-off"] = true,
}
},
["0.27"] = { fzf_opts = { ["--border"] = "none" } },
-- All skim flags not existing in fzf
["all"] = {
fzf_opts = {
["--algo"] = false,
["--inline-info"] = false,
}
},
}
end
end)()
local function warn(flag, val, min_ver)
return utils.warn("Removed flag '%s%s', %s.",
flag, type(val) == "string" and "=" .. val or "",
not min_ver and string.format("not supported with %s", bin)
or string.format("only supported with %s v%s (has=%s)",
bin, utils.ver2str(min_ver), utils.ver2str(version))
)
end
for min_verstr, ver_data in pairs(changelog) do
for flag, non_compat_value in pairs(ver_data.fzf_opts) do
(function()
local min_ver = utils.parse_verstr(min_verstr)
local opt_value = opts.fzf_opts[flag]
if not opt_value then return end
non_compat_value = type(non_compat_value) == "string"
and { non_compat_value } or non_compat_value
if not min_ver or not utils.has(opts, bin, min_ver)
and (non_compat_value == true or type(non_compat_value) == "table"
and utils.tbl_contains(non_compat_value, opt_value))
then
if opts.compat_warn == true then
warn(flag, opt_value, min_ver)
end
opts.fzf_opts[flag] = nil
end
end)()
end
end
end
-- Are we using fzf-tmux? if so get available columns
opts._is_fzf_tmux = (function()
if not vim.env.TMUX then
-- Could have adverse effects with skim (#1974)
opts.fzf_opts["--tmux"] = nil
return
end
local is_tmux =
(opts.fzf_bin:match("fzf%-tmux$") or opts.fzf_bin:match("sk%-tmux$")) and 1
-- fzf v0.53 added native tmux integration
or utils.has(opts, "fzf", { 0, 53 }) and opts.fzf_opts["--tmux"] and 2
-- skim v0.15.5 added native tmux integration
or utils.has(opts, "sk", { 0, 15, 5 }) and opts.fzf_opts["--tmux"] and 2
if is_tmux == 1 then
-- backward compat when using the `fzf-tmux` script: prioritize fzf-tmux
-- split pane flags over the popup flag `-p` from fzf-lua defaults (#865)
if type(opts.fzf_tmux_opts) == "table" then
for _, flag in ipairs({ "-u", "-d", "-l", "-r" }) do
if opts.fzf_tmux_opts[flag] then
opts.fzf_tmux_opts["-p"] = nil
end
end
end
end
return is_tmux
end)()
-- refresh highlights if background/colorscheme changed (#1092)
if not M.__HLS_STATE
or M.__HLS_STATE.bg ~= vim.o.bg
or M.__HLS_STATE.colorscheme ~= vim.g.colors_name then
utils.setup_highlights()
end
-- Cache provider specific highlights so we don't call vim functions
-- within a "fast event" (`vim.in_fast_event()`) and err with:
-- E5560: vimL function must not be called in a lua loop callback
for _, hl_opt in ipairs(opts._cached_hls or {}) do
local hlgroup = opts.hls[hl_opt]
assert(hlgroup ~= nil) -- must exist
local _, escseq = utils.ansi_from_hl(hlgroup)
utils.cache_ansi_escseq(hlgroup, escseq)
end
if opts.file_icons then
-- refresh icons, does nothing if "vim.o.bg" didn't change
if not devicons.load({
plugin = opts.file_icons,
icon_padding = opts.file_icon_padding,
dir_icon = {
icon = opts.dir_icon,
color = utils.hexcol_from_hl(opts.hls.dir_icon, "fg")
}
})
then
-- Disable file_icons if requested package isn't available
-- we set the default value to "1" but since it's the default
-- don't display the warning unless the user specifically set
-- file_icons to `true` or `mini|devicons`
if not tonumber(opts.file_icons) then
utils.warn("error loading '%s', disabling 'file_icons'.",
opts.file_icons == "mini" and "mini.icons" or "nvim-web-devicons")
end
opts.file_icons = nil
end
if opts.file_icons == "mini" then
-- When using "mini.icons" process lines 1-by-1 in the luv callback as having
-- to wait for all lines takes much longer due to the `vim.filetype.match` call
-- which makes the UX appear laggy
-- NOTE: DO NOT UNCOMMENT, bad perforamnce
-- opts.process1 = opts.process1 == nil and true or opts.process1
-- We also want to store the cached extensions/filenames in the main thread
-- which we do in "make_entry.postprocess"
opts.fn_postprocess = opts.multiprocess
and [[return require("fzf-lua.make_entry").postprocess]]
-- NOTE: we don't need to update mini when running on main thread
-- or require("fzf-lua.make_entry").postprocess
or nil
end
end
-- entry type is file, "optional file processing, only trandform
-- entries if an option is present which requires a transform
if opts._type == "file"
and (opts.git_icons
or opts.file_icons
or opts.file_ignore_patterns
or opts.ignore_current_file
or opts.strip_cwd_prefix
or opts.render_crlf
or opts.path_shorten
or opts.formatter
or opts.multiline
or opts.rg_glob)
then
opts.fn_transform = opts.fn_transform == nil
and [[return require("fzf-lua.make_entry").file]]
or opts.fn_transform
opts.fn_preprocess = opts.fn_preprocess == nil
and [[return require("fzf-lua.make_entry").preprocess]]
or opts.fn_preprocess
end
-- Must have preprocess to load icon sets, relocate {argvz}, etc
if opts.fn_transform and opts.fn_preprocess == nil
and (opts.file_icons
or opts.git_icons
or opts.formatter
or opts.fn_transform_cmd)
then
opts.fn_preprocess = [[return require("fzf-lua.make_entry").preprocess]]
end
-- set by git_{commits|diff|hunks} actions
if utils.get_info().cmd and opts["__pos_" .. utils.get_info().cmd] then
opts.locate = opts.locate == nil and true or opts.locate
opts.__locate_pos = opts.__locate_pos or opts["__pos_" .. utils.get_info().cmd]
end
if opts.locate and utils.has(opts, "fzf", { 0, 36 }) then
table.insert(opts._fzf_cli_args, libuv.cli_raw_arg("--bind=", "load:+transform:"
.. FzfLua.shell.stringify_data(function(_, _, _)
if opts.__locate_pos then
return string.format("pos(%d)", opts.__locate_pos)
end
end, opts)))
end
if opts.line_query and not utils.has(opts, "fzf", { 0, 59 }) then
utils.warn("'line_query' requires fzf >= 0.59, ignoring.")
elseif opts.line_query then
opts.line_query = type(opts.line_query) == "function"
and opts.line_query or function(q)
if not q then return end
local lnum = q:match(":(%d+)$")
local new_q, subs = q:gsub(":%d*$", "")
return lnum, (subs > 0 and new_q or nil)
end
utils.map_set(opts, "winopts.preview.winopts.cursorline", true)
table.insert(opts._fzf_cli_args, libuv.cli_raw_arg("--bind=", "start,change:+transform:"
.. FzfLua.shell.stringify_data(function(q, _, _)
local lnum, new_q = opts.line_query(q[1])
if not new_q then return end
local trans = string.format("search(%s)", new_q)
local win = FzfLua.win.__SELF()
-- Do we need to change the offset in native fzf previewer (e.g. bat)?
if lnum and win and win._previewer and win._previewer._preview_offset then
local optstr = opts.fzf_opts["--preview-window"]
local offset = win._previewer:_preview_offset(lnum)
trans = string.format("%s+change-preview-window(%s:%s)", trans, optstr, offset)
end
return trans
end, opts, "{q}")))
end
if type(opts.enrich) == "function" then
opts = opts.enrich(opts)
end
-- nullify profile options
M._profile_opts = nil
-- pid getter/setter, used by stringify to terminate previous pid
opts.PidObject = utils.pid_object("__stringify_pid", opts)
-- mark as normalized
opts._normalized = true
return opts
end ---@diagnostic enable
M.bytecode = function(s, _)
local keys = utils.strsplit(s, "%.")
local iter = M
for i = 1, #keys do
iter = iter[keys[i]]
if not iter then break end
if i == #keys and type(iter) == "function" then
-- string.dump (function [, strip])
-- Returns a string containing a binary representation (a binary chunk) of the given
-- function, so that a later load on this string returns a copy of the function (but
-- with new upvalues). If strip is a true value, the binary representation may not
-- include all debug information about the function, to save space.
return string.dump(iter, true)
end
end
end
M.set_action_helpstr = function(fn, helpstr)
assert(type(fn) == "function")
M._action_to_helpstr[fn] = helpstr
end
---@param v fzf-lua.ActionSpec|any
---@return string
M.get_action_helpstr = function(v)
local t = M._action_to_helpstr
if type(v) ~= "table" or t[v] then return t[v] or tostring(v) end
local res = v.desc or t[v[1]] or t[v.fn] or v.header
if res then return res end
return type(v[1]) == "string" and v[1] or tostring(v)
end
M._action_to_helpstr = {
[actions.dummy_abort] = "abort",
[actions.file_edit] = "file-edit",
[actions.file_edit_or_qf] = "file-edit-or-qf",
[actions.file_split] = "file-split",
[actions.file_vsplit] = "file-vsplit",
[actions.file_tabedit] = "file-tabedit",
[actions.file_sel_to_qf] = "file-selection-to-qf",
[actions.file_sel_to_ll] = "file-selection-to-loclist",
[actions.file_switch] = "file-switch",
[actions.file_switch_or_edit] = "file-switch-or-edit",
-- Since default actions refactor these are just refs to
-- their correspondent `file_xxx` equivalents
-- [actions.buf_edit] = "buffer-edit",
-- [actions.buf_edit_or_qf] = "buffer-edit-or-qf",
-- [actions.buf_sel_to_qf] = "buffer-selection-to-qf",
-- [actions.buf_sel_to_ll] = "buffer-selection-to-loclist",
-- [actions.buf_split] = "buffer-split",
-- [actions.buf_vsplit] = "buffer-vsplit",
-- [actions.buf_tabedit] = "buffer-tabedit",
-- [actions.buf_switch] = "buffer-switch",
-- [actions.buf_switch_or_edit] = "buffer-switch-or-edit",
[actions.buf_del] = "buffer-delete",
[actions.run_builtin] = "run-builtin",
[actions.undo] = "undo-jump",
[actions.ex_run] = "edit-cmd",
[actions.ex_run_cr] = "exec-cmd",
[actions.exec_menu] = "exec-menu",
[actions.search] = "edit-search",
[actions.search_cr] = "exec-search",
[actions.goto_jump] = "goto-jump",
[actions.goto_mark] = "goto-mark",
[actions.goto_mark_split] = "goto-mark-split",
[actions.goto_mark_vsplit] = "goto-mark-vsplit",
[actions.goto_mark_tabedit] = "goto-mark-tabedit",
[actions.keymap_apply] = "keymap-apply",
[actions.keymap_edit] = "keymap-edit",
[actions.keymap_split] = "keymap-split",
[actions.keymap_vsplit] = "keymap-vsplit",
[actions.keymap_tabedit] = "keymap-tabedit",
[actions.nvim_opt_edit_local] = "nvim-opt-edit-local",
[actions.nvim_opt_edit_global] = "nvim-opt-edit-global",
[actions.spell_apply] = "spell-apply",
[actions.spell_suggest] = "spell-suggest",
[actions.set_filetype] = "set-filetype",
[actions.packadd] = "packadd",
[actions.help] = "help-open",
[actions.help_vert] = "help-vertical",
[actions.help_tab] = "help-tab",
[actions.help_curwin] = "help-open-curwin",
[actions.man] = "man-open",
[actions.man_vert] = "man-vertical",
[actions.man_tab] = "man-tab",
[actions.git_branch_add] = "git-branch-add",
[actions.git_branch_del] = "git-branch-del",
[actions.git_switch] = "git-switch",
[actions.git_worktree_cd] = "change-directory",
[actions.git_worktree_add] = "git-worktree-add",
[actions.git_worktree_del] = "git-worktree-del",
[actions.git_checkout] = "git-checkout",
[actions.git_reset] = "git-reset",
[actions.git_stage] = "git-stage",
[actions.git_unstage] = "git-unstage",
[actions.git_stage_unstage] = "git-stage-unstage",
[actions.git_stash_pop] = "git-stash-pop",
[actions.git_stash_drop] = "git-stash-drop",
[actions.git_stash_apply] = "git-stash-apply",
[actions.git_buf_edit] = "git-buffer-edit",
[actions.git_buf_tabedit] = "git-buffer-tabedit",
[actions.git_buf_split] = "git-buffer-split",
[actions.git_buf_vsplit] = "git-buffer-vsplit",
[actions.git_goto_line] = "git-goto-line",
[actions.git_yank_commit] = "git-yank-commit",
[actions.arg_add] = "arg-list-add",
[actions.arg_del] = "arg-list-delete",
[actions.toggle_ignore] = "toggle-ignore",
[actions.toggle_hidden] = "toggle-hidden",
[actions.toggle_follow] = "toggle-follow",
[actions.grep_lgrep] = "grep<->lgrep",
[actions.sym_lsym] = "sym<->lsym",
[actions.tmux_buf_set_reg] = "set-register",
[actions.paste_register] = "paste-register",
[actions.set_qflist] = "set-{qf|loc}list",
[actions.list_del] = "list-delete",
[actions.apply_profile] = "apply-profile",
[actions.complete] = "complete",
[actions.dap_bp_del] = "dap-bp-delete",
[actions.colorscheme] = "colorscheme-apply",
[actions.cs_delete] = "colorscheme-delete",
[actions.cs_update] = "colorscheme-update",
[actions.toggle_bg] = "toggle-background",
[actions.zoxide_cd] = "change-directory",
}
return M