local utils = require "fzf-lua.utils"
local libuv = require "fzf-lua.libuv"
local actions = require "fzf-lua.actions"
local path = require "fzf-lua.path"
local api = vim.api
local fn = vim.fn
---@class fzf-lua.PathShortener
---@field _ns integer?
---@field _wins table<integer, { bufnr: integer, shorten_len: integer }>
local PathShortener = {}
PathShortener._wins = {}
-- Hoist constant byte values for performance
local DOT_BYTE = path.dot_byte
function PathShortener.setup()
if PathShortener._ns then return end
PathShortener._ns = api.nvim_create_namespace("fzf-lua.win.path_shorten")
-- Register decoration provider with ephemeral extmarks
api.nvim_set_decoration_provider(PathShortener._ns, {
on_win = function(_, winid, bufnr, _topline, _botline)
-- Only process registered fzf windows
local win_data = PathShortener._wins[winid]
if not win_data or win_data.bufnr ~= bufnr then
return false
end
return true -- Continue to on_line callbacks
end,
on_line = function(_, winid, bufnr, row)
local win_data = PathShortener._wins[winid]
if not win_data then return end
PathShortener._apply_line(bufnr, row, win_data.shorten_len)
end,
})
end
---Apply path shortening to a single line using ephemeral extmarks
---@param buf integer buffer number
---@param row integer 0-indexed line number
---@param shorten_len integer number of characters to keep
function PathShortener._apply_line(buf, row, shorten_len)
local lines = api.nvim_buf_get_lines(buf, row, row + 1, false)
local line = lines[1]
if not line or #line == 0 then return end
-- Find the path portion of the line
-- Lines may have prefixes like icons separated by nbsp (U+2002)
-- Format: [fzf_pointer] [git_icon nbsp] [file_icon nbsp] path[:line:col:text]
-- When file_icons=false, there's no nbsp separator before the path
-- The fzf terminal also adds pointer/marker prefix (e.g., "> " or " ")
local path_start = 1
local last_nbsp = line:find(utils.nbsp, 1, true)
if last_nbsp then
-- Find the last nbsp (there may be multiple with git_icons + file_icons)
repeat
path_start = last_nbsp + #utils.nbsp
last_nbsp = line:find(utils.nbsp, path_start, true)
until not last_nbsp
else
-- No nbsp means no icons - skip fzf's pointer/marker prefix
-- Paths start with: alphanumeric, `/`, `~`, `.`, or drive letter (Windows)
-- Skip leading whitespace and pointer characters until we hit a path char
local first_path_char = line:find("[%w/~%.]")
if first_path_char then
path_start = first_path_char
end
end
-- Find where the path ends (at first colon after path_start, if any)
-- But be careful with Windows paths like C:\...
local path_end = #line
local colon_search_start = path_start
-- On Windows, skip the drive letter colon (e.g., C:)
-- Check if char at path_start+1 is colon AND char at path_start+2 is a path separator
if string.byte(line, path_start + 1) == path.colon_byte
and path.byte_is_separator(string.byte(line, path_start + 2)) then
colon_search_start = path_start + 2
end
local colon_pos = line:find(":", colon_search_start)
if colon_pos then
path_end = colon_pos - 1
end
-- Now process the path portion for shortening
-- We need to conceal directory components, keeping only `shorten_len` chars
local path_portion = line:sub(path_start, path_end)
-- Use path.find_next_separator to iterate through directory components
local prev_sep = 0
local sep_pos = path.find_next_separator(path_portion, 1)
while sep_pos do
local component_start = prev_sep + 1
local component = path_portion:sub(component_start, sep_pos - 1)
local component_len = #component
-- Count UTF-8 characters using vim.str_utfindex
-- Use "utf-32" to count code points (actual characters), not bytes.
-- "utf-8" would give byte positions, "utf-16" gives UTF-16 code units.
local _, component_charlen = vim.str_utfindex(component, "utf-32")
component_charlen = component_charlen or component_len -- fallback to byte length
-- Only conceal if the component has more characters than shorten_len
if component_charlen > shorten_len then
-- Handle special case: component starts with '.' (hidden files/dirs)
local keep_chars = shorten_len
if string.byte(component, 1) == DOT_BYTE and component_charlen > shorten_len + 1 then
-- Keep the dot plus shorten_len characters
keep_chars = shorten_len + 1
end
-- Bounds check to prevent errors
keep_chars = math.min(keep_chars, component_charlen)
-- Convert character count to byte offset using vim.str_byteindex
local keep_bytes = vim.str_byteindex(component, "utf-32", keep_chars, false)
keep_bytes = keep_bytes or keep_chars -- fallback to character count (ASCII approximation)
-- Calculate 0-indexed byte positions in the full line for extmark
-- path_start is 1-indexed, component_start is 1-indexed within path_portion
local line_offset = path_start - 1 + component_start - 1
local conceal_start = line_offset + keep_bytes
local conceal_end = line_offset + component_len -- end of component (before separator)
if conceal_end > conceal_start then
---@diagnostic disable-next-line: param-type-mismatch
pcall(api.nvim_buf_set_extmark, buf, PathShortener._ns, row, conceal_start, {
end_col = conceal_end,
conceal = "",
ephemeral = true,
})
end
end
prev_sep = sep_pos
sep_pos = path.find_next_separator(path_portion, sep_pos + 1)
end
end
---Register a window for path shortening
---@param winid integer window ID
---@param bufnr integer buffer number
---@param shorten_len integer|boolean number of characters to keep
function PathShortener.attach(winid, bufnr, shorten_len)
if not winid or not bufnr then return end
PathShortener.setup()
---@diagnostic disable-next-line: assign-type-mismatch
shorten_len = (shorten_len == true) and 1 or tonumber(shorten_len) or 1
---@diagnostic disable-next-line: assign-type-mismatch
PathShortener._wins[winid] = { bufnr = bufnr, shorten_len = shorten_len }
end
---Unregister a window from path shortening
---@param winid integer window ID
function PathShortener.detach(winid)
if not winid then return end
PathShortener._wins[winid] = nil
end
---@alias fzf-lua.win.previewPos "up"|"down"|"left"|"right"
---@alias fzf-lua.win.previewLayout { pos: fzf-lua.win.previewPos, size: number, str: string }
---@class fzf-lua.config.WinoptsResolved: fzf-lua.config.Winopts
---@field height integer
---@field width integer
---@field row integer
---@field col integer
---@field zindex integer
---@field preview fzf-lua.config.PreviewOpts
---@class fzf-lua.Win
---@field winopts fzf-lua.config.WinoptsResolved
---@field actions fzf-lua.config.Actions|{}
---@field hls fzf-lua.config.HLS
---@field fzf_bufnr integer
---@field fzf_winid integer
---@field preview_hidden? boolean
---@field preview_wrap? boolean
---@field fullscreen? boolean
---@field layout? fzf-lua.WinLayout
---@field tsinjector? fzf-lua.TSInjector
---@field previewer? fun(...)|table|string
---@field _hidden_fzf_bufnr? integer
---@field toggle_behavior? "extend"|"default"
---@field _previewer? fzf-lua.previewer.Builtin|fzf-lua.previewer.Fzf
---@field _preview_pos_force? fzf-lua.win.previewPos
---@field last_view? [integer, integer, integer]
---@field on_closes table<any, function>
---@field _o fzf-lua.config.Resolved
local FzfWin = {}
-- singleton instance used by "_exported_wapi"
---@type fzf-lua.Win?
local _self = nil
function FzfWin.__SELF()
return _self
end
local _preview_keymaps = {
["toggle-preview-wrap"] = { module = "win", fnc = "toggle_preview_wrap()" },
["toggle-preview-ts-ctx"] = { module = "win", fnc = "toggle_preview_ts_ctx()" },
["toggle-preview-undo"] = { module = "win", fnc = "toggle_preview_undo_diff()" },
["preview-ts-ctx-inc"] = { module = "win", fnc = "preview_ts_ctx_inc_dec(1)" },
["preview-ts-ctx-dec"] = { module = "win", fnc = "preview_ts_ctx_inc_dec(-1)" },
["preview-up"] = { module = "win", fnc = "preview_scroll('line-up')" },
["preview-down"] = { module = "win", fnc = "preview_scroll('line-down')" },
["preview-page-up"] = { module = "win", fnc = "preview_scroll('page-up')" },
["preview-page-down"] = { module = "win", fnc = "preview_scroll('page-down')" },
["preview-half-page-up"] = { module = "win", fnc = "preview_scroll('half-page-up')" },
["preview-half-page-down"] = { module = "win", fnc = "preview_scroll('half-page-down')" },
["preview-reset"] = { module = "win", fnc = "preview_scroll('reset')" },
["preview-top"] = { module = "win", fnc = "preview_scroll('top')" },
["preview-bottom"] = { module = "win", fnc = "preview_scroll('bottom')" },
["focus-preview"] = { module = "win", fnc = "focus_preview()" },
}
function FzfWin:setup_keybinds()
self.keymap = type(self.keymap) == "table" and self.keymap or {}
self.keymap.fzf = type(self.keymap.fzf) == "table" and self.keymap.fzf or {}
self.keymap.builtin = type(self.keymap.builtin) == "table" and self.keymap.builtin or {}
local keymap_tbl = {
["hide"] = { module = "win", fnc = "hide()" },
["toggle-help"] = { module = "win", fnc = "toggle_help()" },
["toggle-fullscreen"] = { module = "win", fnc = "toggle_fullscreen()" },
["toggle-preview"] = { module = "win", fnc = "toggle_preview()" },
["toggle-preview-cw"] = { module = "win", fnc = "toggle_preview_cw(1)" },
["toggle-preview-ccw"] = { module = "win", fnc = "toggle_preview_cw(-1)" },
["toggle-preview-behavior"] = { module = "win", fnc = "toggle_preview_behavior()" },
}
-- use signal when user bind toggle-preview in FZF_DEFAULT_OPTS/FZF_DEFAULT_FILE_OPTS
local function on_SIGWINCH_toggle_preview()
if utils.__IS_WINDOWS then return end -- not sure why ci fail on windows
self.on_SIGWINCH(self._o, "toggle-preview", function(args)
-- hide if visible but do not toggle if hidden as we want to
-- make sure the right layout is set if user rotated the preview
if tonumber(args[1]) then
return "toggle-preview"
else
-- NOTE: always equals?
-- self = _self or self -- may differ with `... resume previewer=...`
return string.format("change-preview-window(%s)", self:normalize_preview_layout().str)
end
end)
end
local function on_SIGWINCH_toggle_preview_cw()
if utils.__IS_WINDOWS then return end -- not sure why ci fail on windows
self.on_SIGWINCH(self._o, "toggle-preview-cw", function(args)
-- only set the layout if preview isn't hidden
if not tonumber(args[1]) then return end
-- NOTE: always equals?
-- self = _self or self -- may differ with `... resume previewer=...`
return string.format("change-preview-window(%s)", self:normalize_preview_layout().str)
end)
end
-- find the toggle_preview keybind, to be sent when using a split for the native
-- pseudo fzf preview window or when using native and treesitter is enabled
self._fzf_toggle_prev_bind = nil
-- use fzf-native binds in fzf-tmux or cli (shell) profile
if self._o._is_fzf_tmux or _G.fzf_jobstart then
for k, v in pairs(self.keymap.builtin) do
if type(v) == "string" and v:match("toggle%-preview%-c?cw") then
k = utils.neovim_bind_to_fzf(k)
self.keymap.fzf[k] = "transform:" .. FzfLua.shell.stringify_data(function(args, _, _)
-- only set the layout if preview isn't hidden
if not tonumber(args[1]) then return end
assert(keymap_tbl[v])
assert(loadstring(string.format([[require("fzf-lua.%s").%s]],
keymap_tbl[v].module, keymap_tbl[v].fnc)))()
return string.format("change-preview-window(%s)", self:normalize_preview_layout().str)
end, {}, utils.__IS_WINDOWS and "%FZF_PREVIEW_LINES%" or "$FZF_PREVIEW_LINES")
end
end
elseif self.winopts.split or not self.previewer_is_builtin then
-- sync toggle-preview
-- 1. always run the toggle-preview(), and self._fzf_toggle_prev_bind
for k, v in pairs(self.keymap.builtin) do
if v == "toggle-preview" then
on_SIGWINCH_toggle_preview()
self.keymap.fzf[utils.neovim_bind_to_fzf(k)] = v
end
if type(v) == "string" and v:match("toggle%-preview%-c?cw") then
on_SIGWINCH_toggle_preview_cw()
end
end
for k, v in pairs(self.keymap.fzf) do
if v == "toggle-preview" then
on_SIGWINCH_toggle_preview()
self._fzf_toggle_prev_bind = utils.fzf_bind_to_neovim(k)
self.keymap.builtin[self._fzf_toggle_prev_bind] = v
end
if type(v) == "string" and v:match("toggle%-preview%-c?cw") then
on_SIGWINCH_toggle_preview_cw()
self.keymap.fzf[k] = nil -- invalid fzf bind, user set bind by mistake
end
end
self._fzf_toggle_prev_bind = self._fzf_toggle_prev_bind or true
end
if self.previewer_is_builtin then
-- These maps are only valid for the builtin previewer
keymap_tbl = vim.tbl_deep_extend("keep", keymap_tbl, _preview_keymaps) --[[@as table]]
end
local function funcref_str(keymap)
return ([[<Cmd>lua require('fzf-lua.%s').%s<CR>]]):format(keymap.module, keymap.fnc)
end
for key, action in pairs(self.keymap.builtin) do
local keymap = keymap_tbl[action]
if keymap and not utils.tbl_isempty(keymap) and action ~= false then
vim.keymap.set("t", key, funcref_str(keymap), { nowait = true, buffer = self.fzf_bufnr })
end
end
-- If the user did not override the Esc action ensure it's
-- not bound to anything else such as `<C-\><C-n>` (#663)
if self.actions["esc"] == actions.dummy_abort and not self.keymap.builtin["<esc>"] then
vim.keymap.set("t", "<Esc>", "<Esc>", { buffer = self.fzf_bufnr, nowait = true })
end
end
-- check if previewer useable (not matter if it's hidden)
function FzfWin:has_previewer()
return self._o.preview or self._previewer and true or false
end
---@return fzf-lua.win.previewLayout
function FzfWin:normalize_preview_layout()
local preview_str, pos ---@type string, fzf-lua.win.previewPos
if self._preview_pos_force then
-- Get the correct layout string and size when set from `:toggle_preview_cw`
preview_str = assert((self._preview_pos_force == "up" or self._preview_pos_force == "down")
and self.winopts.preview.vertical or self.winopts.preview.horizontal)
pos = self._preview_pos_force
else
preview_str = self:fzf_preview_layout_str()
pos = preview_str:match("[^:]+") or "right"
end
local percent = tonumber(preview_str:match(":(%d+)%%"))
local abs = not percent and tonumber(preview_str:match(":(%d+)")) or nil
percent = percent or 50
return {
pos = pos,
size = abs or (percent / 100),
str = string.format("%s:%s", pos, tostring(abs or percent) .. ((not abs) and "%" or ""))
}
end
---@return integer nwin, boolean preview
function FzfWin:normalize_layout()
-- when to use full fzf layout
-- 1. no previewer (always)
-- 2. builtin previewer (hidden and not "extend")
-- 3. fzf previewer (not "extend" or not hidden)
if not self:has_previewer()
or (self.previewer_is_builtin and (self.preview_hidden and self.toggle_behavior ~= "extend"))
or (not self.previewer_is_builtin and (self.toggle_behavior ~= "extend" or not self.preview_hidden)) then
return 1, false
end
-- has previewer, but when nwin=1, reduce fzf main layout as if the previewer is displayed
local nwin = self.preview_hidden and self.toggle_behavior == "extend" and 1 or 2
return nwin, true
end
---@class fzf-lua.WinLayout
---@field fzf vim.api.keyset.win_config
---@field preview? vim.api.keyset.win_config
---@return fzf-lua.WinLayout
function FzfWin:generate_layout()
local winopts = self:normalize_winopts()
local nwin, preview = self:normalize_layout()
local layout = self:normalize_preview_layout()
local border, h, w = self:normalize_border(self._o.winopts.border, {
type = "nvim",
name = "fzf",
layout = preview and layout.pos or nil,
nwin = nwin,
opts = self._o
})
if not preview then
return {
fzf = {
row = winopts.row,
col = winopts.col,
width = winopts.width,
height = winopts.height,
border = border,
style = "minimal",
relative = winopts.relative or "editor",
zindex = winopts.zindex,
hide = winopts.hide,
}
}
end
if self.previewer_is_builtin and winopts.split then
local wininfo = utils.__HAS_NVIM_010 and api.nvim_win_get_config(self.fzf_winid) or
assert(fn.getwininfo(self.fzf_winid)[1])
-- no signcolumn/number/relativenumber (in set_style_minimal)
---@diagnostic disable-next-line: missing-fields
winopts = {
height = wininfo.height,
width = wininfo.width,
split = winopts.split,
row = 0,
col = 0,
}
end
local pwopts
local row, col = winopts.row, winopts.col
local height, width = winopts.height, winopts.width
---@cast height integer
---@cast width integer
local preview_pos, preview_size = layout.pos, layout.size
local pborder, ph, pw = self:normalize_border(self._o.winopts.preview.border,
{ type = "nvim", name = "prev", layout = preview and layout.pos, nwin = nwin, opts = self._o })
if winopts.split then
-- Custom "split"
pwopts = { relative = "win", anchor = "NW", row = 0, col = 0 }
if preview_pos == "down" or preview_pos == "up" then
pwopts.width = width - pw
pwopts.height = self:normalize_size(preview_size, height) - ph
if preview_pos == "down" then
pwopts.row = height - pwopts.height - ph
end
else -- left|right
pwopts.height = height - ph
pwopts.width = self:normalize_size(preview_size, width) - pw
if preview_pos == "right" then
pwopts.col = width - pwopts.width + pw
end
end
else
-- Float window
pwopts = { relative = "editor" }
if preview_pos == "down" or preview_pos == "up" then
pwopts.col = col
pwopts.width = width
-- https://github.com/junegunn/fzf/blob/1afd14381079a35eac0a4c2a5cacb86e2a3f476b/src/terminal.go#L1820
-- fzf's previewer border is draw inside preview window, so shrink builtin previewer if it have "top border"
-- to ensure the fzf list height is the same between fzf/builtin
local off = (preview_size < 1 and self.previewer_is_builtin and ph > 0) and 1
or (preview_size >= 1 and not self.previewer_is_builtin and ph > 0) and -ph
or 0
pwopts.height = self:normalize_size(preview_size, (height + off)) - off
height = height - pwopts.height
if preview_pos == "down" then
-- next row
pwopts.row = row + h + height
else -- up
pwopts.row = row
row = pwopts.row + ph + pwopts.height
end
-- enlarge the height to align fzf with preview win
if self.previewer_is_builtin then
width = width + math.max(pw - w, 0)
pwopts.width = pwopts.width + math.max(w - pw, 0)
end
else -- left|right
pwopts.row = row
pwopts.height = height
local off = (preview_size < 1 and self.previewer_is_builtin and pw > 0) and 1
or (preview_size >= 1 and not self.previewer_is_builtin and pw > 0) and -pw
or 0
pwopts.width = self:normalize_size(preview_size, (width + off)) - off
width = width - pwopts.width
if preview_pos == "right" then
-- next col
pwopts.col = col + w + width
else -- left
pwopts.col = col
col = pwopts.col + pw + pwopts.width
end
-- enlarge the height to align fzf with preview win
if self.previewer_is_builtin then
height = height + math.max(ph - h, 0)
pwopts.height = pwopts.height + math.max(h - ph, 0)
end
end
end
return {
fzf = vim.tbl_extend("force", { row = row, col = col, height = height, width = width }, {
style = "minimal",
border = border,
relative = winopts.relative or "editor",
zindex = winopts.zindex,
hide = winopts.hide,
}),
preview = vim.tbl_extend("force", pwopts, {
style = "minimal",
zindex = winopts.zindex,
border = pborder,
focusable = true,
hide = winopts.hide,
}),
}
end
function FzfWin:tmux_columns()
local is_popup, is_hsplit, opt_val = (function()
-- Backward compat using "fzf-tmux" script
if self._o._is_fzf_tmux == 1 then
for _, flag in ipairs({ "-l", "-r" }) do
if self._o.fzf_tmux_opts[flag] then
-- left/right split, not a popup, is an hsplit
return false, true, self._o.fzf_tmux_opts[flag]
end
end
for _, flag in ipairs({ "-u", "-d" }) do
if self._o.fzf_tmux_opts[flag] then
-- up/down split, not a popup, not an hsplit
return false, false, self._o.fzf_tmux_opts[flag]
end
end
-- Default is a popup with "-p" or without
return true, false, self._o.fzf_tmux_opts["-p"]
else
return true, false, self._o.fzf_opts["--tmux"]
end
end)()
local out = utils.io_system({
"tmux", "display-message", "-p",
is_popup and "#{window_width}" or "#{pane_width}"
})
local cols = tonumber(out:match("%d+"))
-- Calc the correct width when using tmux popup or left|right splits
-- fzf's defaults to "--tmux" is "center,50%" or "50%" for splits
if is_popup or is_hsplit then
local percent = type(opt_val) == "string" and tonumber(opt_val:match("(%d+)%%")) or 50
cols = math.floor(assert(cols) * percent / 100)
end
return cols
end
function FzfWin:columns(no_fullscreen)
-- When called from `core.preview_window` we need to get the no-fullscreen columns
-- in order to get an accurate alternate layout trigger that will also be consistent
-- when starting with `winopts.fullscreen == true`
local winopts = no_fullscreen and self._o.winopts or self.winopts
return self._o._is_fzf_tmux and self:tmux_columns()
or vim.is_callable(_G.fzf_tty_get_width) and _G.fzf_tty_get_width()
or winopts.split and api.nvim_win_get_width(self.fzf_winid or 0)
or self:normalize_size(winopts.width, vim.o.columns)
end
function FzfWin:fzf_preview_layout_str()
local columns = self:columns()
local is_hsplit = self.winopts.preview.layout == "horizontal"
or self.winopts.preview.layout == "flex" and columns > self.winopts.preview.flip_columns
return is_hsplit and self._o.winopts.preview.horizontal or self._o.winopts.preview.vertical
end
---@param border any
---@param metadata fzf-lua.win.borderMetadata
---@return fzf-lua.winborder, integer, integer
function FzfWin:normalize_border(border, metadata)
return require("fzf-lua.win.border").nvim(border, metadata, self._o.silent)
end
---@param size number|integer
---@param max integer
---@return integer
function FzfWin:normalize_size(size, max)
local _ = self
if size <= 1 then return math.floor(max * size) end
---@cast size integer
return math.min(size, max)
end
---@return fzf-lua.config.WinoptsResolved
function FzfWin:normalize_winopts()
-- make a local copy of winopts so we don't pollute the user's options
self.winopts = utils.tbl_deep_clone(self._o.winopts or {}) or {}
local winopts = self.winopts
if self.fullscreen then
-- NOTE: we set `winopts.relative=editor` so fullscreen
-- works even when the user set `winopts.relative=cursor`
winopts.relative = "editor"
winopts.row = 1
winopts.col = 1
winopts.width = 1
winopts.height = 1
end
local nwin, preview = self:normalize_layout()
local preview_pos = preview and self:normalize_preview_layout().pos or nil
if preview and self.previewer_is_builtin then nwin = 2 end
local _, h, w = self:normalize_border(self._o.winopts.border,
{ type = "nvim", name = "fzf", layout = preview_pos, nwin = nwin, opts = self._o })
if preview and self.previewer_is_builtin then
local _, ph, pw = self:normalize_border(self._o.winopts.preview.border,
{ type = "nvim", name = "prev", layout = preview_pos, nwin = nwin, opts = self._o })
if preview_pos == "up" or preview_pos == "down" then
h, w = h + ph, math.max(w, pw)
else -- left|right
h, w = math.max(h, ph), w + pw
end
end
-- #2121 we can suppress cmdline area when zindex >= 200
local ch = winopts.zindex >= 200 and 0 or vim.o.cmdheight
local max_width = vim.o.columns
local max_height = vim.o.lines - ch
winopts.width = self:normalize_size(assert(tonumber(winopts.width)), max_width)
winopts.height = self:normalize_size(assert(tonumber(winopts.height)), max_height)
if winopts.relative == "cursor" then
-- convert cursor relative to absolute ('editor'),
-- this solves the preview positioning seamlessly
-- use the calling window context for correct pos
local winid = utils.CTX().winid
local pos = api.nvim_win_get_cursor(winid)
local screenpos = fn.screenpos(winid, pos[1], pos[2])
winopts.row = math.floor((winopts.row or 0) + screenpos.row - 1)
winopts.col = math.floor((winopts.col or 0) + screenpos.col - 1)
winopts.relative = nil
end
-- make row close to the center of screen (include cmdheight)
-- avoid breaking existing test
winopts.row = self:normalize_size(assert(tonumber(winopts.row)), vim.o.lines - winopts.height)
winopts.col = self:normalize_size(assert(tonumber(winopts.col)), max_width - winopts.width)
winopts.row = math.min(winopts.row, max_height - winopts.height)
-- width/height can be used for text area
winopts.width = math.max(1, winopts.width - w)
winopts.height = math.max(1, winopts.height - h)
---@type fzf-lua.config.WinoptsResolved
return winopts
end
---@param winhls table<string, string|false>|string
---@return string
local function make_winhl(winhls)
if type(winhls) == "string" then return winhls end
local winhl = {}
for k, h in pairs(winhls) do
if h then winhl[#winhl + 1] = ("%s:%s"):format(k, h) end
end
return table.concat(winhl, ",")
end
---@param win integer
---@param pwinhl? table<string, string|false>|string preview winhl
function FzfWin:reset_winhl(win, pwinhl)
-- derive the highlights from the window type
local hls = self.hls
local winhls = pwinhl or {
Normal = hls.normal,
NormalFloat = hls.normal,
FloatBorder = hls.border,
CursorLine = hls.cursorline,
CursorLineNr = hls.cursorlinenr,
}
(pwinhl and utils.wo[win] or utils.wo[win][0]).winhl = make_winhl(winhls)
end
---@param exit_code integer
---@param fzf_bufnr integer?
function FzfWin:check_exit_status(exit_code, fzf_bufnr)
-- see the comment in `FzfWin:close` for more info
if fzf_bufnr and fzf_bufnr ~= self.fzf_bufnr then
return
end
if not self:validate() then return end
-- from 'man fzf':
-- 0 Normal exit
-- 1 No match
-- 2 Error
-- 130 Interrupted with CTRL-C or ESC
if exit_code == 2 then
local lines = api.nvim_buf_get_lines(self.fzf_bufnr, 0, 1, false)
utils.error("fzf error %d: %s", exit_code, lines and #lines[1] > 0 and lines[1] or "<null>")
end
end
function FzfWin:set_autoclose(autoclose)
self._autoclose = autoclose
end
function FzfWin:autoclose()
return self._autoclose
end
function FzfWin:set_backdrop()
-- No backdrop for split, only floats / tmux
if self.winopts.split then return end
self.on_closes.backdrop = require("fzf-lua.win.backdrop").open(self.winopts.backdrop,
self.winopts.zindex - 2, self.hls)
end
---@param o fzf-lua.config.Resolved
---@return fzf-lua.Win
function FzfWin.new(o)
if not _self then
elseif _self._hidden_fzf_bufnr then
_self:close_buf(_self._hidden_fzf_bufnr)
_self = nil
elseif not _self:hidden() then
-- utils.warn("Please close fzf-lua before starting a new instance")
_self._reuse = true
-- switch to fzf-lua's main window in case the user switched out
-- NOTE: `self.fzf_winid == nil` when using fzf-tmux
if _self.fzf_winid and _self.fzf_winid ~= api.nvim_get_current_win() then
api.nvim_set_current_win(_self.fzf_winid)
end
-- Update main win title, required for toggle action flags
_self:update_main_title(o.winopts.title)
-- refersh treesitter settings as new picker might have it disabled
-- detach previewer and refresh signal handler
-- e.g. when switch from fzf previewer to builtin previewer
_self._o = o
o.winopts.preview.hidden = _self.preview_hidden
_self:attach_previewer(nil)
return _self
end
o = o or {} ---@type fzf-lua.config.Resolved
---@type fzf-lua.Win
local self = utils.setmetatable({}, -- gc is unused now, only used to test _self is nullrified
{ __index = FzfWin, __gc = function() _G._fzf_lua_gc_called = true end })
self._o = o
self.hls = o.hls
self.actions = o.actions
self.fullscreen = o.winopts.fullscreen
self.toggle_behavior = o.winopts.toggle_behavior
self.preview_wrap = not not o.winopts.preview.wrap -- force boolean
self.preview_hidden = not not o.winopts.preview.hidden -- force boolean
self.keymap = o.keymap
self.previewer = o.previewer
self:set_autoclose(utils.nonnil(o.autoclose, true))
self.winopts = self:normalize_winopts()
self.on_closes = {}
_self = self
return self
end
---@param win integer
---@param opts vim.wo|{}
---@return vim.wo|{}
function FzfWin:get_winopts(win, opts)
local _ = self
if not win or not api.nvim_win_is_valid(win) then return {} end
local ret = {}
for opt, _ in pairs(opts) do
ret[opt] = utils.wo[win][opt]
end
return ret
end
---@param win integer
---@param opts vim.wo|{}
---@param ignore_events boolean?
---@param global boolean?
function FzfWin:set_winopts(win, opts, ignore_events, global)
local _ = self
if not win or not api.nvim_win_is_valid(win) then return end
-- NOTE: Do not trigger "OptionSet" as this will trigger treesitter-context's
-- `update_single_context` which will in turn close our treesitter-context
local ei = ignore_events and "all" or vim.o.eventignore
local wo = global ~= false and utils.wo[win] or utils.wo[win][0]
utils.eventignore(function()
for opt, value in pairs(opts) do
wo[opt] = value
end
end, ei)
end
---@param previewer fzf-lua.previewer.Builtin|fzf-lua.previewer.Fzf? nil to "detach" previewer
function FzfWin:attach_previewer(previewer)
if previewer then
previewer.win = self
previewer.delay = self.winopts.preview.delay or 100
previewer.title = self.winopts.preview.title
previewer.title_pos = self.winopts.preview.title_pos
previewer.winopts = self.winopts.preview.winopts
end
-- clear the previous previewer if existed
if self._previewer and self._previewer.close then
-- if we press ctrl-g too quickly 'previewer.preview_bufnr' will be nil
-- and even though the temp buffer is set to 'bufhidden:wipe' the buffer
-- won't be closed properly and remain lingering (visible in `:ls!`)
-- make sure the previewer is aware of this buffer
if not self._previewer.preview_bufnr and self:validate_preview() then
---@diagnostic disable-next-line: inject-field
self._previewer.preview_bufnr = api.nvim_win_get_buf(self.preview_winid)
end
if self.on_closes.preview then self.on_closes.preview() end
end
-- This makes sure previewer.base:close is always called on :close
-- (1) Used by swiper/ivy/custom previewers
-- (2) Overwritten (extended) in builtin previewer (in :redraw_preview)
self.on_closes.preview = function(hide) self:close_preview(hide) end
self._previewer = previewer
self.previewer_is_builtin = previewer and previewer.type == "builtin"
self.toggle_behavior = previewer and previewer.toggle_behavior or self.toggle_behavior
self:normalize_winopts()
end
function FzfWin:validate_preview()
return not self.closing
and self.preview_winid
and api.nvim_win_is_valid(self.preview_winid)
end
function FzfWin:redraw_preview()
if not self.previewer_is_builtin or self.preview_hidden then
return
end
local previewer = self._previewer ---@cast previewer fzf-lua.previewer.Builtin
-- Close the exisiting scrollbar
self:close_preview_scrollbar()
-- Generate the preview layout
self.layout = self:generate_layout()
local preview = assert(self.layout.preview)
if self:validate_preview() then
utils.win_set_config(self.preview_winid, preview)
else
local tmp_buf = previewer:get_tmp_buffer()
-- No autocmds, can only be sent with 'nvim_open_win'
self.preview_winid = api.nvim_open_win(tmp_buf, false,
vim.tbl_extend("force", preview, { noautocmd = true }))
-- Add win local var for the preview|border windows
api.nvim_win_set_var(self.preview_winid, "fzf_lua_preview", true)
end
previewer:reset_winhl(self.preview_winid)
previewer:display_last_entry()
previewer:update_ts_context()
local release = previewer:copy_extmarks()
self.on_closes.preview = function(hide)
if release then release() end
self:close_preview(hide)
end
end
function FzfWin:validate()
return self.fzf_winid and self.fzf_winid > 0
and api.nvim_win_is_valid(self.fzf_winid)
end
function FzfWin:redraw()
self:normalize_winopts()
self:set_backdrop()
if self:validate() then
self:redraw_main()
end
if self:validate_preview() then
self:redraw_preview()
end
end
---@param title any
---@param hl? string|false hl will also be used as fallback, if title part don't have hl
---@return [string, string][]
local function make_title(title, hl)
if type(title) == "string" then return { { title, type(hl) == "string" and hl or "FloatTitle" } } end
return type(title) ~= "table" and { { "", hl or "FloatTitle" } }
or vim.tbl_map(function(p) return { p[1], p[2] or hl or "FloatTitle" } end, title)
end
function FzfWin:redraw_main()
if self.winopts.split then return end
self.layout = self:generate_layout()
local winopts = vim.tbl_extend("keep", {
title = make_title(self.winopts.title, self.hls.title),
title_pos = self.winopts.title_pos,
}, self.layout.fzf)
if self:validate() then
local prev = self._previewer
if prev and prev.clear_on_redraw then
if prev.clear_preview_buf then prev:clear_preview_buf(true) end
if prev.bcache then prev.bcache:clear() end
end
utils.win_set_config(self.fzf_winid, winopts)
else
self.fzf_bufnr = self.fzf_bufnr or api.nvim_create_buf(false, true)
-- save 'cursorline' setting prior to opening the popup
-- `:help nvim_open_win`
-- 'minimal' sets 'nocursorline', normally this shouldn't
-- be an issue but for some reason this is affecting opening
-- buffers in new splits and causes them to open with
-- 'nocursorline', see discussion in #254
local cursorline = vim.o.cursorline
self.fzf_winid = utils.nvim_open_win(self.fzf_bufnr, true, winopts)
---@diagnostic disable-next-line: preferred-local-alias
if not utils.__HAS_NVIM_0116 and vim.o.cursorline ~= cursorline then
vim.o.cursorline = cursorline
end
-- disable search highlights as they interfere with fzf's highlights
if vim.o.hlsearch and vim.v.hlsearch == 1 then
vim.cmd("nohls")
-- use `vim.o.hlsearch` as `vim.cmd("hls")` is invalid
self.on_closes.hlsearch = function() vim.o.hlsearch = true end
end
end
end
function FzfWin:on(e, callback, global)
api.nvim_create_autocmd(e, {
group = api.nvim_create_augroup("FzfLua" .. e, { clear = true }),
buffer = global ~= true and self.fzf_bufnr or nil,
callback = callback,
})
end
function FzfWin:setup_autocmds()
-- automatically resize fzf window
self:on("VimResized", function() self:redraw() end)
-- verify the preview is closed, this can happen
-- when running async LSP with 'jump1'
self:on("WinClosed", function() self:close() end)
-- Workaround for using `:wqa` with "hide"
-- https://github.com/neovim/neovim/issues/14061
self:on("ExitPre", function() self:close() end, true)
end
-- attach/detach treesitter (e.g. `grep_lgrep`)
-- Use treesitter to highlight results on the main fzf window
function FzfWin:treesitter_attach()
if not self._o.winopts.treesitter then
if self.tsinjector then self.tsinjector.detach(self.fzf_bufnr) end
return
end
self.tsinjector = require("fzf-lua.win.tsinjector")
self.on_closes.tsinjector = self.tsinjector.attach(self, self.fzf_bufnr, self._o._treesitter)
end
function FzfWin:path_shorten_detach()
PathShortener.detach(self.fzf_winid)
-- Reset conceallevel when path shortening is disabled (e.g., on window reuse)
if api.nvim_win_is_valid(self.fzf_winid) then
vim.wo[self.fzf_winid].conceallevel = 0
vim.wo[self.fzf_winid].concealcursor = ""
end
end
function FzfWin:path_shorten_attach()
local path_shorten = self._o.winopts.path_shorten
if not path_shorten then return end
-- Enable conceallevel for the fzf window to show concealed text
if api.nvim_win_is_valid(self.fzf_winid) then
vim.wo[self.fzf_winid].conceallevel = 2
vim.wo[self.fzf_winid].concealcursor = "nvic"
end
PathShortener.attach(self.fzf_winid, self.fzf_bufnr, path_shorten)
end
---@param buf integer
function FzfWin:close_buf(buf)
utils.nvim_buf_delete(buf, { force = true })
if self.tsinjector then self.tsinjector.clear_cache(buf) end
end
function FzfWin:set_tmp_buffer()
local detached = self.fzf_bufnr
-- detach the buffer, and kill it after win_set_buf (#1850)
-- If called from fzf-tmux/split fzf_bufnr will be `nil` (#1556)
if detached then vim.bo[detached].bufhidden = "hide" end
-- replace the attached buffer with a new temp buffer, setting `self.fzf_bufnr`
-- makes sure the call to `fzf_win:close` (which is triggered by the buf del)
-- won't trigger a close due to mismatched buffers condition on `self:close`
self.fzf_bufnr = api.nvim_create_buf(false, true)
utils.win_set_buf_noautocmd(self.fzf_winid, self.fzf_bufnr)
-- close the previous fzf term buffer without triggering autocmds
-- this also kills the previous fzf process if its still running
if detached then self:close_buf(detached) end
return self.fzf_bufnr
end
function FzfWin:save_style_minimal(winid)
return self:get_winopts(winid, {
number = true,
relativenumber = true,
cursorline = true,
cursorcolumn = true,
spell = true,
list = true,
signcolumn = true,
foldcolumn = true,
colorcolumn = true,
winhl = true, -- for `winopts.split=enew`
})
end
---@param winid integer
---@param global boolean If true, 'wo' can be inherited by other windows/buffers
function FzfWin:set_style_minimal(winid, global)
local _ = self
if not tonumber(winid) or not api.nvim_win_is_valid(winid) then return end
self:set_winopts(winid, {
number = false,
relativenumber = false,
-- BUG(upstream): causes issues with winopts.split=enew
-- https://github.com/neovim/neovim/issues/37484
-- cursorline = false,
cursorcolumn = false,
spell = false,
list = false,
signcolumn = "no",
foldcolumn = "0",
colorcolumn = "",
}, false, global)
end
function FzfWin:create()
self._hidden_fzf_bufnr = nil
-- Generate border-label from window title
self:update_fzf_border_label()
-- When using fzf-tmux we don't need to create windows
-- as tmux popups will be used instead
if self._o._is_fzf_tmux then
self:setup_keybinds()
self:set_backdrop()
return
end
if self._reuse then
self._reuse = nil
-- we can't reuse the fzf term buffer
-- create a new tmp buffer for the fzf win
self.fzf_bufnr = self:set_tmp_buffer()
self:setup_autocmds()
self:setup_keybinds()
self:treesitter_attach()
-- attach/detach path shortening
if self._o.winopts.path_shorten then
self:path_shorten_attach()
else
self:path_shorten_detach()
end
self:reset_winhl(self.fzf_winid)
-- also recall the user's 'on_create' (#394)
if type(self.winopts.on_create) == "function" then
self.winopts.on_create({ winid = self.fzf_winid, bufnr = self.fzf_bufnr })
end
-- redraw all window (e.g. when switch from fzf previewer to builtin previewer)
self:redraw_main()
self:redraw_preview()
-- not sure why but when using a split and reusing the window,
-- fzf will not use all the available width until 'redraw' is
-- called resulting in misaligned native and builtin previews
vim.cmd("redraw")
return self.fzf_bufnr
end
-- Set backdrop
self:set_backdrop()
-- save sending bufnr/winid
self.src_bufnr = api.nvim_get_current_buf()
self.src_winid = api.nvim_get_current_win()
if self.winopts.split then
-- save current window layout cmd
local winrestcmd = fn.winrestcmd()
local cmdheight = vim.o.cmdheight
self.on_closes.winrest = function()
-- when using `split = "belowright new"` closing the fzf
-- window may not always return to the correct source win
-- depending on the user's split configuration (#397)
if self.src_winid and api.nvim_win_is_valid(self.src_winid)
and self.src_winid ~= api.nvim_get_current_win() then
api.nvim_set_current_win(self.src_winid)
end
-- remove all windows from the restore cmd that have been closed in the meantime
-- if we're not doing this the result might be all over the place
local winnrs = vim.tbl_map(api.nvim_win_get_number, api.nvim_tabpage_list_wins(0))
local parts = vim.split(winrestcmd, "|")
local cmd = {}
for _, cmd_part in ipairs(parts) do
local winnr = tonumber(cmd_part:match("(%d+)resize"))
if utils.tbl_contains(winnrs, winnr) then
table.insert(cmd, cmd_part)
end
end
if #cmd > 0 then vim.cmd(table.concat(cmd, "|")) end
-- Also restore cmdheight, will be wrong if vim resized (#1462)
vim.o.cmdheight = cmdheight
end
-- Store the current window styling options (number, cursor, etc)
self.src_winid_style = self:save_style_minimal(self.src_winid)
if type(self.winopts.split) == "function" then
self.winopts.split()
else
vim.cmd(tostring(self.winopts.split))
end
local split_bufnr = api.nvim_get_current_buf()
self.fzf_winid = api.nvim_get_current_win()
if self.fzf_bufnr and api.nvim_buf_is_valid(self.fzf_bufnr) then
-- set to fzf bufnr set by `:unhide()`
utils.win_set_buf_noautocmd(self.fzf_winid, self.fzf_bufnr)
else
-- ensure split buffer is a scratch buffer
self.fzf_bufnr = self:set_tmp_buffer()
end
-- since we're using our own scratch buf, if the
-- split command created a new buffer, delete it
if self.src_bufnr ~= split_bufnr then
utils.nvim_buf_delete(split_bufnr, { force = true })
end
-- match window options with 'nvim_open_win' style:minimal
self:set_style_minimal(self.fzf_winid, false)
else
-- draw the main window
self:redraw_main()
end
self:setup_autocmds()
self:setup_keybinds()
self:treesitter_attach()
-- Use extmarks to visually shorten paths while keeping full paths for actions
self:path_shorten_attach()
self:reset_winhl(self.fzf_winid)
-- potential workarond for `<C-c>` freezing neovim (#1091)
-- https://github.com/neovim/neovim/issues/20726
utils.wo[self.fzf_winid][0].foldmethod = "manual"
if type(self.winopts.on_create) == "function" then
self.winopts.on_create({ winid = self.fzf_winid, bufnr = self.fzf_bufnr })
end
-- create or redraw the preview win
self:redraw_preview()
return self.fzf_bufnr
end
function FzfWin:close_preview(do_not_clear_cache)
self:close_preview_scrollbar()
if self._previewer and self._previewer.close then
self._previewer:close(do_not_clear_cache)
end
if self.preview_winid and api.nvim_win_is_valid(self.preview_winid) then
utils.nvim_win_close(self.preview_winid, true)
end
self.preview_winid = nil
end
---@param winid? integer
---@param last_winid? integer
local restore_lastwin = function(winid, last_winid)
if
not winid
or not last_winid
or not api.nvim_win_is_valid(last_winid)
or not api.nvim_win_is_valid(winid)
then
return
end
-- switching to `last_winid` only restores the `CTRL-W p` history. Floats
-- may use `WinEnter` autoclose handlers (e.g. Mason), don't let them see
-- the intermediate window. The events of the switch back to `winid` are
-- the real focus restore and must keep firing (#2812).
---@param w integer
---@return boolean
local is_float = function(w) return (api.nvim_win_get_config(w).relative or "") ~= "" end
if is_float(winid) or is_float(last_winid) then
utils.eventignore(function() api.nvim_set_current_win(last_winid) end)
else
api.nvim_set_current_win(last_winid)
end
api.nvim_set_current_win(winid)
end
---@param buf? integer
local restore_altbuf = function(buf)
if buf and api.nvim_buf_is_valid(buf) then
fn.setreg("#", buf)
else -- no alt buf or deleted alt buf
local tmpbuf = api.nvim_create_buf(false, true)
fn.setreg("#", tmpbuf)
api.nvim_buf_delete(tmpbuf, { force = true })
end
end
local stopinsert = function(ctx)
_G.fzf_lua_stopinsert_hack = nil
if not (ctx.mode == "nt" and api.nvim_get_mode() ~= "nt") then return end
vim.cmd "stopinsert"
local pat = "FzfLuaStopInsertHack"
_G.fzf_lua_stopinsert_hack = function(cb)
api.nvim_create_autocmd("User", { pattern = pat, once = true, callback = cb })
end
api.nvim_create_autocmd("ModeChanged", {
pattern = "*:nt",
once = true,
callback = function()
-- nvim_get_mode don't get correct mode without schedule_wrap
vim.schedule_wrap(api.nvim_exec_autocmds)("User", { pattern = pat, modeline = false })
_G.fzf_lua_stopinsert_hack = nil
end,
})
end
---@param fzf_bufnr? integer
---@param hide? boolean
function FzfWin:close(fzf_bufnr, hide)
-- When a window is reused, (e.g. open any fzf-lua interface, press <C-\-n> and run
-- ":FzfLua") `FzfWin:set_tmp_buffer()` will call `nvim_buf_delete` on the original
-- fzf terminal buffer which will terminate the fzf process and trigger the call to
-- `fzf_win:close()` within `core.fzf()`. We need to avoid the close in this case.
if self.closing or (fzf_bufnr and fzf_bufnr ~= self.fzf_bufnr) then return end
self.closing = true -- prevents race condition
-- we restore normal mode before exiting fzf due toma neovim bug? whereas
-- switching from a term win to another term win preserves terminal mode
-- even if the target window was in normal terminal mode (#2054 #2419)
local ctx = utils.__CTX() or {}
stopinsert(ctx)
if self.fzf_winid and api.nvim_win_is_valid(self.fzf_winid) then
-- restore the original last window
restore_lastwin(ctx.winid, ctx.last_winid)
-- run in a pcall due to potential errors while closing the window
-- Vim(lua):E5108: Error executing lua
-- experienced while accessing 'vim.b[]' from my statusline code
if self.src_winid == self.fzf_winid then
-- "split" reused the current win (e.g. "enew")
-- restore the original buffer and styling options
self:set_winopts(self.fzf_winid, self.src_winid_style or {}, false)
-- buf may be invalid if we switched away from a scratch buffer
if api.nvim_buf_is_valid(self.src_bufnr) then
-- TODO: why does ignoring events cause the cursor to move to the wrong position?
-- repro steps: open diag + select and item, open code actions and abort
-- utils.win_set_buf_noautocmd(self.fzf_winid, self.src_bufnr)
api.nvim_win_set_buf(self.fzf_winid, self.src_bufnr)
end
-- also restore the original alternate buffer
restore_altbuf(ctx.alt_bufnr)
else
pcall(api.nvim_win_close, self.fzf_winid, true)
end
end
if not hide and self.fzf_bufnr then
self:close_buf(self.fzf_bufnr)
end
-- Detach path shortening decoration provider
PathShortener.detach(self.fzf_winid)
for k, _ in pairs(self.on_closes) do
self.on_closes[k](hide)
self.on_closes[k] = nil
end
if type(self.winopts.on_close) == "function" then
self.winopts.on_close()
end
self.closing = nil
if not hide then _self = nil end
end
local winview = function()
return { vim.o.lines, vim.o.columns, vim.o.cmdheight }
end
function FzfWin:hide()
-- Note: we should never get here with a tmux profile as neovim binds (default: <A-Esc>)
-- do not apply to tmux, validate anyways in case called directly using the API
if self:hidden() or self._o._is_fzf_tmux then return end
vim.bo[self.fzf_bufnr].bufhidden = "hide"
self:close(nil, true)
self.last_view = winview() -- VimResized won't emit on hidden buffer
self._hidden_fzf_bufnr = self.fzf_bufnr
end
function FzfWin:hidden()
return self._hidden_fzf_bufnr and api.nvim_buf_is_valid(self._hidden_fzf_bufnr)
end
-- True after a `:new()` call for a different picker, used in `core.fzf`
-- to avoid post processing an fzf process that was discarded (e.g. kill by :%bw!)
function FzfWin:was_hidden()
return self._hidden_fzf_bufnr and not api.nvim_buf_is_valid(self._hidden_fzf_bufnr)
end
---SIGWINCH/on_SIGWINCH is nop if fzf < v0.46
---@param opts fzf-lua.config.Resolved|{}
---@param event string|integer array part always emit, non-array part only emit on given events
---@param cb function
---@return boolean?
function FzfWin.on_SIGWINCH(opts, event, cb)
if not utils.has(opts, "fzf", { 0, 46 }) then return end
local created = opts.__sigwinch_cb and true or false
opts.__sigwinch_cb = opts.__sigwinch_cb or {}
opts.__sigwinch_cb[event] = cb -- override duplicate keys
if created then return true end
opts._fzf_cli_args = opts._fzf_cli_args or {}
table.insert(opts._fzf_cli_args,
libuv.cli_raw_arg("--bind=", "resize:+transform:" .. FzfLua.shell.stringify_data(function(args)
local events = vim.tbl_keys(vim.list_slice(opts.__sigwinch_cb))
vim.list_extend(events, opts.__sigwinches or {})
opts.__sigwinches = nil
---@diagnostic disable-next-line: undefined-field
local acts = vim.tbl_map(function(k) return opts.__sigwinch_cb[k](args) end, events)
acts = vim.tbl_filter(function(a) return a and #a > 0 end, acts)
return table.concat(acts, "+")
end, opts, utils.__IS_WINDOWS and "%FZF_PREVIEW_LINES%" or "$FZF_PREVIEW_LINES")))
return true
end
---@param scopes string[]?
---@return boolean?
function FzfWin:SIGWINCH(scopes)
-- avoid racing when multiple SIGWINCH trigger at the same time
if not utils.has(self._o, "fzf", { 0, 46 }) or self._o.__sigwinches then return end
local bufnr = self.fzf_bufnr
if not bufnr or not api.nvim_buf_is_valid(bufnr) then return end
local ok, pid = pcall(fn.jobpid, vim.bo[bufnr].channel)
if ok and pid > 0 then ---@cast pid integer
self._o.__sigwinches = scopes or {}
vim.tbl_map(function(_pid) libuv.process_kill(_pid, 28) end, api.nvim_get_proc_children(pid))
end
return true
end
function FzfWin:unhide()
if not self:hidden() then return end
self._o.__CTX = utils.CTX({ includeBuflist = true })
-- Send SIGWINCH to to trigger resize in the fzf process
-- We will use the trigger to reload necessary buffer lists
self:create()
self:SIGWINCH({ "win.unhide" })
if not vim.deep_equal(self.last_view, winview()) then self:redraw() end
vim.cmd("startinsert")
return true
end
---@diagnostic disable-next-line: unused
function FzfWin:close_preview_scrollbar()
require("fzf-lua.win.scrollbar").close()
end
function FzfWin:update_preview_scrollbar()
if not self:validate_preview() then return end
self.on_closes.scrollbar = require("fzf-lua.win.scrollbar").update(self.preview_winid, self.hls,
self.winopts)
end
function FzfWin:update_statusline()
if not self.winopts.split then
-- NOTE: 0.12 added float win local statusline, we nullify the statusline here and
-- not after `nvim_open_win` in case the user also has fzf.vim installed which sets
-- the statusline on WinEnter
if utils.__HAS_NVIM_012 and #api.nvim_win_get_config(self.fzf_winid).relative > 0 then
vim.wo[self.fzf_winid].statusline = ""
end
return
end
local titlestr = self.winopts.title or (" %s "):format(tostring(FzfLua.get_info().cmd))
local title = make_title(titlestr, self.hls.title)
local parts = vim.tbl_map(function(p) return ("%%#%s#%s%%#fzf3#"):format(p[2], p[1]) end, title)
local picker = table.remove(parts, 1) or ""
vim.wo[self.fzf_winid].statusline = "%#fzf1# > %#fzf2#fzf-lua%#fzf3#"
.. string.format(" %s %s", picker, table.concat(parts, ""))
end
function FzfWin:update_fzf_border_label()
if not utils.has(self._o, "fzf", { 0, 35 })
or not self._o.fzf_opts["--border"]
or self._o.fzf_opts["--border-label"] == false
then
return
end
local titlestr = self.winopts.title or (" %s "):format(tostring(FzfLua.get_info().cmd))
local title = make_title(titlestr, self.hls.title)
local parts = vim.tbl_map(function(p) return (utils.ansi_from_hl(p[2], p[1])) end, title)
self._o.fzf_opts["--border-label"] = table.concat(parts, " ")
end
function FzfWin:update_main_title(title)
-- Can be called from fzf-tmux on ctrl-g
if not self:validate() or self.winopts.split then return end
self.winopts.title = title
self._o.winopts.title = title
-- NOTE: <0.11 fail without top border: "title requires border to be set"
utils.win_set_config(self.fzf_winid, {
title = make_title(title, self.hls.title),
title_pos = self.winopts.title_pos,
border = not utils.__HAS_NVIM_011
and (api.nvim_win_get_config(self.fzf_winid).border or "none") or nil,
})
end
function FzfWin:update_preview_title(title)
if not self:validate_preview() or not self._previewer then return end
-- NOTE: <0.11 fail without top border: "title requires border to be set"
utils.win_set_config(self.preview_winid, {
title = make_title(title, self.hls.preview_title),
title_pos = self.winopts.preview.title_pos,
border = not utils.__HAS_NVIM_011
and (api.nvim_win_get_config(self.preview_winid).border or "none") or nil,
})
end
-- keybind methods below
function FzfWin:toggle_fullscreen()
self.fullscreen = not self.fullscreen
self:redraw()
end
function FzfWin:focus_preview()
api.nvim_set_current_win(self.preview_winid)
end
function FzfWin:toggle_preview()
self.preview_hidden = not self.preview_hidden
if self._fzf_toggle_prev_bind then
-- 1. Toggle the empty preview window (under the neovim preview buffer)
-- 2. Trigger resize to cange the preview layout if needed (toggle -> resize -> toggle)
local feedkey
if utils.__IS_WINDOWS then
self:SIGWINCH({})
feedkey = true
elseif not self:SIGWINCH({ "toggle-preview" }) then
feedkey = true
end
if feedkey and type(self._fzf_toggle_prev_bind) == "string" then
utils.feed_keys_termcodes(self._fzf_toggle_prev_bind)
elseif feedkey and not self._o.silent then
utils.warn("missing 'toggle-preview' in opts.keymap.fzf or opts.keymap.builtin")
end
-- TODO: this don't work with <a-x> or <fxx> (wrong keycode)
-- api.nvim_chan_send(vim.bo.channel, vim.keycode(self._fzf_toggle_prev_bind))
end
if self.preview_hidden then
if self:validate_preview() then self:close_preview(true) end
self:redraw_main()
elseif not self.preview_hidden then
self:redraw_main()
self:redraw_preview()
end
end
function FzfWin:toggle_preview_wrap()
if not self:validate_preview() then return end
self.preview_wrap = not utils.wo[self.preview_winid].wrap
utils.wo[self.preview_winid].wrap = self.preview_wrap
end
---@param direction integer
function FzfWin:toggle_preview_cw(direction)
local curpos = self:normalize_preview_layout().pos
local pos = { "up", "right", "down", "left" }
local idx ---@type integer
for i = 1, #pos do
if pos[i] == curpos then
idx = i
break
end
end
if not idx then return end
local newidx = direction > 0 and idx + 1 or idx - 1
if newidx < 1 then newidx = #pos end
if newidx > #pos then newidx = 1 end
self._preview_pos_force = pos[newidx]
if self.winopts.split or not self.previewer_is_builtin then
self:SIGWINCH({ "toggle-preview-cw" })
end
self:redraw()
end
function FzfWin:toggle_preview_behavior()
self.toggle_behavior = not self.toggle_behavior and "extend" or nil
utils.info("preview toggle behavior set to %s", self.toggle_behavior or "default")
self:redraw()
end
function FzfWin:toggle_preview_ts_ctx()
if self:validate_preview()
and self._previewer
and self._previewer.ts_ctx_toggle then
self._previewer:ts_ctx_toggle()
end
end
function FzfWin:toggle_preview_undo_diff()
if self:validate_preview()
and self._previewer
---@diagnostic disable-next-line: undefined-field
and self._previewer.toggle_undo_diff then
---@diagnostic disable-next-line: undefined-field
self._previewer:toggle_undo_diff()
end
end
---@param num integer
function FzfWin:preview_ts_ctx_inc_dec(num)
if self:validate_preview()
and self._previewer
and self._previewer.ts_ctx_inc_dec_maxlines then
self._previewer:ts_ctx_inc_dec_maxlines(num)
end
end
---@alias fzf-lua.win.direction "top"|"bottom"|"half-page-up"|"half-page-down"|"page-up"|"page-down"|"line-up"|"line-down"|"reset"
---@param direction fzf-lua.win.direction
function FzfWin:preview_scroll(direction)
if self:validate_preview()
and self._previewer
and self._previewer.scroll then
-- Do not trigger "ModeChanged"
utils.eventignore(function() self._previewer:scroll(direction) end)
end
end
function FzfWin:toggle_help()
local zindex = self.winopts.zindex + 2
local mode = self.previewer_is_builtin and "builtin" or "fzf"
self.on_closes.help = require("fzf-lua.win.help").toggle(self.keymap, self.actions, self.hls,
zindex, _preview_keymaps, mode, self._o.help_open_win)
end
---@type fzf-lua.win.api
local M = setmetatable({}, {
__index = function(m, k)
rawset(m, k, FzfLua._exported_wapi[k] and function(...)
if not _self then return end
return _self[k](_self, ...)
end or FzfWin[k])
return rawget(m, k)
end,
})
return M