---@diagnostic disable-next-line: deprecated
local uv = vim.uv or vim.loop
local core = require "fzf-lua.core"
local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
local libuv = require "fzf-lua.libuv"
local config = require "fzf-lua.config"
local M = {}
---@param opts fzf-lua.config.Builtin|{}
---@return thread?, string?, table?
M.metatable = function(opts)
if not opts then return end
---@diagnostic disable-next-line: inject-field
if not opts.metatable then opts.metatable = getmetatable("").__index end
local methods = {}
for k, _ in pairs(opts.metatable) do
if not opts.metatable_exclude or opts.metatable_exclude[k] == nil then
table.insert(methods, k)
end
end
table.sort(methods, function(a, b) return a < b end)
return core.fzf_exec(methods, opts)
end
---@param dir string
---@param fn fun(fname: string, name: string, type: string)
local function ls(dir, fn)
local handle = uv.fs_scandir(dir)
while handle do
local name, t = uv.fs_scandir_next(handle)
if not name then
break
end
local fname = path.join({ dir, name })
-- HACK: type is not always returned due to a bug in luv,
-- so fecth it with fs_stat instead when needed.
-- see https://github.com/folke/lazy.nvim/issues/306
fn(fname, name, t or (uv.fs_stat(fname) or {}).type)
end
end
---@param opts fzf-lua.config.Profiles|{}?
---@return thread?, string?, table?
M.profiles = function(opts)
---@type fzf-lua.config.Profiles
opts = config.normalize_opts(opts, "profiles")
if not opts then return end
if opts.load then
-- silent = [2]
require("fzf-lua").setup({ opts.load, false })
return
end
local dirs = {
path.join({ vim.g.fzf_lua_directory, "profiles" })
}
local contents = function(cb)
coroutine.wrap(function()
local co = coroutine.running()
---@cast co thread
for _, d in ipairs(dirs) do
ls(d, function(fname, name, type)
local ext = path.extension(fname)
if type == "file" and ext == "lua" then
local profile = name:sub(1, #name - 4)
local res = utils.load_profile_fname(fname, profile, true)
if res then
local entry = string.format("%s:%-30s%s", fname,
utils.ansi_codes.yellow(profile), res.desc or "")
cb(entry, function(err)
coroutine.resume(co)
if err then cb(nil) end
end)
coroutine.yield()
end
end
end)
end
-- done
cb(nil)
end)()
end
return core.fzf_exec(contents, opts)
end
local function gen_def(n, o)
local _, contents, opts = FzfLua[n](o)
return {
name = n,
opts = opts,
contents = contents,
}
end
M.combine = function(t)
t = t or {}
local pickers = (type(t.pickers) == "table" and t.pickers
or type(t.pickers) == "string" and utils.strsplit(t.pickers, "[,;]"))
assert(pickers)
local opts = t
t.pickers = nil
-- Tells fzf_wrap to not start the fzf process
opts._start = false
local cmds = {}
local opts_copy = vim.deepcopy(opts)
for i, name in ipairs(pickers) do
if FzfLua[name] then
-- Default picker opts set the tone for this picker options
-- this way convert reload / exec_silent actions will use a consistent
-- opts ref in the callbacks so we can later modify internal values
local def = gen_def(name, i == 1 and opts or opts_copy)
if i == 1 then
-- Override opts with the modified return opts and remove start suppression
opts = def.opts
opts._start = nil
end
-- Instantiate the previewer, nil check as opts isn't guaranteed if the
-- picker isn't avilable, e.g. `tags` when no tags file exists
if def.opts and def.opts.previewer then
def.previewer = require("fzf-lua.previewer").new(def.opts.previewer, def.opts)
end
-- Add content (shell command) to cmd array
table.insert(cmds, def.contents)
else
utils.warn("invalid picker '%s', ignoring.", name)
end
end
-- _G.dump(cmds)
local contents = table.concat(cmds, utils.__IS_WINDOWS and "&" or ";")
return core.fzf_wrap(contents, opts)
end
---@param opts fzf-lua.config.Global|{}?
---@return thread?, string?, table?
M.global = function(opts)
-- -@type fzf-lua.config.GlobalStrict
---@type fzf-lua.config.Global
opts = config.normalize_opts(opts, "global")
if not opts then return end
if opts.line_query and not utils.has(opts, "fzf", { 0, 59 }) then
utils.warn("'global' requires fzf >= 0.59, reverting to files.")
return FzfLua.files(opts)
end
if type(opts.pickers) == "function" then
opts.pickers = opts.pickers()
end
opts._start = false -- Tells fzf_wrap to not start the fzf process
opts._normalized = nil -- We need to "normalize" again with the picker opts
local pickers = {}
local opts_copy = vim.deepcopy(opts)
for _, t in ipairs(opts.pickers) do
local name = t[1]
if FzfLua[name] then
local def
if not t.prefix then
-- Default picker opts set the tone for this picker options
-- this way convert reload / exec_silent actions will use a consistent
-- opts ref in the callbacks so we can later modify internal values
def = gen_def(name, opts)
pickers[name] = def
-- Override opts with the return opts and store a copy of `pickers[]`
-- as we patch the opts when switching a picker in the change event
opts = def.opts
opts._start = nil -- remove the start suppression
def.opts = vim.deepcopy(opts)
else
-- Each subsequent picker gets a fresh copy of the original opts
-- (unmodified by the default picker)
def = gen_def(name, t.opts
and vim.tbl_deep_extend("force", {}, opts_copy, t.opts)
or opts_copy)
pickers[name] = def
end
-- HACK: inherit fzf_opts from the normalized picker opts so we have
-- with-nth for the transform (e.g. for blines)
-- TODO: we can probably inerit all opts but I'm not sure what would be
-- the effects of that, this entire picker needs refactoring
local picker_opts = config.normalize_opts(t.opts,
name:gsub("^lsp_", "lsp."):gsub("^git_", "git."):gsub("^dap_", "dap."))
if not utils.map_get(def, "opts.fzf_opts") then utils.map_set(def, "opts.fzf_opts", {}) end
def.opts.fzf_opts = vim.tbl_deep_extend("force", def.opts.fzf_opts, picker_opts.fzf_opts)
-- Instantiate the previewer, opts isn't guaranteed if the picker
-- isn't avilable, e.g. `tags` when not tags file exists
if def.opts and def.opts.previewer then
---@diagnostic disable-next-line: inject-field
def.previewer = require("fzf-lua.previewer").new(def.opts.previewer, def.opts)
end
else
utils.warn("invalid picker '%s', ignoring.", name)
end
end
-- Test for default/starting picker
local default_picker = opts.pickers[1] and pickers[opts.pickers[1][1]]
if not default_picker or default_picker.prefix then
utils.error("default picker not defined or has a prefix, aborting.")
return
end
---@param q string?
---@return table?, integer?
local get_picker = function(q)
if type(q) == "string" and #q > 0 then
for _, t in ipairs(opts.pickers) do
local name = t[1]
if t.prefix and #t.prefix > 0 and q:match("^" .. utils.lua_regex_escape(t.prefix)) then
return pickers[name], #t.prefix + 1
end
end
end
return default_picker, 1
end
local cur_picker, cur_sub
local transform_picker = function(start)
return FzfLua.shell.stringify_data(function(args, _, _)
local q = args[1]
local new_picker, new_sub = get_picker(q)
assert(new_picker)
local reload = ""
if start or new_picker and new_picker ~= cur_picker then
-- New picker requested, reload the contents and transform
-- the search string to exclude the picker prefix
-- Patch the opts refs with important values for path parsing
-- e.g. formatter, path_shorten, etc
cur_picker = cur_picker or new_picker
-- TODO: is there a better way to override the callback opts ref?
opts.__alt_opts = new_picker.opts
-- Attach a new previewer if exists
local win = FzfLua.utils.fzf_winobj()
if win
and new_picker.previewer
and new_picker.previewer ~= cur_picker.previewer
then
win:close_preview()
win:attach_previewer(new_picker.previewer)
win:redraw_preview()
end
reload = string.format("reload(%s)+", new_picker.contents)
-- fzf >= 0.69 supports `change-with-nth` to dynamically change `--with-nth`
-- dynamically update `--with-nth` per-picker (fzf >= 0.69)
-- e.g. blines uses `--with-nth=4..` to hide buffer numbers
if utils.has(opts, "fzf", { 0, 69 }) then
-- `false|nil` defaults to all fields (..)
local with_nth = utils.map_get(new_picker, "opts.fzf_opts.--with-nth")
reload = reload .. string.format("change-with-nth(%s)+", with_nth or "..")
end
cur_sub = new_sub
cur_picker = new_picker
end
if not q or not cur_sub then return end
return reload .. string.format("search(%s)", q:sub(cur_sub))
end, opts, "{q}")
end
opts._fzf_cli_args = opts._fzf_cli_args or {}
-- Insert at the start of the args table so `line_query` callback is first
table.insert(opts._fzf_cli_args, 1,
libuv.cli_raw_arg("--bind=", "start:+transform:" .. transform_picker(true)))
table.insert(opts._fzf_cli_args, 2,
libuv.cli_raw_arg("--bind=", "change:+transform:" .. transform_picker(false)))
if opts.header ~= false then
local header = {}
for _, t in pairs(opts.pickers) do
table.insert(header, string.format("<%s> %s",
utils.ansi_from_hl(opts.hls.header_bind, t.prefix or "default"),
utils.ansi_from_hl(opts.hls.header_text, t.desc or t[1])))
end
opts.header = table.concat(header, "|")
end
return core.fzf_wrap(utils.shell_nop(), opts)
end
return M