---@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 config = require "fzf-lua.config"
local devicons = require "fzf-lua.devicons"
local make_entry = require "fzf-lua.make_entry"
local M = {}
---@param opts fzf-lua.config.Commands|{}?
---@return thread?, string?, table?
M.commands = function(opts)
---@type fzf-lua.config.Commands
opts = config.normalize_opts(opts, "commands")
if not opts then return end
local global_commands = vim.api.nvim_get_commands {}
local buf_commands = vim.api.nvim_buf_get_commands(0, {})
local builtin_commands = {}
-- parse help doc to get builtin commands and descriptions
if opts.include_builtin then
local help = vim.fn.globpath(vim.o.rtp, "doc/index.txt")
if uv.fs_stat(help) then
local cmd, desc
for line in utils.read_file(help):gmatch("[^\n]*\n") do
if line:match("^|:[^|]") then
if cmd then builtin_commands[cmd] = desc end
cmd, desc = line:match("^|:(%S+)|%s*%S+%s*(.*%S)")
elseif cmd then -- found
if line:match("^%s+%S") then
local desc_continue = line:match("^%s*(.*%S)")
desc = desc .. (desc_continue and " " .. desc_continue or "")
end
if line:match("^%s*$") then break end
end
end
if cmd then builtin_commands[cmd] = desc end
end
end
local commands = vim.tbl_extend("force", {}, global_commands, buf_commands, builtin_commands)
local entries = {}
if opts.sort_lastused then
-- display last used commands at the top of the list (#748)
-- iterate the command history from last used backwards
-- each command found gets added to the top of the list
-- and removed from the command map
local history = vim.split(vim.fn.execute("history"), "\n")
for i = #history, #history - 3, -1 do
local cmd = history[i]:match("%d+%s+([^%s]+)")
if buf_commands[cmd] then
table.insert(entries, cmd)
buf_commands[cmd] = nil
end
if global_commands[cmd] then
table.insert(entries, cmd)
global_commands[cmd] = nil
end
if builtin_commands[cmd] then
table.insert(entries, cmd)
end
end
end
local add_subcommand = function(k, ansi_color)
---@diagnostic disable-next-line: call-non-callable
local flattened = vim.is_callable(opts.flatten[k]) and opts.flatten[k](opts)
or opts.flatten[k] and vim.fn.getcompletion(k .. " ", "cmdline")
or {}
---@cast flattened table
vim.list_extend(entries,
---@diagnostic disable-next-line: param-type-mismatch
vim.tbl_map(function(cmd) return ansi_color(k .. " " .. cmd) end,
flattened))
end
opts.flatten = opts.flatten or {}
for k, _ in pairs(global_commands) do
table.insert(entries, utils.ansi_codes[opts.hls.cmd_global](k))
add_subcommand(k, utils.ansi_codes[opts.hls.cmd_global])
end
for k, v in pairs(buf_commands) do
if type(v) == "table" then
table.insert(entries, utils.ansi_codes[opts.hls.cmd_buf](k))
add_subcommand(k, utils.ansi_codes[opts.hls.cmd_buf])
end
end
-- Sort before adding "builtin" so they don't end up atop the list
if not opts.sort_lastused then
table.sort(entries, function(a, b) return a < b end)
end
for k, _ in pairs(builtin_commands) do
---@diagnostic disable-next-line: undefined-field
table.insert(entries, utils.ansi_codes[opts.hls.cmd_ex](k))
end
opts.preview = function(args)
local cmd = args[1]
if commands[cmd] then
cmd = vim.inspect(commands[cmd])
end
return cmd
end
return core.fzf_exec(entries, opts)
end
---@param opts fzf-lua.config.CommandHistory
---@param str ":"|"/"
local history = function(opts, str)
local histnr = vim.fn.histnr(str)
local dr = opts.reverse_list and 1 or -1
local bulk = 500
local from, to, delta = dr, dr * histnr, dr * bulk
local content = function(cb)
local co = coroutine.running()
---@cast co thread
for i = from, to, delta do
vim.schedule(function()
local count = bulk
for j = 0, delta - dr, dr do
local index = i + j
if dr > 0 and index <= to or dr < 0 and index >= to then
-- removed duplicates leave empty slots in the history, skip them
-- but still call `done()` so the batched producer keeps moving
local entry = vim.fn.histget(str, index)
local done = function()
count = count - 1
if count == 0 or index == to then
coroutine.resume(co)
end
end
if entry ~= "" then cb(entry, done) else done() end
end
end
end)
coroutine.yield()
end
cb(nil)
end
core.fzf_exec(function(cb) coroutine.wrap(content)(cb) end, opts)
end
---@param opts fzf-lua.config.CommandHistory|{}?
---@return thread?, string?, table?
M.command_history = function(opts)
---@type fzf-lua.config.CommandHistory
opts = config.normalize_opts(opts, "command_history")
if not opts then return end
history(opts, ":")
end
---@param opts fzf-lua.config.SearchHistory|{}?
---@return thread?, string?, table?
M.search_history = function(opts)
---@type fzf-lua.config.SearchHistory
opts = config.normalize_opts(opts, "search_history")
if not opts then return end
history(opts, "/")
end
---@param opts fzf-lua.config.Changes|{}?
---@return thread?, string?, table?
M.changes = function(opts)
---@type fzf-lua.config.Changes
opts = config.normalize_opts(opts, "changes")
if not opts then return end
return M.changes_or_jumps(opts)
end
---@param opts fzf-lua.config.Jumps|{}?
---@return thread?, string?, table?
M.jumps = function(opts)
---@type fzf-lua.config.Jumps
opts = config.normalize_opts(opts, "jumps")
if not opts then return end
return M.changes_or_jumps(opts)
end
M.changes_or_jumps = function(opts)
local jumps = vim.split(vim.fn.execute(opts.cmd), "\n")
local entries = {}
for i = #jumps - 1, 3, -1 do
local entry = jumps[i] ---@cast entry -?
local jump, line, col, text = entry:match("(%d+)%s+(%d+)%s+(%d+)%s+(.*)")
table.insert(entries, string.format(" %16s %15s %15s %s",
utils.ansi_codes.yellow(jump),
utils.ansi_codes.blue(line),
utils.ansi_codes.green(col),
text))
end
if utils.tbl_isempty(entries) then
utils.info(("%s list is empty."):format(opts.h1 or "jump"))
return
end
table.insert(entries, 1,
string.format("%6s %s %s %s", opts.h1 or "jump", "line", "col", "file/text"))
opts.fzf_opts["--header-lines"] = 1
return core.fzf_exec(entries, opts)
end
---@class fzf-lua.TagStackItem
---@field bufnr integer
---@field from [integer, integer, integer] # {line, col, ...}
---@field matchnr integer
---@field tagname string
---@field filename? string
---@field lnum? integer
---@field col? integer
---@field text? string
---@param opts fzf-lua.config.Tagstack|{}?
---@return thread?, string?, table?
M.tagstack = function(opts)
---@type fzf-lua.config.Tagstack
opts = config.normalize_opts(opts, "tagstack")
if not opts then return end
---@type fzf-lua.TagStackItem[]
local tagstack = vim.fn.gettagstack().items
local tags = {}
for i = #tagstack, 1, -1 do
local tag = tagstack[i]
tag.bufnr = tag.from[1]
if vim.api.nvim_buf_is_valid(tag.bufnr) then
tags[#tags + 1] = tag
tag.filename = vim.fn.bufname(tag.bufnr)
tag.lnum = tag.from[2]
tag.col = tag.from[3]
tag.text = vim.api.nvim_buf_get_lines(tag.bufnr, tag.lnum - 1, tag.lnum, false)[1] or ""
end
end
if utils.tbl_isempty(tags) then
utils.info("No tagstack available")
return
end
local entries = {}
for i, tag in ipairs(tags) do
local bufname = path.HOME_to_tilde(path.relative_to(tag.filename, utils.cwd()))
local buficon, hl
if opts.file_icons then
buficon, hl = devicons.get_devicon(bufname)
if hl and opts.color_icons then
buficon = utils.ansi_from_rgb(hl, buficon)
end
end
-- table.insert(entries, ("%s)%s[%s]%s%s%s%s:%s:%s: %s %s"):format(
table.insert(entries, ("%s)%s%s%s%s:%s:%s: %s %s"):format(
utils.ansi_codes.yellow(tostring(i)),
utils.nbsp,
-- utils.ansi_codes.yellow(tostring(tag.bufnr)),
-- utils.nbsp,
buficon or "",
buficon and utils.nbsp or "",
utils.ansi_codes.magenta(#bufname > 0 and bufname or "[No Name]"),
utils.ansi_codes.green(tostring(tag.lnum)),
tag.col,
utils.ansi_codes.red("[" .. tag.tagname .. "]"),
tag.text))
end
return core.fzf_exec(entries, opts)
end
---@param opts fzf-lua.config.Marks|{}?
---@return thread?, string?, table?
M.marks = function(opts)
---@type fzf-lua.config.Marks
opts = config.normalize_opts(opts, "marks")
if not opts then return end
local contents = function(cb)
local buf = utils.CTX().bufnr
local entries = {}
local function add_mark(mark, line, col, text)
if opts.marks and not string.match(mark, opts.marks) then return end
table.insert(entries, string.format("%s %s %s %s",
utils.ansi_codes[opts.hls.buf_nr](string.format("%4s", mark)),
utils.ansi_codes[opts.hls.path_linenr](string.format("%4s", tostring(line))),
utils.ansi_codes[opts.hls.path_colnr](string.format("%3s", tostring(col))),
text))
end
-- local buffer marks
for _, m in ipairs(vim.fn.getmarklist(buf)) do
local mark, lnum, col = m.mark:sub(2, 2), m.pos[2], m.pos[3]
local text = vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1]
add_mark(mark, lnum, col, utils.ansi_from_hl("Directory", text or "-invalid-"))
end
-- global marks
for _, m in ipairs(vim.fn.getmarklist()) do
local mark, bufnr, lnum, col, file = m.mark:sub(2, 2), m.pos[1], m.pos[2], m.pos[3],
m.file --[[@as string]]
---@diagnostic disable-next-line: assign-type-mismatch
file = make_entry.file(file, opts)
if bufnr == utils.CTX().bufnr then
local text = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1]
add_mark(mark, lnum, col, utils.ansi_from_hl("Directory", text or "-invalid-"))
else
add_mark(mark, lnum, col, file or "-invalid-")
end
end
if opts.sort then
table.sort(entries, function(a, b) return a < b end)
end
table.insert(entries, 1,
string.format("%-5s %s %s %s", "mark", "line", "col", "file/text"))
vim.tbl_map(cb, entries)
cb(nil)
end
opts.fzf_opts["--header-lines"] = 1
--[[ opts.preview = function (args, fzf_lines, _)
local mark = args[1]:match("[^ ]+")
local bufnr, lnum, _, _ = unpack(vim.fn.getpos("'"..mark))
if vim.api.nvim_buf_is_loaded(bufnr) then
return vim.api.nvim_buf_get_lines(bufnr, lnum, fzf_lines+lnum, false)
else
local name = vim.fn.expand(args[1]:match(".* (.*)"))
if vim.fn.filereadable(name) ~= 0 then
return vim.fn.readfile(name, "", fzf_lines)
end
return "UNLOADED: " .. name
end
end ]]
return core.fzf_exec(contents, opts)
end
---@param opts fzf-lua.config.Registers|{}?
---@return thread?, string?, table?
M.registers = function(opts)
---@type fzf-lua.config.Registers
opts = config.normalize_opts(opts, "registers")
if not opts then return end
local registers = { [["]], "_", "#", "=", "_", "/", "*", "+", ":", ".", "%" }
-- named
for i = 0, 9 do
table.insert(registers, tostring(i))
end
-- alphabetical
for i = 65, 90 do
table.insert(registers, string.char(i))
end
if type(opts.filter) == "string" or type(opts.filter) == "function" then
local filter = type(opts.filter) == "function" and opts.filter
or function(r)
return r:match(opts.filter) ~= nil
end
registers = vim.tbl_filter(filter, registers)
end
local function register_escape_special(reg, nl)
if not reg then return end
local gsub_map = {
["\3"] = "^C", -- <C-c>
["\27"] = "^[", -- <Esc>
["\18"] = "^R", -- <C-r>
}
for k, v in pairs(gsub_map) do
reg = reg:gsub(k, utils.ansi_codes.magenta(v))
end
return not nl and reg
or nl == 2 and reg:gsub("\n$", "")
or reg:gsub("\n", utils.ansi_codes.magenta("\\n"))
end
local entries = {}
for _, r in ipairs(registers) do
-- pcall as this could fail with:
-- E5108: Error executing lua Vim:clipboard:
-- provider returned invalid data
local _, contents = pcall(vim.fn.getreg, r)
contents = register_escape_special(contents, opts.multiline and 2 or 1)
if (contents and #contents > 0) or not opts.ignore_empty then
local regtype = vim.fn.getregtype(r)
local function convert_regtype(t)
local first = string.sub(t, 1, 1)
if first == "v" then
return "c"
elseif first == "V" then
return "l"
elseif first == "\22" then
return "b" .. string.sub(t, 2)
else
return t
end
end
local reg_fmt = require("fzf-lua.utils").ansi_codes.yellow(r)
local regtype_fmt = require("fzf-lua.utils").ansi_codes.blue(convert_regtype(regtype))
entries[#entries + 1] = string.format("[%s] [%s] %s", reg_fmt, regtype_fmt, contents)
end
end
opts.preview = function(args)
local r = args[1]:match("%[(.*)%] ")
local _, contents = pcall(vim.fn.getreg, r)
return contents and register_escape_special(contents) or args[1]
end
return core.fzf_exec(entries, opts)
end
---@param opts fzf-lua.config.Keymaps|{}?
---@return thread?, string?, table?
M.keymaps = function(opts)
---@type fzf-lua.config.Keymaps
opts = config.normalize_opts(opts, "keymaps")
if not opts then return end
local key_modes = opts.modes or { "n", "i", "c", "v", "t" }
local modes = {
n = "blue",
i = "red",
c = "yellow",
v = "magenta",
t = "green"
}
local keymaps = {}
local separator = "│"
local fields = { "mode", "lhs", "desc", "rhs" }
local field_fmt = { mode = "%s", lhs = "%-14s", desc = "%-33s", rhs = "%s" }
if opts.show_desc == false then field_fmt.desc = nil end
if opts.show_details == false then field_fmt.rhs = nil end
local format = function(info)
info.desc = field_fmt.rhs and string.sub(info.desc or "", 1, 33) or info.desc
local ret
for _, f in ipairs(fields) do
if field_fmt[f] then
ret = string.format("%s%s" .. field_fmt[f], ret or "",
ret and string.format(" %s ", separator) or "", info[f] or "")
end
end
return ret
end
local function add_keymap(keymap)
-- ignore dummy mappings
if type(keymap.rhs) == "string" and #keymap.rhs == 0 then
return
end
-- by default we ignore <SNR> and <Plug> mappings
if type(keymap.lhs) == "string" and type(opts.ignore_patterns) == "table" then
for _, p in ipairs(opts.ignore_patterns) do
-- case insensitive pattern match
local pattern, lhs = p:lower(), vim.trim(keymap.lhs:lower())
if lhs:match(pattern) then
return
end
end
end
keymap.str = format({
mode = utils.ansi_codes[modes[keymap.mode] or "blue"](keymap.mode),
lhs = keymap.lhs:gsub("%s", "<Space>"),
-- desc can be a multi-line string, normalize it
desc = keymap.desc and string.gsub(keymap.desc, "\n%s+", "\r"),
rhs = keymap.rhs or string.format("%s", tostring(keymap.callback))
})
local k = string.format("[%s:%s:%s]", keymap.buffer, keymap.mode, keymap.lhs)
keymaps[k] = keymap
end
for _, mode in pairs(key_modes) do
local global = vim.api.nvim_get_keymap(mode)
for _, keymap in pairs(global) do
add_keymap(keymap)
end
local buf_local = vim.api.nvim_buf_get_keymap(0, mode)
for _, keymap in pairs(buf_local) do
add_keymap(keymap)
end
end
local entries = {}
for _, v in pairs(keymaps) do
table.insert(entries, v.str)
end
opts.fzf_opts["--header-lines"] = "1"
-- sort alphabetically
table.sort(entries)
local header_str = format({ mode = "m", lhs = "keymap", desc = "description", rhs = "detail" })
table.insert(entries, 1, header_str)
return core.fzf_exec(entries, opts)
end
---@param opts fzf-lua.config.NvimOptions|{}?
---@return thread?, string?, table?
M.nvim_options = function(opts)
---@type fzf-lua.config.NvimOptions
opts = config.normalize_opts(opts, "nvim_options")
if not opts then return end
local format_str = function(info)
local fields = { "option", "value" }
local field_fmt = { option = "%-20s", value = "%s" }
local ret
for _, f in ipairs(fields) do
if field_fmt[f] then
ret = string.format(
"%s%s" .. field_fmt[f],
ret or "",
ret and string.format(" %s ", utils.ansi_codes["grey"](opts.separator))
or " ",
info[f] or ""
)
end
end
return ret
end
local format_option_entries = function()
local entries = {}
for _, v in pairs(vim.api.nvim_get_all_options_info()) do
local ok, value = pcall(vim.api.nvim_get_option_value, v.name, {})
if ok then
local color_value = utils.ansi_codes["grey"](tostring(value))
if value == true and opts.color_values then
color_value = utils.ansi_codes["green"](tostring(value))
elseif value == false and opts.color_values then
color_value = utils.ansi_codes["red"](tostring(value))
end
local str = format_str({ option = v.name, value = color_value })
table.insert(entries, str)
end
end
table.sort(entries)
local header = format_str({ option = "Option", value = "Value" })
local keymaps = (":: %s %s, %s %s"):format(
utils.ansi_from_hl(opts.hls.header_bind, "<enter>"),
utils.ansi_from_hl(opts.hls.header_text, "local scope"),
utils.ansi_from_hl(opts.hls.header_bind, "<alt-enter>"),
utils.ansi_from_hl(opts.hls.header_text, "global scope"))
table.insert(entries, 1, keymaps)
table.insert(entries, 2, header)
return entries
end
local contents = function(cb)
vim.api.nvim_win_call(opts.__CTX.winid, function()
coroutine.wrap(function()
local co = coroutine.running()
---@cast co thread
local entries = format_option_entries()
for _, entry in pairs(entries) do
vim.schedule(function()
cb(entry, function()
coroutine.resume(co)
end)
end)
coroutine.yield()
end
cb()
end)()
end)
end
opts.fzf_opts["--header-lines"] = "2"
return core.fzf_exec(contents, opts)
end
---@param opts fzf-lua.config.SpellSuggest|{}?
---@return thread?, string?, table?
M.spell_suggest = function(opts)
---@type fzf-lua.config.SpellSuggest
opts = config.normalize_opts(opts, "spell_suggest")
if not opts then return end
local match = opts.word_pattern or "[^%s\"'%(%)%.%%%+%-%*%?%[%]%^%$:#,]*"
local line = vim.api.nvim_get_current_line()
local col = vim.api.nvim_win_get_cursor(0)[2] + 1
local before = col > 1 and assert(line:sub(1, col - 1):reverse():match(match)):reverse() or ""
local after = line:sub(col):match(match) or ""
-- special case when the cursor is on the left surrounding char
if #before == 0 and #after == 0 and #line > col then
col = col + 1
after = line:sub(col):match(match) or ""
end
local cursor_word = before .. after
local entries = vim.fn.spellsuggest(cursor_word)
opts.complete = function(selected, _o, l, _)
if #selected == 0 then return end
local replace_at = col - #before
local before_path = replace_at > 1 and l:sub(1, replace_at - 1) or ""
local rest_of_line = #l >= (col + #after) and l:sub(col + #after) or ""
return before_path .. selected[1] .. rest_of_line,
-- this goes to `nvim_win_set_cursor` which is 0-based
replace_at + #selected[1] - 2
end
if utils.tbl_isempty(entries) then return end
return core.fzf_exec(entries, opts)
end
---@param opts fzf-lua.config.Filetypes|{}?
---@return thread?, string?, table?
M.filetypes = function(opts)
---@type fzf-lua.config.Filetypes
opts = config.normalize_opts(opts, "filetypes")
if not opts then return end
local entries = vim.fn.getcompletion("", "filetype")
if utils.tbl_isempty(entries) then return end
if opts.file_icons then
entries = vim.tbl_map(function(ft)
local buficon, hl = devicons.icon_by_ft(ft)
if not buficon then buficon = " " end
if hl then buficon = utils.ansi_from_hl(hl, buficon) end
return string.format("%s%s%s", buficon, utils.nbsp, ft)
end, entries)
end
return core.fzf_exec(entries, opts)
end
---@param opts fzf-lua.config.Packadd|{}?
---@return thread?, string?, table?
M.packadd = function(opts)
---@type fzf-lua.config.Packadd
opts = config.normalize_opts(opts, "packadd")
if not opts then return end
local entries = vim.fn.getcompletion("", "packadd")
if utils.tbl_isempty(entries) then return end
return core.fzf_exec(entries, opts)
end
---@param opts fzf-lua.config.Menus|{}?
---@return thread?, string?, table?
M.menus = function(opts)
---@type fzf-lua.config.Menus
opts = config.normalize_opts(opts, "menus")
if not opts then return end
-- @param prefix will be prepended to the entry name
local function gen_menu_entries(prefix, entry)
local name = prefix and ("%s.%s"):format(prefix, entry.name) or entry.name
if entry.submenus then
-- entry.submenus is a list of {}
return vim.tbl_map(
function(x)
return gen_menu_entries(name, x)
end, entry.submenus)
else
-- if we reached a leaf
return name
end
end
local entries = utils.tbl_flatten(vim.tbl_map(
function(x)
return gen_menu_entries(nil, x)
end, vim.fn.menu_get("")))
if utils.tbl_isempty(entries) then
utils.info("No menus available")
return
end
return core.fzf_exec(entries, opts)
end
---@param opts fzf-lua.config.Autocmds|{}?
---@return thread?, string?, table?
M.autocmds = function(opts)
---@type fzf-lua.config.Autocmds
opts = config.normalize_opts(opts, "autocmds")
if not opts then return end
local autocmds = vim.api.nvim_get_autocmds({})
if not autocmds or utils.tbl_isempty(autocmds) then
return
end
local separator = "│"
local fields = { "event", "pattern", "group", "code", "desc" }
local field_fmt = {
event = "%-28s",
pattern = "%-22s",
group = "%-40s",
code = "%-44s",
desc = "%s",
}
if opts.show_desc == false then field_fmt.desc = nil end
local format = function(info)
local ret
for _, f in ipairs(fields) do
if field_fmt[f] then
local fmt = field_fmt[f]
if info.color == false then
local len = tonumber(fmt:match("%d+"))
if len then
fmt = fmt:gsub("%d+", tostring(len - 11))
end
end
ret = string.format("%s%s" .. fmt, ret or "",
ret and string.format(" %s ", separator) or "", info[f] or "")
end
end
return ret
end
local contents = function(cb)
coroutine.wrap(function()
local co = coroutine.running()
---@cast co thread
cb(string.format("%s:%d:%s%s", "<none>", 0, separator, format({
event = "event",
pattern = "pattern",
group = "group",
code = "code",
desc = "description",
color = false,
})), function(err)
coroutine.resume(co)
if err then cb(nil) end
end)
for _, a in ipairs(autocmds) do
local file, line = "<none>", 0
if a.callback then
local info = debug.getinfo(a.callback, "S")
file = info and info.source and info.source:sub(2) or ""
line = info and info.linedefined or 0
end
local entry = string.format("%s:%d:%s%s", file, line, separator, format({
event = utils.ansi_codes.blue(a.event),
pattern = utils.ansi_codes.yellow(a.pattern),
group = utils.ansi_codes.green(a.group_name and vim.trim(tostring(a.group_name)) or " "),
code = a.callback and utils.ansi_codes.red(tostring(a.callback)) or a.command,
desc = a.desc,
}))
cb(entry, function(err)
coroutine.resume(co)
if err then cb(nil) end
end)
coroutine.yield()
end
cb(nil)
end)()
end
opts.fzf_opts["--header-lines"] = "1"
return core.fzf_exec(contents, opts)
end
---Scan the platform listener namespace for candidate servers (`nvim*`
---sockets under `stdpath("run")/..` on unix, `\\.\pipe\nvim*` named pipes
---via glob on Windows). Same role on both platforms; `serverlist()` only
---ever reports this process, so the scan is what finds peers (including
---detached servers, which keep their listener).
---@return string[] listener paths
local function scan_listeners()
if utils.__IS_WINDOWS then
return vim.fn.glob([[\\.\pipe\nvim*]], false, true)
end
local root = vim.fs.normalize(vim.fn.stdpath("run") .. "/..")
local found = vim.fs.find(function(name, _)
return name:match("nvim.*")
end, { path = root, type = "socket", limit = math.huge })
return found
end
---Discover running Neovim servers: the listener scan, authoritative on both
---platforms. `vim.fn.serverlist()` only ever reports this process (the
---`--listen`/default address plus any `serverstart()` listeners), so it adds
---no peer candidates; every address it reports is instead excluded below in
---all slash forms, so none of our own sockets/pipes can be listed. Entries
---failing an RPC probe, ourselves and our own `fzf-lua*` infra are skipped
---everywhere; our headless spawns (`scripts/cli.lua`, `spawn.lua`,
---`rpc.lua`, `win_spawn.lua`) additionally stop their default listener
---(`serverstop`), so they never register a pipe at all (every Windows nvim
---listens by default); every other responsive listener is listed, including
---UI-less `--embed` backends (indistinguishable from detached user
---sessions). Addresses stay in native
---form; `utils.serverlist_parse_entry` repairs the single backslash
---fzf/cmd eats from `\\.\pipe\...` on the way back.
---Returns an iterator of responsive server addresses.
---@param listed string[]? explicit candidates (tests), defaults per platform
local function discover_servers(listed)
local servers ---@type string[]
if listed then
servers = vim.list_extend({}, listed)
else
-- peers come from the listener scan alone; `serverlist()` is used only
-- to exclude our own addresses further down, never to populate candidates
servers = {}
end
local seen = utils.list_to_map(servers)
for _, s in ipairs(scan_listeners()) do
if not seen[s] then
servers[#servers + 1] = s
seen[s] = true
end
end
local dedup = {}
local self_bs, self_fs = vim.v.servername or "", (vim.v.servername or ""):gsub("\\", "/")
-- every address this process owns: `vim.v.servername` is only the
-- `--listen`/default one, while `serverlist()` also reports extra
-- `serverstart()` listeners; add each in all slash forms (runtimes
-- disagree on the reported separator)
local own = {}
for _, s in ipairs(vim.fn.serverlist()) do
own[s] = true
if utils.__IS_WINDOWS then
own[(s:gsub("\\", "/"))] = true
own[(s:gsub("/", "\\"))] = true
end
end
local filter = function(socket)
-- `fzf-lua*` is our own picker infrastructure; never list ourselves
-- (swap/kill/preview of self is meaningless at best). `nvim-term-*`
-- pty pipes are windows-only: on `\\.\pipe\` probing them stalls until
-- timeout, elsewhere they are probed like any other listener.
-- Self is compared in both slash forms: runtimes disagree on the
-- reported separator and a miss here lists (or kills!) ourselves.
if dedup[socket]
or socket == self_bs
or (utils.__IS_WINDOWS and socket == self_fs)
or own[socket]
or (utils.__IS_WINDOWS and socket:match("nvim%-term%-"))
or socket:match("fzf%-lua") then
return false
end
dedup[socket] = true
if not utils.server_probe(socket) then return false end
-- Deliberately no `--embed`/UI filtering: an `--embed` backend with
-- no UI is indistinguishable from a detached user session (same argv,
-- same empty UI list), and hiding them emptied the picker for
-- embed-based workflows.
return true
end
---@diagnostic disable-next-line: redundant-parameter, call-non-callable
return vim.iter(servers):filter(filter)
end
-- Test seam (cf. `undotree._draw_tree`): exercise discovery without UI.
M._discover_servers = discover_servers
---List, preview, kill and attach to running Neovim servers.
---@param opts fzf-lua.config.Serverlist|{}?
---@return thread?, string?, table?
M.serverlist = function(opts)
-- libuv normalizes `ru_maxrss` to kilobytes on all platforms
local function fmt_mem(rusage)
if not rusage or not rusage.maxrss then return "?" end
local kb = rusage.maxrss
if kb < 1024 then return string.format("%dK", kb) end
if kb < 1024 * 1024 then return string.format("%.1fM", kb / 1024) end
return string.format("%.2fG", kb / (1024 * 1024))
end
local function cpu_time_us(rusage)
if not rusage or not rusage.utime or not rusage.stime then return nil end
return (rusage.utime.sec * 1e6 + rusage.utime.usec)
+ (rusage.stime.sec * 1e6 + rusage.stime.usec)
end
local function fmt_cpu(pct)
if not pct then return " ?" end
if pct < 10 then return string.format("%.1f%%", pct) end
if pct < 100 then return string.format("%3.0f%%", pct) end
return string.format("%4.0f%%", pct)
end
opts = require("fzf-lua.config").normalize_opts(opts or {}, "serverlist")
local SAMPLE_INTERVAL_MS = 50
local f = function(cb)
local entries = {}
discover_servers():each(function(p)
local ok, ret = utils.rpcexec_timed(p, "nvim_exec_lua",
"return { vim.uv.cwd(), vim.uv.getrusage(), vim.uv.hrtime() }", {})
if not ok or type(ret) ~= "table" then return end
local cwd, rusage, wall_start = ret[1], ret[2], ret[3]
if not cwd then return end
entries[#entries + 1] = {
cwd = FzfLua.path.normalize(cwd),
rusage = rusage,
wall_start = wall_start,
socket = p,
}
end)
vim.defer_fn(function()
for _, e in ipairs(entries) do
local ok, ret = utils.rpcexec_timed(e.socket, "nvim_exec_lua",
"return { vim.uv.getrusage(), vim.uv.hrtime() }", {})
if ok and type(ret) == "table" then
local rusage2, wall_end = ret[1], ret[2]
if rusage2 and wall_end and e.wall_start then
local cpu1, cpu2 = cpu_time_us(e.rusage), cpu_time_us(rusage2)
if cpu1 and cpu2 and wall_end > e.wall_start then
e.cpu_pct = (cpu2 - cpu1) / ((wall_end - e.wall_start) / 1000) * 100
end
end
end
end
local max_cwd = 0
for _, e in ipairs(entries) do
if #e.cwd > max_cwd then max_cwd = #e.cwd end
end
for _, e in ipairs(entries) do
cb(("%-" .. max_cwd .. "s %s %s%s%s"):format(
e.cwd,
utils.ansi_codes.cyan(string.format("%7s", fmt_mem(e.rusage))),
utils.ansi_codes.yellow(string.format("%5s", fmt_cpu(e.cpu_pct))),
utils.nbsp,
utils.ansi_codes.grey(e.socket)))
end
cb(nil)
end, SAMPLE_INTERVAL_MS)
end
---@cast opts table
if utils.has(opts, "sk", "3.0.0") then
opts = vim.tbl_deep_extend("force", opts, { winopts = { preview = { pty = true } } }) --[[@as table]]
end
---@diagnostic disable-next-line: need-check-nil
if utils.has(opts, "fzf", "0.73.0") then
---@cast opts table
opts.actions = opts.actions or {}
opts.actions["every(2)"] = { fn = function() end, reload = true }
end
core.fzf_exec(f, opts)
end
return M