Compare commits

...

1 Commits

Author SHA1 Message Date
Dmitriy Kovalenko 2f546c3ec2 fix: Fully support winborder setting
docs / docs (push) Has been cancelled
closes https://github.com/dmtrKovalenko/fff.nvim/issues/195
2026-01-08 20:48:49 -08:00
2 changed files with 80 additions and 34 deletions
+17 -9
View File
@@ -93,6 +93,7 @@ local function position_overlay_window(state_key, buf, width, row, col)
row = row,
col = col,
style = 'minimal',
border = 'none',
focusable = false,
zindex = 250,
}
@@ -106,15 +107,15 @@ local function position_overlay_window(state_key, buf, width, row, col)
vim.api.nvim_win_set_option(overlay_state[state_key], 'winhl', 'Normal:Normal')
end
local function update_overlays(list_win, combo_header_line, border_hl, prompt_position)
local function update_overlays(list_win, combo_header_line, border_hl)
local list_config = vim.api.nvim_win_get_config(list_win)
-- combo_header_line is a 1-based buffer line index
-- list_config.row is the window position (includes border)
-- Buffer content starts at row + 1 (after top border)
-- For bottom prompt: overlay needs adjustment due to different border handling
-- For top prompt: use standard calculation
-- combo_header_line is a 1-based buffer line index (includes any padding offset)
-- list_config.row is where the window starts (0-based, at the top border)
-- Content starts at list_config.row + 1 (after the top border)
-- Buffer line 1 -> screen row (list_config.row + 1)
-- Buffer line N -> screen row (list_config.row + N)
-- Since combo_header_line is 1-based, the formula naturally works out
local combo_header_row = list_config.row + combo_header_line
if prompt_position == 'bottom' then combo_header_row = combo_header_row - 1 end
-- Skip update if position and highlight haven't changed
if
@@ -185,7 +186,8 @@ function M.render_highlights_and_overlays(
ns_id,
border_hl,
item_to_lines,
prompt_position
prompt_position,
total_items
)
local was_rendered_before = overlay_state.was_rendered
local is_rendering_now = false
@@ -199,7 +201,13 @@ function M.render_highlights_and_overlays(
else
local combo_header_line_idx = combo_item_lines.first
apply_header_highlights(list_buf, ns_id, combo_header_line_idx, text_len, border_hl)
update_overlays(list_win, combo_header_line_idx, border_hl, prompt_position)
if prompt_position == 'bottom' and total_items and total_items > 1 then
combo_header_line_idx = combo_header_line_idx - 1
end
-- when rendering items in the reverse order for some reason this makes the
-- indexing shifted by one in the internal list config, so just adjust for that
update_overlays(list_win, combo_header_line_idx, border_hl)
is_rendering_now = true
end
end
+63 -25
View File
@@ -8,6 +8,36 @@ local location_utils = require('fff.location_utils')
local combo_renderer = require('fff.combo_renderer')
local scrollbar = require('fff.scrollbar')
local BORDER_PRESETS = {
single = { '', '', '', '', '', '', '', '' },
double = { '', '', '', '', '', '', '', '' },
rounded = { '', '', '', '', '', '', '', '' },
solid = { '', '', '', '', '', '', '', '' },
shadow = { '', '', ' ', ' ', ' ', ' ', ' ', '' },
none = { '', '', '', '', '', '', '', '' },
}
local T_JUNCTION_PRESETS = {
single = { '', '' },
double = { '', '' },
rounded = { '', '' }, -- Rounded only affects corners
solid = { '', '' },
shadow = { '', '' },
none = { '', '' },
}
--- Get border characters from vim.o.winborder for custom connected borders
--- @return table Array of 8 border characters
--- @return table Array of 2 T-junction characters (left, right)
local function get_border_chars()
local winborder = vim.o.winborder or 'single'
if BORDER_PRESETS[winborder] then return BORDER_PRESETS[winborder], T_JUNCTION_PRESETS[winborder] end
-- Fallback to single for unknown border styles
return BORDER_PRESETS.single, T_JUNCTION_PRESETS.single
end
local function get_prompt_position()
local config = M.state.config
@@ -405,7 +435,7 @@ function M.create_ui()
M.state.file_info_buf = nil
end
-- Create list window with conditional title based on prompt position
local border_chars, t_junctions = get_border_chars()
local list_window_config = {
relative = 'editor',
width = layout.list_width,
@@ -414,8 +444,20 @@ function M.create_ui()
row = layout.list_row,
-- To make the input feel connected with the picker, we customize the
-- respective corner border characters based on prompt_position
border = prompt_position == 'bottom' and { '', '', '', '', '', '', '', '' }
or { '', '', '', '', '', '', '', '' },
-- When prompt at bottom: list has top border + sides, no bottom (connects to input below)
-- When prompt at top: list has sides + bottom with T-junctions at top (connects to input above)
border = prompt_position == 'bottom'
and { border_chars[1], border_chars[2], border_chars[3], border_chars[4], '', '', '', border_chars[8] }
or {
t_junctions[1],
border_chars[2],
t_junctions[2],
border_chars[4],
border_chars[5],
border_chars[6],
border_chars[7],
border_chars[8],
},
style = 'minimal',
}
@@ -436,7 +478,6 @@ function M.create_ui()
height = layout.file_info.height,
col = layout.file_info.col,
row = layout.file_info.row,
border = 'single',
style = 'minimal',
title = ' File Info ',
title_pos = 'left',
@@ -453,14 +494,12 @@ function M.create_ui()
height = layout.preview.height,
col = layout.preview.col,
row = layout.preview.row,
border = 'single',
style = 'minimal',
title = ' Preview ',
title_pos = 'left',
})
end
-- Create input window with conditional title based on prompt position
local input_window_config = {
relative = 'editor',
width = layout.input_width,
@@ -469,12 +508,21 @@ function M.create_ui()
row = layout.input_row,
-- To make the input feel connected with the picker, we customize the
-- respective corner border characters based on prompt_position
border = prompt_position == 'bottom' and { '', '', '', '', '', '', '', '' }
or { '', '', '', '', '', '', '', '' },
-- if prompt at bottom: input has T-junctions at top (connects to list above), full bottom border
-- if prompt at top: input has top border + sides, no bottom (connects to list below)
border = prompt_position == 'bottom' and {
t_junctions[1],
border_chars[2],
t_junctions[2],
border_chars[4],
border_chars[5],
border_chars[6],
border_chars[7],
border_chars[8],
} or { border_chars[1], border_chars[2], border_chars[3], border_chars[4], '', '', '', border_chars[8] },
style = 'minimal',
}
-- Add title if prompt is at top - title appears above the prompt
if prompt_position == 'top' then
input_window_config.title = title
input_window_config.title_pos = 'left'
@@ -1154,12 +1202,10 @@ local function apply_bottom_padding(lines, item_to_lines, ctx)
local empty_lines_needed = math.max(0, ctx.win_height - total_content_lines)
if empty_lines_needed > 0 then
-- Insert empty lines at the beginning
for i = empty_lines_needed, 1, -1 do
for _ = empty_lines_needed, 1, -1 do
table.insert(lines, 1, string.rep(' ', ctx.win_width + 5))
end
-- Adjust item_to_lines mapping
for i = ctx.display_start, ctx.display_end do
if item_to_lines[i] then
item_to_lines[i].first = item_to_lines[i].first + empty_lines_needed
@@ -1174,22 +1220,18 @@ end
--- @param item_to_lines table Item to lines mapping
--- @param ctx table Render context
local function update_buffer_and_cursor(lines, item_to_lines, ctx)
-- Calculate cursor line position
local cursor_line = 0
if #ctx.items > 0 and ctx.cursor >= 1 and ctx.cursor <= #ctx.items then
local cursor_item = item_to_lines[ctx.cursor]
if cursor_item then cursor_line = cursor_item.last end
end
-- Update buffer
vim.api.nvim_buf_set_option(M.state.list_buf, 'modifiable', true)
vim.api.nvim_buf_set_lines(M.state.list_buf, 0, -1, false, lines)
vim.api.nvim_buf_set_option(M.state.list_buf, 'modifiable', false)
-- Clear existing highlights
vim.api.nvim_buf_clear_namespace(M.state.list_buf, M.state.ns_id, 0, -1)
-- Position cursor
if #ctx.items > 0 and cursor_line > 0 and cursor_line <= #lines then
vim.api.nvim_win_set_cursor(M.state.list_win, { cursor_line, 0 })
end
@@ -1216,21 +1258,17 @@ local function apply_all_highlights(lines, item_to_lines, ctx)
if not line_content then goto continue end
-- Apply highlights using renderer.apply_highlights
renderer.apply_highlights(item, ctx, i, M.state.list_buf, M.state.ns_id, line_idx, line_content)
::continue::
end
end
-- Renders all virtual buffer overalys
local function finalize_render(item_to_lines, ctx)
-- Get text_len from item_to_lines if combo exists
local combo_text_len = nil
if ctx.combo_item_index and item_to_lines[ctx.combo_item_index] then
combo_text_len = item_to_lines[ctx.combo_item_index].combo_header_text_len
end
-- Render combo overlays
local combo_was_hidden = combo_renderer.render_highlights_and_overlays(
ctx.combo_item_index,
combo_text_len or ctx.combo_header_text_len,
@@ -1239,13 +1277,13 @@ local function finalize_render(item_to_lines, ctx)
M.state.ns_id,
ctx.config.hl.border,
item_to_lines,
ctx.prompt_position
ctx.prompt_position,
#ctx.items
)
-- Handle combo hiding with scroll adjustment
-- it's important part of functionality when user scrolls to the middle of the page we hide
-- the combo overlay which leaves the gap of the internal neovim buffer, so scroll to show last item
if combo_was_hidden and ctx.prompt_position == 'bottom' then scroll_to_bottom() end
-- Render scrollbar
scrollbar.render(M.state.layout, ctx.config, M.state.list_win, M.state.pagination, ctx.prompt_position)
end
@@ -1661,7 +1699,7 @@ function M.toggle_select()
M.render_list()
-- only when selecting the element not deslecting
-- only when selecting the element not deselecting
if not was_selected then
if get_prompt_position() == 'bottom' then
M.move_up()