Compare commits

...

6 Commits

Author SHA1 Message Date
Dmitriy Kovalenko 461a615878 fix: Not downloadable binary
docs / docs (push) Has been cancelled
closes https://github.com/dmtrKovalenko/fff.nvim/issues/196

This issue was related to the fact that I chagned vim.mkdir to
vim.uv.fs_mkdir but it is not recursive so when you installed a fresh
version it never created the dir for it.
2025-12-18 18:05:11 -08:00
Dmitriy Kovalenko 2951756ae3 fix: Restore scroll position after last match result showing (#204)
* fix: Scroll position after last match result showing

* chore: Update docs for - fix: Scroll position after last match result showing

* fix: Scroll position after last match result showing
2025-12-18 17:17:01 -08:00
Mohamed Ibraheem c17056bcb6 fix: directory_path highlight is not working if there are no icons (#203) 2025-12-18 17:10:01 -08:00
Dmitriy Kovalenko abfa5d0ef7 feat: Add config param for git based filename highlighting (#202)
* feat: Add config param for git based filename highlighting

* chore: Update docs for - feat: Add config param for git based filename highlighting
2025-12-16 17:34:27 -08:00
Dmitriy Kovalenko d997344fd7 fix: Invert pagination scrollbar when the prompt is in the bottom (#201) 2025-12-16 13:16:45 -08:00
Dmitriy Kovalenko e3ba972db6 fix: Pagination layout when combo box renderer used (#200) 2025-12-16 13:13:24 -08:00
13 changed files with 752 additions and 331 deletions
+1 -1
View File
@@ -32,4 +32,4 @@ jobs:
- uses: stefanzweifel/git-auto-commit-action@v6
with:
commit_author: ${{ steps.last-commit.outputs.author }}
commit_message: chore: Update docs for - ${{ steps.last-commit.outputs.message }}
commit_message: "chore: Update docs for - ${{ steps.last-commit.outputs.message }}"
+58
View File
@@ -170,6 +170,7 @@ require('fff').setup({
debug = 'Comment',
combo_header = 'Number',
scrollbar = 'Comment', -- Highlight for scrollbar thumb (track uses border)
directory_path = 'Comment', -- Highlight for directory path in file list
-- Multi-select highlights
selected = 'FFFSelected',
selected_active = 'FFFSelectedActive',
@@ -207,6 +208,10 @@ require('fff').setup({
min_combo_count = 3, -- file will get a boost if it was selected 3 in a row times per specific query
combo_boost_score_multiplier = 100, -- Score multiplier for combo matches
},
-- Git integration
git = {
status_text_color = false, -- Apply git status colors to filename text (default: false, only sign column)
},
debug = {
enabled = false, -- Set to true to show scores in the UI
show_scores = false,
@@ -263,6 +268,59 @@ Select multiple files and send them to Neovim's quickfix list (keymaps are confi
- `<Tab>` - Toggle selection for the current file (shows thick border `▊` in signcolumn)
- `<C-q>` - Send selected files to quickfix list and close picker
#### Git Status Highlighting
FFF integrates with git to show file status through sign column indicators (enabled by default) and optional filename text coloring.
**Sign Column Indicators** (enabled by default) - Border characters shown in the sign column:
```lua
hl = {
git_sign_staged = 'FFFGitSignStaged',
git_sign_modified = 'FFFGitSignModified',
git_sign_deleted = 'FFFGitSignDeleted',
git_sign_renamed = 'FFFGitSignRenamed',
git_sign_untracked = 'FFFGitSignUntracked',
git_sign_ignored = 'FFFGitSignIgnored',
}
```
**Text Highlights** (opt-in) - Apply colors to filenames based on git status:
To enable git status text coloring, set `git.status_text_color = true`:
```lua
require('fff').setup({
git = {
status_text_color = true, -- Enable git status colors on filename text
},
hl = {
git_staged = 'FFFGitStaged', -- Files staged for commit
git_modified = 'FFFGitModified', -- Modified unstaged files
git_deleted = 'FFFGitDeleted', -- Deleted files
git_renamed = 'FFFGitRenamed', -- Renamed files
git_untracked = 'FFFGitUntracked', -- New untracked files
git_ignored = 'FFFGitIgnored', -- Git-ignored files
}
})
```
The plugin provides sensible default highlight groups that link to common git highlight groups (e.g., GitSignsAdd, GitSignsChange). You can override these with your own custom highlight groups to match your colorscheme.
**Example - Custom Bright Colors for Text:**
```lua
vim.api.nvim_set_hl(0, 'CustomGitModified', { fg = '#FFA500' })
vim.api.nvim_set_hl(0, 'CustomGitUntracked', { fg = '#00FF00' })
require('fff').setup({
git = {
status_text_color = true,
},
hl = {
git_modified = 'CustomGitModified',
git_untracked = 'CustomGitUntracked',
}
})
```
### Troubleshooting
+120 -19
View File
@@ -1,4 +1,4 @@
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2025 October 17
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2025 December 19
==============================================================================
Table of Contents *fff.nvim-table-of-contents*
@@ -86,10 +86,10 @@ VIM.PACK
>lua
vim.pack.add({ 'https://github.com/dmtrKovalenko/fff.nvim' })
nvim.create_autocmd('PackChanged', {
vim.api.nvim_create_autocmd('PackChanged', {
callback = function(event)
if event.data.updated then
require('fff.download').download_or_build_binary()
if event.data.updated then
require('fff.download').download_or_build_binary()
end
end,
})
@@ -128,11 +128,10 @@ all available options:
layout = {
height = 0.8,
width = 0.8,
row = nil, -- ratio (0.0 = top edge, 1.0 = bottom edge) nil is centered
col = nil, -- ratio (0.0 = left edge, 1.0 = right edge) nil is centered
prompt_position = 'bottom', -- or 'top'
preview_position = 'right', -- or 'left', 'right', 'top', 'bottom'
preview_size = 0.5,
show_scrollbar = true, -- Show scrollbar for pagination
},
preview = {
enabled = true,
@@ -155,11 +154,17 @@ all available options:
select_split = '<C-s>',
select_vsplit = '<C-v>',
select_tab = '<C-t>',
-- you can assign multiple keys to any action
move_up = { '<Up>', '<C-p>' },
move_down = { '<Down>', '<C-n>' },
preview_scroll_up = '<C-u>',
preview_scroll_down = '<C-d>',
toggle_debug = '<F2>',
-- goes to the previous query in history
cycle_previous_query = '<C-Up>',
-- multi-select keymaps for quickfix
toggle_select = '<Tab>',
send_to_quickfix = '<C-q>',
},
hl = {
border = 'FloatBorder',
@@ -171,11 +176,50 @@ all available options:
active_file = 'Visual',
frecency = 'Number',
debug = 'Comment',
combo_header = 'Number',
scrollbar = 'Comment', -- Highlight for scrollbar thumb (track uses border)
directory_path = 'Comment', -- Highlight for directory path in file list
-- Multi-select highlights
selected = 'FFFSelected',
selected_active = 'FFFSelectedActive',
-- Git text highlights for file names
git_staged = 'FFFGitStaged',
git_modified = 'FFFGitModified',
git_deleted = 'FFFGitDeleted',
git_renamed = 'FFFGitRenamed',
git_untracked = 'FFFGitUntracked',
git_ignored = 'FFFGitIgnored',
-- Git sign/border highlights
git_sign_staged = 'FFFGitSignStaged',
git_sign_modified = 'FFFGitSignModified',
git_sign_deleted = 'FFFGitSignDeleted',
git_sign_renamed = 'FFFGitSignRenamed',
git_sign_untracked = 'FFFGitSignUntracked',
git_sign_ignored = 'FFFGitSignIgnored',
-- Git sign selected highlights
git_sign_staged_selected = 'FFFGitSignStagedSelected',
git_sign_modified_selected = 'FFFGitSignModifiedSelected',
git_sign_deleted_selected = 'FFFGitSignDeletedSelected',
git_sign_renamed_selected = 'FFFGitSignRenamedSelected',
git_sign_untracked_selected = 'FFFGitSignUntrackedSelected',
git_sign_ignored_selected = 'FFFGitSignIgnoredSelected',
},
-- Store file open frecency
frecency = {
enabled = true,
db_path = vim.fn.stdpath('cache') .. '/fff_nvim',
},
-- Store successfully opened queries with respective matches
history = {
enabled = true,
db_path = vim.fn.stdpath('data') .. '/fff_queries',
min_combo_count = 3, -- file will get a boost if it was selected 3 in a row times per specific query
combo_boost_score_multiplier = 100, -- Score multiplier for combo matches
},
-- Git integration
git = {
status_text_color = false, -- Apply git status colors to filename text (default: false, only sign column)
},
debug = {
enabled = false, -- Set to true to show scores in the UI
show_scores = false,
@@ -217,19 +261,6 @@ FFF.nvim provides several commands for interacting with the file picker:
- `:FFFOpenLog` - Open the FFF log file in a new tab
MULTIPLE KEY BINDINGS
You can assign multiple key combinations to the same action:
>lua
keymaps = {
move_up = { '<Up>', '<C-p>', '<C-k>' }, -- Three ways to move up
close = { '<Esc>', '<C-c>' }, -- Two ways to close
select = '<CR>', -- Single binding still works
}
<
MULTILINE PASTE SUPPORT
The input field automatically handles multiline clipboard content by joining
@@ -246,6 +277,76 @@ Toggle scoring information display:
- Enable by default with `debug.show_scores = true`
MULTI-SELECT AND QUICKFIX INTEGRATION
Select multiple files and send them to Neovims quickfix list (keymaps are
configurable):
- `<Tab>` - Toggle selection for the current file (shows thick border `▊` in signcolumn)
- `<C-q>` - Send selected files to quickfix list and close picker
GIT STATUS HIGHLIGHTING
FFF integrates with git to show file status through sign column indicators
(enabled by default) and optional filename text coloring.
**Sign Column Indicators** (enabled by default) - Border characters shown in
the sign column:
>lua
hl = {
git_sign_staged = 'FFFGitSignStaged',
git_sign_modified = 'FFFGitSignModified',
git_sign_deleted = 'FFFGitSignDeleted',
git_sign_renamed = 'FFFGitSignRenamed',
git_sign_untracked = 'FFFGitSignUntracked',
git_sign_ignored = 'FFFGitSignIgnored',
}
<
**Text Highlights** (opt-in) - Apply colors to filenames based on git status:
To enable git status text coloring, set `git.status_text_color = true`:
>lua
require('fff').setup({
git = {
status_text_color = true, -- Enable git status colors on filename text
},
hl = {
git_staged = 'FFFGitStaged', -- Files staged for commit
git_modified = 'FFFGitModified', -- Modified unstaged files
git_deleted = 'FFFGitDeleted', -- Deleted files
git_renamed = 'FFFGitRenamed', -- Renamed files
git_untracked = 'FFFGitUntracked', -- New untracked files
git_ignored = 'FFFGitIgnored', -- Git-ignored files
}
})
<
The plugin provides sensible default highlight groups that link to common git
highlight groups (e.g., GitSignsAdd, GitSignsChange). You can override these
with your own custom highlight groups to match your colorscheme.
**Example - Custom Bright Colors for Text:**
>lua
vim.api.nvim_set_hl(0, 'CustomGitModified', { fg = '#FFA500' })
vim.api.nvim_set_hl(0, 'CustomGitUntracked', { fg = '#00FF00' })
require('fff').setup({
git = {
status_text_color = true,
},
hl = {
git_modified = 'CustomGitModified',
git_untracked = 'CustomGitUntracked',
}
})
<
TROUBLESHOOTING ~
+44 -17
View File
@@ -10,6 +10,8 @@ local overlay_state = {
last_row = nil,
last_col = nil,
last_border_hl = nil,
-- Track if combo was rendered in last call
was_rendered = false,
}
local LEFT_OVERLAY_CONTENT = '├────'
@@ -104,9 +106,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)
local function update_overlays(list_win, combo_header_line, border_hl, prompt_position)
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
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
@@ -121,20 +129,16 @@ local function update_overlays(list_win, combo_header_line, border_hl)
return
end
-- Cache current values
overlay_state.last_row = combo_header_row
overlay_state.last_col = list_config.col
overlay_state.last_border_hl = border_hl
-- Update both overlays in a batch to minimize API calls
local left_buf = get_or_create_overlay_buf('left_buf')
local right_buf = get_or_create_overlay_buf('right_buf')
-- Update content for both buffers
update_overlay_content(left_buf, LEFT_OVERLAY_CONTENT, border_hl)
update_overlay_content(right_buf, RIGHT_OVERLAY_CONTENT, border_hl)
-- Position both windows
position_overlay_window('left_win', left_buf, LEFT_OVERLAY_WIDTH, combo_header_row, list_config.col)
position_overlay_window(
'right_win',
@@ -156,10 +160,10 @@ local function clear_overlays_internal()
overlay_state.right_win = nil
end
-- Clear cache
overlay_state.last_row = nil
overlay_state.last_col = nil
overlay_state.last_border_hl = nil
-- Note: we intentionally don't clear was_rendered here to track the transition
end
function M.detect_and_prepare(items, file_picker, win_width, combo_boost_score_multiplier, disable_combo_display)
@@ -171,6 +175,8 @@ function M.detect_and_prepare(items, file_picker, win_width, combo_boost_score_m
return true, header_line, text_len, combo_item_index
end
--- Render combo highlights and overlays
--- @return boolean was_hidden True if combo was just hidden (was rendered before, not now)
function M.render_highlights_and_overlays(
combo_item_index,
text_len,
@@ -178,26 +184,47 @@ function M.render_highlights_and_overlays(
list_win,
ns_id,
border_hl,
item_to_lines
item_to_lines,
prompt_position
)
local was_rendered_before = overlay_state.was_rendered
local is_rendering_now = false
if not combo_item_index then
clear_overlays_internal()
return
else
local combo_item_lines = item_to_lines[combo_item_index]
if not combo_item_lines then
clear_overlays_internal()
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)
is_rendering_now = true
end
end
local combo_item_lines = item_to_lines[combo_item_index]
if not combo_item_lines then
clear_overlays_internal()
return
end
overlay_state.was_rendered = is_rendering_now
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)
-- Return true if combo was just hidden (transition from visible to hidden)
return was_rendered_before and not is_rendering_now
end
--- Get the combo header text for a given item
--- @param combo_count number The combo multiplier count
--- @param win_width number Window width for formatting
--- @param disable_combo_display boolean Whether to show combo count
--- @return string header_text The formatted header line
--- @return number text_len Length of the header text (without padding)
function M.get_combo_header_text(combo_count, win_width, disable_combo_display)
return create_header_text(combo_count, win_width, disable_combo_display)
end
function M.get_overlay_widths() return LEFT_OVERLAY_WIDTH, RIGHT_OVERLAY_WIDTH end
function M.cleanup() clear_overlays_internal() end
function M.cleanup()
clear_overlays_internal()
overlay_state.was_rendered = false
end
return M
+4
View File
@@ -157,6 +157,7 @@ local function init()
debug = 'Comment',
combo_header = 'Number',
scrollbar = 'Comment',
directory_path = 'Comment', -- Highlight for directory path in file list
-- Multi-select highlights
selected = 'FFFSelected',
selected_active = 'FFFSelectedActive',
@@ -192,6 +193,9 @@ local function init()
min_combo_count = 3, -- Minimum selections before combo boost applies (3 = boost starts on 3rd selection)
combo_boost_score_multiplier = 100, -- Score multiplier for combo matches (files repeatedly opened with same query)
},
git = {
status_text_color = false, -- Apply git status colors to filename text (default: false, only sign column)
},
debug = {
enabled = false, -- Set to true to show scores in the UI
show_scores = false,
+6 -6
View File
@@ -1,6 +1,5 @@
local M = {}
local system = require('fff.utils.system')
local uv = vim and vim.uv or require('luv')
local GITHUB_REPO = 'dmtrKovalenko/fff.nvim'
@@ -24,7 +23,7 @@ end
local function binary_exists(plugin_dir)
local binary_path = get_binary_path(plugin_dir)
local stat = uv.fs_stat(binary_path)
local stat = vim.uv.fs_stat(binary_path)
return stat and stat.type == 'file'
end
@@ -32,9 +31,10 @@ local function download_file(url, output_path, opts, callback)
opts = opts or {}
local dir = vim.fn.fnamemodify(output_path, ':h')
uv.fs_mkdir(dir, 493, function(err) -- 493 = 0755 octal
if err and not err:match('EEXIST') then
callback(false, 'Failed to create directory: ' .. err)
mkdir_recursive(dir, function(mkdir_ok, mkdir_err)
if not mkdir_ok then
callback(false, mkdir_err)
return
end
@@ -89,7 +89,7 @@ local function download_from_github(version, binary_path, opts, callback)
local ok, err_msg = pcall(function() package.loadlib(binary_path, 'luaopen_fff_nvim') end)
if not ok then
uv.fs_unlink(binary_path)
vim.uv.fs_unlink(binary_path)
callback(false, 'Downloaded binary is not valid: ' .. (err_msg or 'unknown error'))
return
end
+248
View File
@@ -0,0 +1,248 @@
--- File Renderer
--- Simple renderer for file items with 2 functions: render_line and apply_highlights
local M = {}
--- Render Context passed to renderer functions
--- @class RenderContext
--- @field config table User configuration from conf.get()
--- @field items table[] Array of file items being rendered
--- @field cursor number Current cursor position (1-based index into items)
--- @field win_height number Window height in lines
--- @field win_width number Window width in columns
--- @field max_path_width number Maximum width for file paths
--- @field debug_enabled boolean Whether debug mode is enabled (shows frecency scores)
--- @field prompt_position string Prompt position: 'top' or 'bottom'
--- @field has_combo boolean Whether combo boost is active
--- @field combo_header_line string Formatted combo header line (if has_combo)
--- @field combo_header_text_len number Length of combo header text (if has_combo)
--- @field combo_item_index number Index of item with combo (usually 1)
--- @field display_start number Start index for displayed items
--- @field display_end number End index for displayed items
--- @field iter_start number Iteration start (may differ from display_start for bottom prompt)
--- @field iter_end number Iteration end (may differ from display_end for bottom prompt)
--- @field iter_step number Iteration step (1 for top prompt, -1 for bottom prompt)
--- @field format_file_display fun(item: table, max_width: number): string, string Helper function to format filename and dir path
--- @field selected_files table<string, boolean> Map of selected file paths
--- @field query string Current search query
--- @field renderer table|nil Custom renderer (if provided via opts)
--- File Item structure from Rust
--- @class FileItem
--- @field path string Absolute file path
--- @field relative_path string Relative file path from base directory
--- @field name string File name
--- @field extension string File extension
--- @field size number File size in bytes
--- @field modified number Last modified timestamp
--- @field total_frecency_score number Total frecency score
--- @field access_frecency_score number Access-based frecency score
--- @field modification_frecency_score number Modification-based frecency score
--- @field git_status number|nil Git status enum (if file is in git repo)
--- Renderer Interface:
--- @field render_line fun(item: FileItem, ctx: RenderContext, item_idx: number): string[] Returns array of line strings
--- @field apply_highlights fun(item: FileItem, ctx: RenderContext, item_idx: number, buf: number, ns_id: number, line_idx: number, line_content: string): nil Applies highlights to the rendered line
--- Render a file item line
--- @param item FileItem File item from Rust
--- @param ctx RenderContext Render context with all state
--- @param item_idx number Item index (1-based)
--- @return string[] Array of line strings (1 or 2 lines if combo)
function M.render_line(item, ctx, item_idx)
local icons = require('fff.file_picker.icons')
local lines = {}
-- Check if this should have combo header (first item with combo boost)
local has_combo = item_idx == 1 and ctx.has_combo and ctx.combo_header_line
if has_combo then table.insert(lines, ctx.combo_header_line) end
-- Get icon
local icon, icon_hl_group = icons.get_icon(item.name, item.extension, false)
-- Build frecency indicator (debug mode only)
local frecency = ''
if ctx.debug_enabled then
local total = item.total_frecency_score or 0
local access = item.access_frecency_score or 0
local mod = item.modification_frecency_score or 0
if total > 0 then
local indicator = ''
if mod >= 6 then
indicator = '🔥'
elseif access >= 4 then
indicator = ''
elseif total >= 3 then
indicator = ''
elseif total >= 1 then
indicator = ''
end
frecency = string.format(' %s%d', indicator, total)
end
end
-- Format filename and path
local icon_width = icon and (vim.fn.strdisplaywidth(icon) + 1) or 0
local available_width = math.max(ctx.max_path_width - icon_width - #frecency, 40)
local filename, dir_path = ctx.format_file_display(item, available_width)
-- Build line
local line = icon and string.format('%s %s %s%s', icon, filename, dir_path, frecency)
or string.format('%s %s%s', filename, dir_path, frecency)
local padding = math.max(0, ctx.win_width - vim.fn.strdisplaywidth(line) + 5)
table.insert(lines, line .. string.rep(' ', padding))
return lines
end
--- Apply highlights to a rendered line
--- @param item FileItem File item from Rust
--- @param ctx RenderContext Render context with all state
--- @param item_idx number Item index (1-based)
--- @param buf number Buffer handle
--- @param ns_id number Namespace ID
--- @param line_idx number 1-based line index in buffer
--- @param line_content string The actual line content
function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_content)
local icons = require('fff.file_picker.icons')
local git_utils = require('fff.git_utils')
local file_picker = require('fff.file_picker')
local is_cursor = (ctx.cursor == item_idx)
local score = file_picker.get_file_score(item_idx)
local is_current_file = score and score.current_file_penalty and score.current_file_penalty < 0
-- Get icon and paths
local icon, icon_hl_group = icons.get_icon(item.name, item.extension, false)
local icon_width = icon and (vim.fn.strdisplaywidth(icon) + 1) or 0
local available_width = math.max(ctx.max_path_width - icon_width, 40)
local filename, dir_path = ctx.format_file_display(item, available_width)
-- 1. Cursor highlight
if is_cursor then
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx - 1, 0, {
end_col = 0,
end_row = line_idx,
hl_group = ctx.config.hl.active_file,
hl_eol = true,
priority = 100,
})
end
-- 2. Icon
if icon and icon_hl_group and vim.fn.strdisplaywidth(icon) > 0 then
local icon_hl = is_current_file and 'Comment' or icon_hl_group
vim.api.nvim_buf_add_highlight(buf, ns_id, icon_hl, line_idx - 1, 0, vim.fn.strdisplaywidth(icon))
end
-- 3. Git text color (filename)
if ctx.config.git and ctx.config.git.status_text_color and icon and #filename > 0 then
local git_text_hl = item.git_status and git_utils.get_text_highlight(item.git_status) or nil
if git_text_hl and git_text_hl ~= '' and not is_current_file then
local filename_start = #icon + 1
vim.api.nvim_buf_add_highlight(buf, ns_id, git_text_hl, line_idx - 1, filename_start, filename_start + #filename)
end
end
-- 4. Frecency indicator
if ctx.debug_enabled then
local start_pos, end_pos = line_content:find('[⭐🔥✨•]%d+')
if start_pos then
vim.api.nvim_buf_add_highlight(buf, ns_id, ctx.config.hl.frecency, line_idx - 1, start_pos - 1, end_pos)
end
end
-- 5. Directory path (dimmed)
if #filename > 0 and #dir_path > 0 then
local prefix_len = #filename + 1 -- filename bytes + space
if icon then
prefix_len = prefix_len + #icon + 1 -- if icon add icon bytes + space
end
vim.api.nvim_buf_add_highlight(
buf,
ns_id,
ctx.config.hl.directory_path or 'Comment',
line_idx - 1,
prefix_len,
prefix_len + #dir_path
)
end
-- 6. Current file
if is_current_file then
if not is_cursor then vim.api.nvim_buf_add_highlight(buf, ns_id, 'Comment', line_idx - 1, 0, -1) end
local virt_text_hl = is_cursor and ctx.config.hl.active_file or 'Comment'
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx - 1, 0, {
virt_text = { { ' (current)', virt_text_hl } },
virt_text_pos = 'right_align',
})
end
-- 7. Git sign
if item.git_status and git_utils.should_show_border(item.git_status) then
local border_char = git_utils.get_border_char(item.git_status)
local border_hl
if is_cursor then
local base_hl = git_utils.get_border_highlight(item.git_status)
if base_hl and base_hl ~= '' then
local border_fg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(base_hl)), 'fg')
local cursor_bg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(ctx.config.hl.active_file)), 'bg')
local temp_hl_name = 'FFFGitBorderSelected_' .. item_idx
if border_fg ~= '' and cursor_bg ~= '' then
vim.api.nvim_set_hl(0, temp_hl_name, { fg = border_fg, bg = cursor_bg })
border_hl = temp_hl_name
else
border_hl = git_utils.get_border_highlight_selected(item.git_status)
end
else
border_hl = ctx.config.hl.active_file
end
else
border_hl = git_utils.get_border_highlight(item.git_status)
end
if border_hl and border_hl ~= '' then
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx - 1, 0, {
sign_text = border_char,
sign_hl_group = border_hl,
priority = 1000,
})
end
elseif is_cursor then
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx - 1, 0, {
sign_text = ' ',
sign_hl_group = ctx.config.hl.active_file,
priority = 1000,
})
end
-- 8. Selection
if ctx.selected_files and ctx.selected_files[item.path] then
local selection_hl = is_cursor and ctx.config.hl.selected_active or ctx.config.hl.selected
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx - 1, 0, {
sign_text = '',
sign_hl_group = selection_hl,
priority = 1001,
})
end
-- 9. Query match
if ctx.query and ctx.query ~= '' then
local match_start, match_end = string.find(line_content, ctx.query, 1)
if match_start and match_end then
vim.api.nvim_buf_add_highlight(
buf,
ns_id,
ctx.config.hl.matched or 'IncSearch',
line_idx - 1,
match_start - 1,
match_end
)
end
end
end
return M
+4 -4
View File
@@ -72,9 +72,9 @@ end
--- Get highlight group for git status text
--- @param git_status string Git status
--- @return string Highlight group name
function M.get_highlight(git_status)
function M.get_text_highlight(git_status)
ensure_cache()
return highlights_cache[git_status] or ''
return highlights_cache and highlights_cache[git_status] or ''
end
--- Get border highlight group for git status
@@ -82,7 +82,7 @@ end
--- @return string Highlight group name
function M.get_border_highlight(git_status)
ensure_cache()
return border_highlights_cache[git_status] or ''
return border_highlights_cache and border_highlights_cache[git_status] or ''
end
--- Get selected border highlight group for git status
@@ -90,7 +90,7 @@ end
--- @return string Highlight group name
function M.get_border_highlight_selected(git_status)
ensure_cache()
return border_highlights_selected_cache[git_status] or ''
return border_highlights_selected_cache and border_highlights_selected_cache[git_status] or ''
end
function M.get_border_char(git_status) return M.border_chars[git_status] or '' end
+3 -2
View File
@@ -10,10 +10,11 @@ M.state = { initialized = false }
function M.setup(config) vim.g.fff = config end
--- Find files in current directory
function M.find_files()
--- @param opts? table Optional configuration {renderer = custom_renderer}
function M.find_files(opts)
local picker_ok, picker_ui = pcall(require, 'fff.picker_ui')
if picker_ok then
picker_ui.open()
picker_ui.open(opts)
else
vim.notify('Failed to load picker UI: ' .. picker_ui, vim.log.levels.ERROR)
end
+216 -278
View File
@@ -3,8 +3,6 @@ local M = {}
local conf = require('fff.conf')
local file_picker = require('fff.file_picker')
local preview = require('fff.file_picker.preview')
local icons = require('fff.file_picker.icons')
local git_utils = require('fff.git_utils')
local utils = require('fff.utils')
local location_utils = require('fff.location_utils')
local combo_renderer = require('fff.combo_renderer')
@@ -260,6 +258,10 @@ M.state = {
history_offset = nil, -- Current offset in history (nil = not cycling, 0 = first query)
next_search_force_combo_boost = false, -- Force combo boost on next search (for history recall)
-- Combo state
combo_visible = true, -- Whether to show combo indicator (hidden after significant navigation)
combo_initial_cursor = nil, -- Initial cursor position when combo was shown
-- Pagination state
pagination = {
page_index = 0, -- Current page index (0-based)
@@ -270,6 +272,9 @@ M.state = {
config = nil,
-- Custom renderer (optional, defaults to file_renderer if not provided)
renderer = nil,
ns_id = nil,
last_status_info = nil,
@@ -798,6 +803,10 @@ function M.update_results_sync()
M.state.pagination.page_size = page_size
M.state.pagination.page_index = 0 -- Reset to first page on new search
-- Reset combo visibility on new search
M.state.combo_visible = true
M.state.combo_initial_cursor = 1 -- Will be at position 1 after search
-- Check if we should force combo boost for this search (history recall)
local min_combo_override = nil
if M.state.next_search_force_combo_boost then
@@ -1015,76 +1024,56 @@ local function format_file_display(item, max_width)
return filename, display_path
end
--- Calculate number of rows an item will occupy when rendered
--- @param item_index number Index of the item (1-based)
--- @param has_combo boolean Whether any combo boost exists
--- @param combo_item_index number|nil Index of the combo-boosted item
--- @return number Number of rows (1 or 2 currently)
local function get_item_row_count(item_index, has_combo, combo_item_index)
if has_combo and item_index == combo_item_index then
return 2 -- Combo header line + content line
end
return 1 -- Just content line
--- Adjust scroll for bottom prompt to eliminate gaps
local function scroll_to_bottom()
if not M.state.list_win or not vim.api.nvim_win_is_valid(M.state.list_win) then return end
local win_height = vim.api.nvim_win_get_height(M.state.list_win)
local buf_lines = vim.api.nvim_buf_line_count(M.state.list_buf)
vim.api.nvim_win_call(M.state.list_win, function()
local view = vim.fn.winsaveview()
-- Force topline to show content at bottom
view.topline = math.max(1, buf_lines - win_height + 1)
vim.fn.winrestview(view)
end)
end
function M.render_list()
if not M.state.active then return end
--- Build rendering context with all necessary data
--- @return table Context object with items, config, dimensions, combo info, etc.
local function build_render_context()
local config = conf.get()
local items = M.state.filtered_items
local max_path_width = config.ui and config.ui.max_path_width or 80
local debug_enabled = config and config.debug and config.debug.show_scores
local win_height = vim.api.nvim_win_get_height(M.state.list_win)
local win_width = vim.api.nvim_win_get_width(M.state.list_win)
local empty_lines_needed = 0
local combo_boost_score_multiplier = config.history and config.history.combo_boost_score_multiplier or 100
local has_combo, combo_header_line, combo_header_text_len, combo_item_index = combo_renderer.detect_and_prepare(
items,
file_picker,
win_width,
combo_boost_score_multiplier,
-- disable rendering of combos if cycling through history or user wants to always show the last match
M.state.next_search_force_combo_boost or config.history.min_combo_count == 0
)
M.state.next_search_force_combo_boost = false -- effectively reset if set by the history recall
-- Calculate how many items fit (accounting for multi-row items)
local display_count = 0
local accumulated_rows = 0
if #items > 0 then
display_count = 1 -- Always show at least first item, even if it exceeds win_height
accumulated_rows = get_item_row_count(1, has_combo, combo_item_index)
for i = 2, #items do
local item_rows = get_item_row_count(i, has_combo, combo_item_index)
if accumulated_rows + item_rows > win_height then
break -- Next item won't fit
end
accumulated_rows = accumulated_rows + item_rows
display_count = i
end
end
local prompt_position = get_prompt_position()
-- All items in M.state.items should be displayed (already paginated)
local display_start = 1
local display_end = #items
-- Simple cursor validation
-- Cursor validation
if M.state.cursor < 1 then
M.state.cursor = 1
elseif M.state.cursor > #items then
M.state.cursor = #items
end
local padded_lines = {}
local icon_data = {}
local path_data = {}
local item_to_lines = {} -- Maps item index to its line indices {first_line, last_line}
-- Combo detection
local combo_boost_score_multiplier = config.history and config.history.combo_boost_multiplier or 100
local has_combo, combo_header_line, combo_header_text_len, combo_item_index = combo_renderer.detect_and_prepare(
items,
file_picker,
win_width,
combo_boost_score_multiplier,
M.state.next_search_force_combo_boost or config.history.min_combo_count == 0
)
M.state.next_search_force_combo_boost = false
-- For bottom prompt, iterate in reverse order to render best results at bottom
if has_combo and not M.state.combo_visible then
has_combo = false
combo_item_index = nil
end
-- Determine iteration order
local display_start = 1
local display_end = #items
local iter_start, iter_end, iter_step
if prompt_position == 'bottom' then
iter_start, iter_end, iter_step = display_end, display_start, -1
@@ -1092,265 +1081,187 @@ function M.render_list()
iter_start, iter_end, iter_step = display_start, display_end, 1
end
for i = iter_start, iter_end, iter_step do
local item = items[i]
local item_start_line = #padded_lines + 1
return {
config = config,
items = items,
cursor = M.state.cursor,
win_height = win_height,
win_width = win_width,
max_path_width = config.ui and config.ui.max_path_width or 80,
debug_enabled = config and config.debug and config.debug.show_scores,
prompt_position = prompt_position,
has_combo = has_combo,
combo_header_line = combo_header_line,
combo_header_text_len = combo_header_text_len,
combo_item_index = combo_item_index,
display_start = display_start,
display_end = display_end,
iter_start = iter_start,
iter_end = iter_end,
iter_step = iter_step,
renderer = M.state.renderer, -- Custom renderer (if provided)
}
end
-- For combo items, insert header first
if has_combo and combo_item_index and i == combo_item_index then table.insert(padded_lines, combo_header_line) end
--- Generate all display lines
--- Uses renderer.render_line for each item
--- @param ctx table Render context
--- @return table lines Array of padded line strings
--- @return table item_to_lines Mapping of item index to {first, last} line indices
local function generate_item_lines(ctx)
local lines = {}
local item_to_lines = {}
local icon, icon_hl_group = icons.get_icon(item.name, item.extension, false)
icon_data[i] = { icon, icon_hl_group }
-- Add format_file_display to context for renderers
ctx.format_file_display = format_file_display
local frecency = ''
if debug_enabled then
local total_frecency = (item.total_frecency_score or 0)
local access_frecency = (item.access_frecency_score or 0)
local mod_frecency = (item.modification_frecency_score or 0)
-- Use custom renderer if provided, otherwise use default file_renderer
local renderer = ctx.renderer
if not renderer then renderer = require('fff.file_renderer') end
if total_frecency > 0 then
local indicator = ''
if mod_frecency >= 6 then
indicator = '🔥'
elseif access_frecency >= 4 then
indicator = ''
elseif total_frecency >= 3 then
indicator = ''
elseif total_frecency >= 1 then
indicator = ''
end
frecency = string.format(' %s%d', indicator, total_frecency)
end
for i = ctx.iter_start, ctx.iter_end, ctx.iter_step do
local item = ctx.items[i]
local item_start_line = #lines + 1
-- Render item lines using renderer.render_line
local item_lines = renderer.render_line(item, ctx, i)
-- Add rendered lines
for _, line in ipairs(item_lines) do
table.insert(lines, line)
end
local icon_width = icon and (vim.fn.strdisplaywidth(icon) + 1) or 0
local available_width = math.max(max_path_width - icon_width - #frecency, 40)
local filename, dir_path = format_file_display(item, available_width)
path_data[i] = { filename, dir_path }
local line = icon and string.format('%s %s %s%s', icon, filename, dir_path, frecency)
or string.format('%s %s%s', filename, dir_path, frecency)
local line_len = vim.fn.strdisplaywidth(line)
local padding = math.max(0, win_width - line_len + 5)
table.insert(padded_lines, line .. string.rep(' ', padding))
-- Record line range for this item
local item_end_line = #padded_lines
local item_end_line = #lines
item_to_lines[i] = {
first = item_start_line,
last = item_end_line,
}
end
-- Handle bottom positioning: add empty lines at the top
local empty_line_offset = 0
if prompt_position == 'bottom' then
local total_content_lines = #padded_lines
empty_lines_needed = math.max(0, win_height - total_content_lines)
return lines, item_to_lines
end
if empty_lines_needed > 0 then
-- Insert empty lines at the beginning
for i = empty_lines_needed, 1, -1 do
table.insert(padded_lines, 1, string.rep(' ', win_width + 5))
end
empty_line_offset = empty_lines_needed
--- Apply bottom padding for bottom prompt position
--- Adds empty lines at the top and adjusts all line indices
--- @param lines table Array of line strings (mutated in place)
--- @param item_to_lines table Item to lines mapping (mutated in place)
--- @param ctx table Render context
local function apply_bottom_padding(lines, item_to_lines, ctx)
if ctx.prompt_position ~= 'bottom' then return end
-- Adjust item_to_lines mapping
for i = display_start, display_end do
if item_to_lines[i] then
item_to_lines[i].first = item_to_lines[i].first + empty_line_offset
item_to_lines[i].last = item_to_lines[i].last + empty_line_offset
end
local total_content_lines = #lines
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
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
item_to_lines[i].last = item_to_lines[i].last + empty_lines_needed
end
end
end
end
-- Calculate cursor line based on current item
--- Update buffer content and position cursor
--- @param lines table Array of line strings
--- @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 #items > 0 and M.state.cursor >= 1 and M.state.cursor <= #items then
local cursor_item = item_to_lines[M.state.cursor]
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, padded_lines)
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)
-- Set cursor position
if #items > 0 and cursor_line > 0 and cursor_line <= win_height then
-- 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
end
-- Apply highlighting to all items
if #items > 0 then
for i = display_start, display_end do
local item = items[i]
local item_lines = item_to_lines[i]
if item_lines then
local is_cursor_item = (M.state.cursor == i)
--- Apply all highlights using renderer.apply_highlights
--- @param lines table Array of line strings
--- @param item_to_lines table Item to lines mapping
--- @param ctx table Render context
local function apply_all_highlights(lines, item_to_lines, ctx)
ctx.selected_files = M.state.selected_files
ctx.query = M.state.query
-- Highlight only the content line (last line), not the combo header
if is_cursor_item then
local content_line = item_lines.last
-- Highlight entire line and extend to EOL
vim.api.nvim_buf_set_extmark(M.state.list_buf, M.state.ns_id, content_line - 1, 0, {
end_col = 0,
end_row = content_line,
hl_group = M.state.config.hl.active_file,
hl_eol = true,
priority = 100,
})
end
local renderer = ctx.renderer
if not renderer then renderer = require('fff.file_renderer') end
-- Now apply file-specific highlights to the last line
local line_idx = item_lines.last
local line_content = padded_lines[line_idx]
for i = ctx.display_start, ctx.display_end do
local item = ctx.items[i]
local item_lines = item_to_lines[i]
if not item_lines then goto continue end
if line_content then
local icon, icon_hl_group = unpack(icon_data[i])
local filename, dir_path = unpack(path_data[i])
local line_idx = item_lines.last
local line_content = lines[line_idx]
local score = file_picker.get_file_score(i)
local is_current_file = score and score.current_file_penalty and score.current_file_penalty < 0
if not line_content then goto continue end
-- Icon highlighting
if icon and icon_hl_group and vim.fn.strdisplaywidth(icon) > 0 then
local icon_highlight = is_current_file and 'Comment' or icon_hl_group
vim.api.nvim_buf_add_highlight(
M.state.list_buf,
M.state.ns_id,
icon_highlight,
line_idx - 1,
0,
vim.fn.strdisplaywidth(icon)
)
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
-- Frecency highlighting
if debug_enabled then
local star_start, star_end = line_content:find('⭐%d+')
if star_start then
vim.api.nvim_buf_add_highlight(
M.state.list_buf,
M.state.ns_id,
M.state.config.hl.frecency,
line_idx - 1,
star_start - 1,
star_end
)
end
end
local icon_match = line_content:match('^%S+')
if icon_match and #filename > 0 and #dir_path > 0 then
local prefix_len = #icon_match + 1 + #filename + 1
vim.api.nvim_buf_add_highlight(
M.state.list_buf,
M.state.ns_id,
'Comment',
line_idx - 1,
prefix_len,
prefix_len + #dir_path
)
end
if is_current_file then
if not is_cursor_item then
vim.api.nvim_buf_add_highlight(M.state.list_buf, M.state.ns_id, 'Comment', line_idx - 1, 0, -1)
end
local virt_text_hl = is_cursor_item and M.state.config.hl.active_file or 'Comment'
vim.api.nvim_buf_set_extmark(M.state.list_buf, M.state.ns_id, line_idx - 1, 0, {
virt_text = { { ' (current)', virt_text_hl } },
virt_text_pos = 'right_align',
})
end
if item.git_status and git_utils.should_show_border(item.git_status) then
local border_char = git_utils.get_border_char(item.git_status)
local border_hl
if is_cursor_item then
-- When selected, create a combined highlight: border color on cursor background
local base_hl = git_utils.get_border_highlight(item.git_status)
if base_hl and base_hl ~= '' then
-- Get the foreground color from the border highlight
local border_fg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(base_hl)), 'fg')
-- Get the background from cursor highlight
local cursor_bg = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(M.state.config.hl.active_file)), 'bg')
-- Create temporary highlight group
local temp_hl_name = 'FFFGitBorderSelected_' .. i
if border_fg ~= '' and cursor_bg ~= '' then
vim.api.nvim_set_hl(0, temp_hl_name, { fg = border_fg, bg = cursor_bg })
border_hl = temp_hl_name
else
border_hl = git_utils.get_border_highlight_selected(item.git_status)
end
else
border_hl = M.state.config.hl.active_file
end
else
border_hl = git_utils.get_border_highlight(item.git_status)
end
if border_hl and border_hl ~= '' then
vim.api.nvim_buf_set_extmark(M.state.list_buf, M.state.ns_id, line_idx - 1, 0, {
sign_text = border_char,
sign_hl_group = border_hl,
priority = 1000,
})
end
elseif is_cursor_item then
vim.api.nvim_buf_set_extmark(M.state.list_buf, M.state.ns_id, line_idx - 1, 0, {
sign_text = ' ',
sign_hl_group = M.state.config.hl.active_file,
priority = 1000,
})
end
if M.state.selected_files[item.path] then
local selection_hl = is_cursor_item and M.state.config.hl.selected_active or M.state.config.hl.selected
vim.api.nvim_buf_set_extmark(M.state.list_buf, M.state.ns_id, line_idx - 1, 0, {
sign_text = '',
sign_hl_group = selection_hl,
priority = 1001, -- Higher than git status (1000)
})
end
local match_start, match_end = string.find(line_content, M.state.query, 1)
if match_start and match_end then
vim.api.nvim_buf_add_highlight(
M.state.list_buf,
M.state.ns_id,
config.hl.matched or 'IncSearch',
line_idx - 1,
match_start - 1,
match_end
)
end
end
end
end
combo_renderer.render_highlights_and_overlays(
combo_item_index,
combo_header_text_len,
M.state.list_buf,
M.state.list_win,
M.state.ns_id,
M.state.config.hl.border,
item_to_lines
)
-- 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 scrollbar (will be created lazily if needed)
scrollbar.render(M.state.layout, M.state.config, M.state.list_win, M.state.pagination)
-- 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,
M.state.list_buf,
M.state.list_win,
M.state.ns_id,
ctx.config.hl.border,
item_to_lines,
ctx.prompt_position
)
-- Handle combo hiding with scroll adjustment
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
function M.render_list()
if not M.state.active then return end
local ctx = build_render_context()
local lines, item_to_lines = generate_item_lines(ctx)
apply_bottom_padding(lines, item_to_lines, ctx)
update_buffer_and_cursor(lines, item_to_lines, ctx)
if #ctx.items > 0 then apply_all_highlights(lines, item_to_lines, ctx) end
-- 6. Finalize with combo overlays and scrollbar
finalize_render(item_to_lines, ctx)
end
function M.update_preview()
@@ -1553,6 +1464,18 @@ function M.move_up()
M.render_list()
M.update_preview()
M.update_status()
if M.state.combo_initial_cursor and M.state.combo_visible then
local cursor_distance = math.abs(M.state.cursor - M.state.combo_initial_cursor)
local half_page = math.floor(M.state.pagination.page_size / 2)
if cursor_distance > half_page then
M.state.combo_visible = false
combo_renderer.cleanup()
M.render_list() -- Re-render once without combo
-- Scroll to bottom for bottom prompt to eliminate gap
if get_prompt_position() == 'bottom' then scroll_to_bottom() end
end
end
end
function M.move_down()
@@ -1597,6 +1520,18 @@ function M.move_down()
M.render_list()
M.update_preview()
M.update_status()
if M.state.combo_initial_cursor and M.state.combo_visible then
local cursor_distance = math.abs(M.state.cursor - M.state.combo_initial_cursor)
local half_page = math.floor(M.state.pagination.page_size / 2)
if cursor_distance > half_page then
M.state.combo_visible = false
combo_renderer.cleanup()
M.render_list() -- Re-render once without combo
-- Scroll to bottom for bottom prompt to eliminate gap
if get_prompt_position() == 'bottom' then scroll_to_bottom() end
end
end
end
--- Scroll preview up by half window height
@@ -1901,6 +1836,8 @@ function M.close()
M.state.current_file_cache = nil
M.state.location = nil
M.state.selected_files = {}
M.state.combo_visible = true
M.state.combo_initial_cursor = nil
M.reset_history_state()
-- Clean up picker focus autocmds
pcall(vim.api.nvim_del_augroup_by_name, 'fff_picker_focus')
@@ -2051,11 +1988,12 @@ end
--- @param opts.layout.prompt_position? string|function Prompt position: 'top'|'bottom' or function(terminal_width, terminal_height): string (default: 'bottom')
--- @param opts.layout.preview_position? string|function Preview position: 'left'|'right'|'top'|'bottom' or function(terminal_width, terminal_height): string (default: 'right')
--- @param opts.layout.preview_size? number|function Preview size as ratio (0.0-1.0) or function(terminal_width, terminal_height): number (default: 0.5)
--- @param opts.renderer? table Custom renderer implementing {render_line, apply_highlights} interface (default: file_renderer)
function M.open(opts)
if M.state.active then return end
-- Initialize selection state
M.state.selected_files = {}
M.state.renderer = opts and opts.renderer or nil
local merged_config, base_path = initialize_picker(opts)
if not merged_config then return end
+4 -2
View File
@@ -37,6 +37,7 @@ local function try_load_library()
local stat = vim.uv.fs_stat(actual_path)
if stat and stat.type == 'file' then
local loader, err = package.loadlib(actual_path, 'luaopen_fff_nvim')
if err then return nil, string.format('Error loading library from %s: %s', actual_path, err) end
if loader then return loader() end
end
end
@@ -44,12 +45,13 @@ local function try_load_library()
end
local backend, load_err = try_load_library()
if not backend then
if not backend or load_err then
local err_msg = string.format(
'Failed to load fff rust backend.\nError: %s\nSearched paths:\n%s\nMake sure binary exists with `cargo build --release`',
'Failed to load fff rust backend.\nError: %s\nSearched paths:\n%s\nMake sure binary exists or make it exists using \n `:lua require("fff.download").download_or_build_binary()`\nor\n`cargo build --release`\n(and rerun neovim after)',
tostring(load_err),
vim.inspect(paths)
)
error(err_msg)
end
+13 -2
View File
@@ -16,12 +16,15 @@ local ns_id = vim.api.nvim_create_namespace('fff_scrollbar')
--- @param config table Config with hl (highlight groups)
--- @param list_win number List window handle
--- @param pagination table Pagination state with page_index, page_size, total_matched
function M.render(layout, config, list_win, pagination)
--- @param prompt_position string|nil Prompt position ('top' or 'bottom', defaults to 'bottom')
function M.render(layout, config, list_win, pagination, prompt_position)
if layout.show_scrollbar == false then return end
-- this is the most often path, we don't want to show scrollbar if use doesn't scrolling
if not scrollbar_state.ever_shown and pagination.page_index == 0 then return end
prompt_position = prompt_position or 'bottom'
local total_pages = pagination.page_size > 0 and math.ceil(pagination.total_matched / pagination.page_size) or 1
local has_multiple_pages = total_pages > 1
local scrollbar_exists = scrollbar_state.win and vim.api.nvim_win_is_valid(scrollbar_state.win)
@@ -61,7 +64,15 @@ function M.render(layout, config, list_win, pagination)
local thumb_size = math.max(1, math.floor(win_height / total_pages))
local scrollbar_range = win_height - thumb_size
local thumb_start = math.floor((pagination.page_index / math.max(1, total_pages - 1)) * scrollbar_range)
-- inverse the scrollbar when the position is at the bottom
local thumb_start
if prompt_position == 'bottom' then
thumb_start =
math.floor(((total_pages - 1 - pagination.page_index) / math.max(1, total_pages - 1)) * scrollbar_range)
else
thumb_start = math.floor((pagination.page_index / math.max(1, total_pages - 1)) * scrollbar_range)
end
local lines = {}
for i = 1, win_height do
+31
View File
@@ -59,4 +59,35 @@ function M.is_one_of(value, values)
return false
end
-- Recursively create directories using uv.fs_mkdir
local function mkdir_recursive(path, callback)
vim.uv.fs_stat(path, function(err, stat)
if not err and stat then
callback(true, nil)
return
end
local parent = vim.fn.fnamemodify(path, ':h')
if parent == path or parent == '' or parent == '.' then
callback(false, 'Cannot create root directory')
return
end
mkdir_recursive(parent, function(parent_ok, parent_err)
if not parent_ok then
callback(false, parent_err)
return
end
uv.fs_mkdir(path, 493, function(mkdir_err) -- 493 = 0755 octal
if mkdir_err and not mkdir_err:match('EEXIST') then
callback(false, 'Failed to create directory: ' .. mkdir_err)
return
end
callback(true, nil)
end)
end)
end)
end
return M