From d19270808dc9956249fbe99759a1a18dbe0e11c3 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Tue, 5 Aug 2025 22:57:31 +0200 Subject: [PATCH 01/19] feat: implement prompt_position layout configuration Allows users to position the input prompt at 'top' or 'bottom' of the picker window. When prompt is at top, best matches appear at the top (closest to prompt). When prompt is at bottom (default), best matches appear at the bottom. - Add prompt_position config option with validation - Adapt list rendering order based on prompt position - Fix cursor navigation to work correctly with both layouts - Optimize array reversal for better performance - Extract helper function to reduce code duplication --- lua/fff/file_picker/init.lua | 2 +- lua/fff/main.lua | 2 +- lua/fff/picker_ui.lua | 85 +++++++++++++++++++++++++++--------- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/lua/fff/file_picker/init.lua b/lua/fff/file_picker/init.lua index 6b66597e..fc01f337 100644 --- a/lua/fff/file_picker/init.lua +++ b/lua/fff/file_picker/init.lua @@ -41,7 +41,7 @@ function M.setup(config) preview_down = '', }, layout = { - prompt_position = 'top', + prompt_position = 'bottom', preview_position = 'right', preview_width = 0.4, height = 0.8, diff --git a/lua/fff/main.lua b/lua/fff/main.lua index bc6031e6..db89373c 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -55,7 +55,7 @@ function M.setup(config) debug = 'Comment', }, layout = { - prompt_position = 'top', + prompt_position = 'bottom', preview_position = 'right', preview_width = 0.4, height = 0.8, diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index 436b9173..34e5cc30 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -6,6 +6,18 @@ local icons = require('fff.file_picker.icons') local git_utils = require('fff.git_utils') local main = require('fff.main') +local function get_prompt_position() + local config = M.state.config + + if config and config.layout and config.layout.prompt_position then + local pos = config.layout.prompt_position + + if pos == 'top' or pos == 'bottom' then return pos end + end + + return 'bottom' +end + -- Initialize preview with main config if main.config and main.config.preview then preview.setup(main.config.preview) end @@ -63,6 +75,17 @@ function M.create_ui() local list_width = width - preview_width - 3 -- Account for separators local list_height = height - 4 -- Same as list window height + local prompt_position = get_prompt_position() + local list_row, input_row + + if prompt_position == 'top' then + input_row = row + 1 + list_row = row + 3 + else + list_row = row + 1 + input_row = row + height - 2 + end + local file_info_height = 0 local preview_height = list_height if debug_enabled_in_preview then @@ -86,7 +109,7 @@ function M.create_ui() width = list_width, height = list_height, -- Use calculated list height col = col + 1, - row = row + 1, + row = list_row, border = 'single', style = 'minimal', title = ' Files ', @@ -131,7 +154,7 @@ function M.create_ui() width = list_width, height = 1, col = col + 1, - row = row + height - 2, + row = input_row, -- Use calculated row position based on prompt_position border = 'single', style = 'minimal', }) @@ -586,14 +609,22 @@ function M.render_list() table.insert(items_to_show, items[i]) end - local reversed_items = {} - for i = #items_to_show, 1, -1 do - table.insert(reversed_items, items_to_show[i]) + local prompt_position = get_prompt_position() + local display_items = {} + + if prompt_position == 'top' then + display_items = items_to_show + else + local items_length = #items_to_show + + for i = 1, items_length do + display_items[i] = items_to_show[items_length - i + 1] + end end local line_data = {} - for i, item in ipairs(reversed_items) do + for i, item in ipairs(display_items) do local icon, icon_hl_group = icons.get_icon_display(item.name, item.extension, false) local frecency = '' local total_frecency = (item.total_frecency_score or 0) @@ -656,8 +687,14 @@ function M.render_list() vim.api.nvim_buf_set_option(M.state.list_buf, 'modifiable', false) if #items > 0 then - -- cursor=1 means first/best item, which appears at the last line after reversal - local cursor_line = empty_lines_needed + (display_count - M.state.cursor + 1) + local prompt_position = get_prompt_position() + local cursor_line + + if prompt_position == 'top' then + cursor_line = empty_lines_needed + M.state.cursor + else + cursor_line = empty_lines_needed + (display_count - M.state.cursor + 1) + end if cursor_line > 0 and cursor_line <= win_height then vim.api.nvim_win_set_cursor(M.state.list_win, { cursor_line, 0 }) @@ -886,28 +923,36 @@ function M.update_status() }) end ---- Move cursor up (towards worse results, which are visually higher) function M.move_up() if not M.state.active then return end - if M.state.cursor < #M.state.filtered_items then - M.state.cursor = M.state.cursor + 1 - M.render_list() - M.update_preview() - M.update_status() + local prompt_position = get_prompt_position() + + if prompt_position == 'top' then + if M.state.cursor > 1 then M.state.cursor = M.state.cursor - 1 end + else + if M.state.cursor < #M.state.filtered_items then M.state.cursor = M.state.cursor + 1 end end + + M.render_list() + M.update_preview() + M.update_status() end ---- Move cursor down (towards better results, which are visually lower) function M.move_down() if not M.state.active then return end - if M.state.cursor > 1 then - M.state.cursor = M.state.cursor - 1 - M.render_list() - M.update_preview() - M.update_status() + local prompt_position = get_prompt_position() + + if prompt_position == 'top' then + if M.state.cursor < #M.state.filtered_items then M.state.cursor = M.state.cursor + 1 end + else + if M.state.cursor > 1 then M.state.cursor = M.state.cursor - 1 end end + + M.render_list() + M.update_preview() + M.update_status() end --- Scroll preview up by half window height From 94966bd43eb2f59caef024f39bb3a382bfe401fd Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Tue, 5 Aug 2025 23:58:43 +0200 Subject: [PATCH 02/19] feat: restructure configuration with layout object Move width/height into layout namespace for better organization. Adds max_threads field and reorganizes config hierarchy to be more intuitive and extensible. BREAKING CHANGE: config.width/height moved to config.layout.width/height --- README.md | 49 ++++++++++++++++++++++++------------------- doc/fff.nvim.txt | 39 +++++++++++++++++++--------------- lua/fff/main.lua | 10 ++++----- lua/fff/picker_ui.lua | 6 +++--- 4 files changed, 56 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 8f5da5c3..fde74faf 100644 --- a/README.md +++ b/README.md @@ -71,20 +71,25 @@ FFF.nvim requires: FFF.nvim comes with sensible defaults. Here's the complete default configuration: ```lua -require("fff").setup({ - -- UI dimensions and appearance - width = 0.8, -- Window width as fraction of screen - height = 0.8, -- Window height as fraction of screen - prompt = 'πŸͺΏ ', -- Input prompt symbol +require('fff').setup({ + prompt = 'πŸͺΏ ', + title = 'FFF Files', + max_results = 100, -- Maximum search results to display + max_threads = 4, -- Maximum threads for fuzzy search + preview = { - enabled = true, - width = 0.5, - max_lines = 100, - max_size = 1024 * 1024, -- 1MB + enabled = true, + max_lines = 100, + max_size = 1024 * 1024, -- 1MB + }, + + layout = { + height = 0.8, -- Window height as fraction of screen + width = 0.8, -- Window width as fraction of screen + prompt_position = 'bottom', -- 'top' or 'bottom' + preview_position = 'right', -- Currently only 'right' is supported + preview_width = 0.4, -- Preview width as fraction of window }, - title = 'FFF Files', -- Window title - max_results = 60, -- Maximum search results to display - max_threads = 4, -- Maximum threads for fuzzy search keymaps = { close = '', @@ -114,7 +119,7 @@ require("fff").setup({ -- Debug options debug = { - show_scores = false, -- Toggle with F2 or :FFFDebug + show_scores = false, -- Toggle with F2 or :FFFDebug }, }) ``` @@ -124,12 +129,12 @@ require("fff").setup({ #### Available methods ```lua -require("fff").find_files() -- Find files in current directory -require("fff").find_in_git_root() -- Find files in the current git repository -require("fff").scan_files() -- Trigger rescan of files in the current directory -require("fff").refresh_git_status() -- Refresh git status for the active file lock -require("fff").find_files_in_dir(path) -- Find files in a specific directory -require("fff").change_indexing_directory(new_path) -- Change the base directory for the file picker +require('fff').find_files() -- Find files in current directory +require('fff').find_in_git_root() -- Find files in the current git repository +require('fff').scan_files() -- Trigger rescan of files in the current directory +require('fff').refresh_git_status() -- Refresh git status for the active file lock +require('fff').find_files_in_dir(path) -- Find files in a specific directory +require('fff').change_indexing_directory(new_path) -- Change the base directory for the file picker ``` #### Multiple Key Bindings @@ -138,9 +143,9 @@ You can assign multiple key combinations to the same action: ```lua keymaps = { - move_up = { '', '', '' }, -- Three ways to move up - close = { '', '' }, -- Two ways to close - select = '', -- Single binding still works + move_up = { '', '', '' }, -- Three ways to move up + close = { '', '' }, -- Two ways to close + select = '', -- Single binding still works } ``` diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index 8e60e00c..633a56ef 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -74,25 +74,30 @@ LAZY.NVIM DEFAULT CONFIGURATION ~ -FFF.nvim comes with sensible defaults. Here’s the complete default +FFF.nvim comes with sensible defaults. Here's the complete default configuration: >lua - require("fff").setup({ - -- UI dimensions and appearance - width = 0.8, -- Window width as fraction of screen - height = 0.8, -- Window height as fraction of screen - prompt = 'πŸͺΏ ', -- Input prompt symbol + require('fff').setup({ + prompt = 'πŸͺΏ ', -- Input prompt symbol + title = 'FFF Files', -- Window title + max_results = 100, -- Maximum search results to display + max_threads = 4, -- Maximum threads for fuzzy search + preview = { - enabled = true, - width = 0.5, - max_lines = 100, - max_size = 1024 * 1024, -- 1MB + enabled = true, + max_lines = 100, + max_size = 1024 * 1024, -- 1MB }, - title = 'FFF Files', -- Window title - max_results = 60, -- Maximum search results to display - max_threads = 4, -- Maximum threads for fuzzy search - + + layout = { + height = 0.8, -- Window height as fraction of screen + width = 0.8, -- Window width as fraction of screen + prompt_position = 'bottom', -- 'top' or 'bottom' + preview_position = 'right', -- Currently only 'right' is supported + preview_width = 0.4, -- Preview width as fraction of window + }, + keymaps = { close = '', select = '', @@ -105,7 +110,7 @@ configuration: preview_scroll_up = '', preview_scroll_down = '', }, - + -- Highlight groups hl = { border = 'FloatBorder', @@ -118,10 +123,10 @@ configuration: frecency = 'Number', debug = 'Comment', }, - + -- Debug options debug = { - show_scores = false, -- Toggle with F2 or :FFFDebug + show_scores = false, -- Toggle with F2 or :FFFDebug }, }) < diff --git a/lua/fff/main.lua b/lua/fff/main.lua index db89373c..bd7db422 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -10,14 +10,12 @@ M.state = { initialized = false } function M.setup(config) local default_config = { base_path = vim.fn.getcwd(), - max_results = 100, prompt = 'πŸͺΏ ', -- Input prompt symbol title = 'FFF Files', -- Window title - width = 0.8, - height = 0.8, + max_results = 100, + max_threads = 4, preview = { enabled = true, - width = 0.5, max_lines = 5000, max_size = 10 * 1024 * 1024, -- 10MB imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', @@ -55,11 +53,11 @@ function M.setup(config) debug = 'Comment', }, layout = { + height = 0.8, + width = 0.8, prompt_position = 'bottom', preview_position = 'right', preview_width = 0.4, - height = 0.8, - width = 0.8, }, frecency = { enabled = true, diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index 34e5cc30..174f76a1 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -66,12 +66,12 @@ function M.create_ui() and main.config.debug and main.config.debug.show_scores - local width = math.floor(vim.o.columns * config.width) - local height = math.floor(vim.o.lines * config.height) + local width = math.floor(vim.o.columns * config.layout.width) + local height = math.floor(vim.o.lines * config.layout.height) local col = math.floor((vim.o.columns - width) / 2) local row = math.floor((vim.o.lines - height) / 2) - local preview_width = M.enabled_preview() and math.floor(width * config.preview.width) or 0 + local preview_width = M.enabled_preview() and math.floor(width * config.layout.preview_width) or 0 local list_width = width - preview_width - 3 -- Account for separators local list_height = height - 4 -- Same as list window height From 636c138c075b7875854768ec397af6cb6befdfdf Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 00:10:57 +0200 Subject: [PATCH 03/19] feat: implement flexible configuration deprecation system Replace hardcoded deprecation handling with rule-based system for easier maintenance. Each deprecation is now defined as a simple rule with old_path, new_path, and message. Enables easy addition of future config deprecations without touching core migration logic. --- lua/fff/main.lua | 96 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/lua/fff/main.lua b/lua/fff/main.lua index bd7db422..83d05806 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -2,16 +2,104 @@ local fuzzy = require('fff.fuzzy') if not fuzzy then error('Failed to load fff.fuzzy module. Ensure the Rust backend is compiled and available.') end local M = {} + M.config = {} M.state = { initialized = false } +local DEPRECATION_RULES = { + { + -- Top-level width -> layout.width + old_path = { 'width' }, + new_path = { 'layout', 'width' }, + message = 'config.width is deprecated. Use config.layout.width instead.', + }, + { + -- Top-level height -> layout.height + old_path = { 'height' }, + new_path = { 'layout', 'height' }, + message = 'config.height is deprecated. Use config.layout.height instead.', + }, + { + -- preview.width -> layout.preview_width + old_path = { 'preview', 'width' }, + new_path = { 'layout', 'preview_width' }, + message = 'config.preview.width is deprecated. Use config.layout.preview_width instead.', + }, +} + +--- Get value from nested table using path array +--- @param tbl table Source table +--- @param path table Array of keys to traverse +--- @return any|nil Value at path or nil if not found +local function get_nested_value(tbl, path) + local current = tbl + for _, key in ipairs(path) do + if type(current) ~= 'table' or current[key] == nil then return nil end + current = current[key] + end + + return current +end + +--- Set value in nested table using path array, creating intermediate tables +--- @param tbl table Target table +--- @param path table Array of keys to traverse +--- @param value any Value to set +local function set_nested_value(tbl, path, value) + local current = tbl + for i = 1, #path - 1 do + local key = path[i] + if type(current[key]) ~= 'table' then current[key] = {} end + current = current[key] + end + + current[path[#path]] = value +end + +--- Remove value from nested table using path array +--- @param tbl table Target table +--- @param path table Array of keys to traverse +local function remove_nested_value(tbl, path) + if #path == 0 then return end + + local current = tbl + for i = 1, #path - 1 do + local key = path[i] + if type(current[key]) ~= 'table' then return end + current = current[key] + end + + current[path[#path]] = nil +end + +--- Handle deprecated configuration options with migration warnings +--- @param user_config table User provided configuration +--- @return table Migrated configuration +local function handle_deprecated_config(user_config) + if not user_config then return {} end + + local migrated_config = vim.deepcopy(user_config) + + for _, rule in ipairs(DEPRECATION_RULES) do + local old_value = get_nested_value(user_config, rule.old_path) + if old_value ~= nil then + set_nested_value(migrated_config, rule.new_path, old_value) + remove_nested_value(migrated_config, rule.old_path) + + vim.notify('FFF: ' .. rule.message, vim.log.levels.WARN) + end + end + + return migrated_config +end + --- Setup the file picker with the given configuration --- @param config table Configuration options function M.setup(config) local default_config = { base_path = vim.fn.getcwd(), - prompt = 'πŸͺΏ ', -- Input prompt symbol - title = 'FFF Files', -- Window title + prompt = 'πŸͺΏ ', + title = 'FFF Files', max_results = 100, max_threads = 4, preview = { @@ -88,7 +176,9 @@ function M.setup(config) ui_enabled = true, } - local merged_config = vim.tbl_deep_extend('force', default_config, config or {}) + local migrated_user_config = handle_deprecated_config(config) + local merged_config = vim.tbl_deep_extend('force', default_config, migrated_user_config) + M.config = merged_config local db_path = merged_config.frecency.db_path or (vim.fn.stdpath('cache') .. '/fff_nvim') From f270e16909b82695d3cdd86555bf930993a74460 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 11:14:53 +0200 Subject: [PATCH 04/19] refactor: extract config resolution utilities to improve maintainability Replace repetitive inline pcall functions with centralized utilities in fff.utils. Eliminates code duplication and improves error handling consistency across dynamic configuration resolution. --- lua/fff/picker_ui.lua | 332 ++++++++++++++++++++++++++++++++++++------ lua/fff/utils.lua | 44 ++++++ 2 files changed, 332 insertions(+), 44 deletions(-) diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index 174f76a1..0521e4b4 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -5,19 +5,235 @@ local preview = require('fff.file_picker.preview') local icons = require('fff.file_picker.icons') local git_utils = require('fff.git_utils') local main = require('fff.main') +local utils = require('fff.utils') local function get_prompt_position() local config = M.state.config if config and config.layout and config.layout.prompt_position then - local pos = config.layout.prompt_position - - if pos == 'top' or pos == 'bottom' then return pos end + local terminal_width = vim.o.columns + local terminal_height = vim.o.lines + + return utils.resolve_config_value( + config.layout.prompt_position, + terminal_width, + terminal_height, + function(value) return utils.is_valid_position(value, { 'top', 'bottom' }) end, + 'bottom', + 'layout.prompt_position' + ) end return 'bottom' end +local function get_preview_position() + local config = M.state.config + + if config and config.layout and config.layout.preview_position then + local terminal_width = vim.o.columns + local terminal_height = vim.o.lines + + return utils.resolve_config_value( + config.layout.preview_position, + terminal_width, + terminal_height, + function(value) return utils.is_valid_position(value, { 'left', 'right', 'top', 'bottom' }) end, + 'right', + 'layout.preview_position' + ) + end + + return 'right' +end + +--- Function-based config options: +--- config.layout.width: number|function(terminal_width, terminal_height): number +--- config.layout.height: number|function(terminal_width, terminal_height): number +--- config.layout.preview_size: number|function(terminal_width, terminal_height): number +--- config.layout.preview_position: string|function(terminal_width, terminal_height): string +--- config.layout.prompt_position: string|function(terminal_width, terminal_height): string + +--- @class LayoutConfig +--- @field total_width number +--- @field total_height number +--- @field start_col number +--- @field start_row number +--- @field preview_position string|function Preview position ('left'|'right'|'top'|'bottom') or function(terminal_width, terminal_height): string +--- @field prompt_position string +--- @field debug_enabled boolean +--- @field preview_width number +--- @field preview_height number +--- @field separator_width number +--- @field file_info_height number + +--- Calculate layout dimensions and positions for all windows +--- @param cfg LayoutConfig +--- @return table Layout configuration +function M.calculate_layout_dimensions(cfg) + local BORDER_SIZE = 2 + local PROMPT_HEIGHT = 3 + local SEPARATOR_WIDTH = 1 + local SEPARATOR_HEIGHT = 1 + + local layout = {} + local preview_enabled = M.enabled_preview() + + -- Section 1: Base dimensions and bounds checking + local total_width = math.max(0, cfg.total_width - BORDER_SIZE) + local total_height = math.max(0, cfg.total_height - BORDER_SIZE - PROMPT_HEIGHT) + + -- Section 2: Calculate dimensions based on preview position + if cfg.preview_position == 'left' then + -- Left preview layout + local separator_width = preview_enabled and SEPARATOR_WIDTH or 0 + local list_width = math.max(0, total_width - cfg.preview_width - separator_width) + local list_height = total_height + + -- Position main windows + layout.list_col = cfg.start_col + cfg.preview_width + separator_width + layout.list_width = list_width + layout.list_height = list_height + layout.input_col = layout.list_col + layout.input_width = list_width + + -- Position preview window + if preview_enabled then + layout.preview = { + col = cfg.start_col + 1, + width = cfg.preview_width, + row = cfg.start_row + 1, + height = list_height, + } + end + elseif cfg.preview_position == 'right' then + -- Right preview layout + local separator_width = preview_enabled and SEPARATOR_WIDTH or 0 + local list_width = math.max(0, total_width - cfg.preview_width - separator_width) + local list_height = total_height + + -- Position main windows + layout.list_col = cfg.start_col + 1 + layout.list_width = list_width + layout.list_height = list_height + layout.input_col = layout.list_col + layout.input_width = list_width + + -- Position preview window + if preview_enabled then + layout.preview = { + col = cfg.start_col + list_width + separator_width, + width = cfg.preview_width, + row = cfg.start_row + 1, + height = list_height, + } + end + elseif cfg.preview_position == 'top' then + -- Top preview layout + local separator_height = preview_enabled and SEPARATOR_HEIGHT or 0 + local list_height = math.max(0, total_height - cfg.preview_height - separator_height) + + -- Position main windows + layout.list_col = cfg.start_col + 1 + layout.list_width = total_width + layout.list_height = list_height + layout.input_col = layout.list_col + layout.input_width = total_width + + -- Position preview window + if preview_enabled then + layout.preview = { + col = cfg.start_col + 1, + row = cfg.start_row + 1, + width = total_width, + height = cfg.preview_height, + } + end + + -- Adjust list position for top preview + local list_start_row = cfg.start_row + (preview_enabled and (cfg.preview_height + separator_height) or 1) + layout.list_start_row = list_start_row + else -- cfg.preview_position == 'bottom' + -- Bottom preview layout + local separator_height = preview_enabled and SEPARATOR_HEIGHT or 0 + local list_height = math.max(0, total_height - cfg.preview_height - separator_height) + + -- Position main windows + layout.list_col = cfg.start_col + 1 + layout.list_width = total_width + layout.list_height = list_height + layout.input_col = layout.list_col + layout.input_width = total_width + + -- Position preview window (will be positioned after list in Section 3) + if preview_enabled then + layout.preview = { + col = cfg.start_col + 1, + width = total_width, + height = cfg.preview_height, + } + end + + -- Set list start position + layout.list_start_row = cfg.start_row + 1 + end + + -- Section 3: Position prompt and adjust row positions + if cfg.preview_position == 'left' or cfg.preview_position == 'right' then + -- Vertical splits: prompt above or below list + if cfg.prompt_position == 'top' then + layout.input_row = cfg.start_row + 1 + layout.list_row = cfg.start_row + PROMPT_HEIGHT + else + layout.list_row = cfg.start_row + 1 + layout.input_row = cfg.start_row + cfg.total_height - BORDER_SIZE + end + else + -- Horizontal splits: prompt positioned relative to list area + local list_start_row = layout.list_start_row + if cfg.prompt_position == 'top' then + layout.input_row = list_start_row + layout.list_row = list_start_row + BORDER_SIZE + layout.list_height = math.max(0, layout.list_height - BORDER_SIZE) + else + layout.list_row = list_start_row + layout.input_row = list_start_row + layout.list_height + 1 + end + + -- Position bottom preview after list + if cfg.preview_position == 'bottom' and layout.preview then + layout.preview.row = layout.list_row + layout.list_height + PROMPT_HEIGHT + end + end + + -- Section 4: Position debug panel (if enabled) + if cfg.debug_enabled and preview_enabled and layout.preview then + if cfg.preview_position == 'left' or cfg.preview_position == 'right' then + -- Debug panel above preview in vertical splits + layout.file_info = { + width = cfg.preview_width, + height = cfg.file_info_height, + col = layout.preview.col, + row = cfg.start_row + 1, + } + layout.preview.row = cfg.start_row + cfg.file_info_height + PROMPT_HEIGHT + layout.preview.height = math.max(0, layout.list_height - cfg.file_info_height) + else + -- Debug panel above preview in horizontal splits + layout.file_info = { + width = layout.preview.width, + height = cfg.file_info_height, + col = layout.preview.col, + row = layout.preview.row, + } + layout.preview.row = layout.preview.row + cfg.file_info_height + BORDER_SIZE + layout.preview.height = math.max(0, layout.preview.height - cfg.file_info_height - BORDER_SIZE) + end + end + + return layout +end + -- Initialize preview with main config if main.config and main.config.preview then preview.setup(main.config.preview) end @@ -66,32 +282,59 @@ function M.create_ui() and main.config.debug and main.config.debug.show_scores - local width = math.floor(vim.o.columns * config.layout.width) - local height = math.floor(vim.o.lines * config.layout.height) + local terminal_width = vim.o.columns + local terminal_height = vim.o.lines + + -- Calculate width and height (support function or number) + local width_ratio = utils.resolve_config_value( + config.layout.width, + terminal_width, + terminal_height, + utils.is_valid_ratio, + 0.8, + 'layout.width' + ) + local height_ratio = utils.resolve_config_value( + config.layout.height, + terminal_width, + terminal_height, + utils.is_valid_ratio, + 0.8, + 'layout.height' + ) + + local width = math.floor(terminal_width * width_ratio) + local height = math.floor(terminal_height * height_ratio) local col = math.floor((vim.o.columns - width) / 2) local row = math.floor((vim.o.lines - height) / 2) - local preview_width = M.enabled_preview() and math.floor(width * config.layout.preview_width) or 0 - local list_width = width - preview_width - 3 -- Account for separators - local list_height = height - 4 -- Same as list window height - local prompt_position = get_prompt_position() - local list_row, input_row + local preview_position = get_preview_position() + + local preview_size_ratio = utils.resolve_config_value( + config.layout.preview_size, + terminal_width, + terminal_height, + utils.is_valid_ratio, + 0.4, + 'layout.preview_size' + ) - if prompt_position == 'top' then - input_row = row + 1 - list_row = row + 3 - else - list_row = row + 1 - input_row = row + height - 2 - end + local layout_config = { + total_width = width, + total_height = height, + start_col = col, + start_row = row, + preview_position = preview_position, + prompt_position = prompt_position, + debug_enabled = debug_enabled_in_preview, + preview_width = M.enabled_preview() and math.floor(width * preview_size_ratio) or 0, + preview_height = M.enabled_preview() and math.floor(height * preview_size_ratio) or 0, + separator_width = 3, + file_info_height = debug_enabled_in_preview and 10 or 0, + } - local file_info_height = 0 - local preview_height = list_height - if debug_enabled_in_preview then - file_info_height = 10 -- Fixed height of 10 lines for file info - preview_height = list_height - file_info_height -- No subtraction needed - borders are handled by window positioning - end + local layout = M.calculate_layout_dimensions(layout_config) local buf_opts = { false, true } -- nofile, scratch buffer M.state.input_buf = vim.api.nvim_create_buf(buf_opts[1], buf_opts[2]) @@ -104,25 +347,27 @@ function M.create_ui() M.state.file_info_buf = nil end + -- Create list window M.state.list_win = vim.api.nvim_open_win(M.state.list_buf, false, { relative = 'editor', - width = list_width, - height = list_height, -- Use calculated list height - col = col + 1, - row = list_row, + width = layout.list_width, + height = layout.list_height, + col = layout.list_col, + row = layout.list_row, border = 'single', style = 'minimal', title = ' Files ', title_pos = 'left', }) - if debug_enabled_in_preview then + -- Create file info window if debug enabled + if debug_enabled_in_preview and layout.file_info then M.state.file_info_win = vim.api.nvim_open_win(M.state.file_info_buf, false, { relative = 'editor', - width = preview_width, - height = file_info_height, - col = col + list_width + 3, - row = row + 1, + width = layout.file_info.width, + height = layout.file_info.height, + col = layout.file_info.col, + row = layout.file_info.row, border = 'single', style = 'minimal', title = ' File Info ', @@ -132,29 +377,28 @@ function M.create_ui() M.state.file_info_win = nil end - local preview_row = debug_enabled_in_preview and (row + file_info_height + 3) or (row + 1) - local preview_height_adj = debug_enabled_in_preview and preview_height or (list_height + 2) - - if M.enabled_preview() then + -- Create preview window + if M.enabled_preview() and layout.preview then M.state.preview_win = vim.api.nvim_open_win(M.state.preview_buf, false, { relative = 'editor', - width = preview_width, - height = preview_height_adj, - col = col + list_width + 3, - row = preview_row, + width = layout.preview.width, + height = layout.preview.height, + col = layout.preview.col, + row = layout.preview.row, border = 'single', style = 'minimal', - title = ' PREVIEW TEST TITLE ', + title = ' Preview ', title_pos = 'left', }) end + -- Create input window M.state.input_win = vim.api.nvim_open_win(M.state.input_buf, false, { relative = 'editor', - width = list_width, + width = layout.input_width, height = 1, - col = col + 1, - row = input_row, -- Use calculated row position based on prompt_position + col = layout.input_col, + row = layout.input_row, border = 'single', style = 'minimal', }) diff --git a/lua/fff/utils.lua b/lua/fff/utils.lua index a6f6b69a..9abfdb36 100644 --- a/lua/fff/utils.lua +++ b/lua/fff/utils.lua @@ -17,4 +17,48 @@ function M.format_file_size(size) end end +--- Safely resolve a config value that can be either a static value or a function +--- @param config_value any The config value (can be function or static value) +--- @param terminal_width number Terminal width for function calls +--- @param terminal_height number Terminal height for function calls +--- @param validator function Function to validate the result +--- @param fallback any Fallback value if function fails or returns invalid value +--- @param error_context string Context for error messages +--- @return any The resolved and validated value +function M.resolve_config_value(config_value, terminal_width, terminal_height, validator, fallback, error_context) + if type(config_value) == 'function' then + local success, result = pcall(config_value, terminal_width, terminal_height) + + if success and validator(result) then + return result + else + if not success then + vim.notify('FFF: Error in ' .. error_context .. ' function: ' .. tostring(result), vim.log.levels.WARN) + end + return fallback + end + else + return config_value + end +end + +--- Validate numeric ratio (0 < value <= 1) +--- @param value any Value to validate +--- @return boolean True if valid numeric ratio +function M.is_valid_ratio(value) + return type(value) == 'number' and value > 0 and value <= 1 +end + +--- Validate position string +--- @param value any Value to validate +--- @param valid_positions table List of valid position strings +--- @return boolean True if valid position +function M.is_valid_position(value, valid_positions) + if type(value) ~= 'string' then return false end + for _, pos in ipairs(valid_positions) do + if value == pos then return true end + end + return false +end + return M From 37f42b9cd1ae4800b4fbe39e05d0e873dd1ab403 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 11:15:09 +0200 Subject: [PATCH 05/19] feat: add flexible configuration deprecation system Implement structured deprecation rules for configuration migration, enabling smooth transition from legacy config options while maintaining backward compatibility and clear user warnings. --- lua/fff/file_picker/init.lua | 2 +- lua/fff/main.lua | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lua/fff/file_picker/init.lua b/lua/fff/file_picker/init.lua index fc01f337..ea74a374 100644 --- a/lua/fff/file_picker/init.lua +++ b/lua/fff/file_picker/init.lua @@ -43,7 +43,7 @@ function M.setup(config) layout = { prompt_position = 'bottom', preview_position = 'right', - preview_width = 0.4, + preview_size = 0.4, height = 0.8, width = 0.8, }, diff --git a/lua/fff/main.lua b/lua/fff/main.lua index 83d05806..789ecb2c 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -20,10 +20,16 @@ local DEPRECATION_RULES = { message = 'config.height is deprecated. Use config.layout.height instead.', }, { - -- preview.width -> layout.preview_width + -- preview.width -> layout.preview_size old_path = { 'preview', 'width' }, - new_path = { 'layout', 'preview_width' }, - message = 'config.preview.width is deprecated. Use config.layout.preview_width instead.', + new_path = { 'layout', 'preview_size' }, + message = 'config.preview.width is deprecated. Use config.layout.preview_size instead.', + }, + { + -- layout.preview_width -> layout.preview_size + old_path = { 'layout', 'preview_width' }, + new_path = { 'layout', 'preview_size' }, + message = 'config.layout.preview_width is deprecated. Use config.layout.preview_size instead.', }, } @@ -144,8 +150,8 @@ function M.setup(config) height = 0.8, width = 0.8, prompt_position = 'bottom', - preview_position = 'right', - preview_width = 0.4, + preview_position = 'right', -- 'left', 'right', 'top', 'bottom' + preview_size = 0.4, }, frecency = { enabled = true, From 875987a5427def567ba070912a923157b128eaf6 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 11:15:22 +0200 Subject: [PATCH 06/19] docs: update documentation for comprehensive layout positioning Document all supported preview positions (left, right, top, bottom) and preview_size parameter, removing outdated limitation claims and ensuring configuration examples match current capabilities. --- README.md | 4 ++-- doc/fff.nvim.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fde74faf..6d613b9f 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ require('fff').setup({ height = 0.8, -- Window height as fraction of screen width = 0.8, -- Window width as fraction of screen prompt_position = 'bottom', -- 'top' or 'bottom' - preview_position = 'right', -- Currently only 'right' is supported - preview_width = 0.4, -- Preview width as fraction of window + preview_position = 'right', -- 'left', 'right', 'top', 'bottom' + preview_size = 0.4, -- Preview size as fraction of window (width for left/right, height for top/bottom) }, keymaps = { diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index 633a56ef..93f5fa4c 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -94,8 +94,8 @@ configuration: height = 0.8, -- Window height as fraction of screen width = 0.8, -- Window width as fraction of screen prompt_position = 'bottom', -- 'top' or 'bottom' - preview_position = 'right', -- Currently only 'right' is supported - preview_width = 0.4, -- Preview width as fraction of window + preview_position = 'right', -- 'left', 'right', 'top', 'bottom' + preview_size = 0.4, -- Preview size as fraction of window (width for left/right, height for top/bottom) }, keymaps = { From 659176a04c7fb66af29ad18fb778adcaef83ac10 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 11:18:21 +0200 Subject: [PATCH 07/19] docs: add comprehensive documentation for all configuration options Document complete configuration API including dynamic functions, all preview settings, logging, frecency, UI options, and automatic migration system. Ensures users understand both static and responsive configuration patterns. --- README.md | 112 ++++++++++++++++++++++++++++++++++++++++++---- doc/fff.nvim.txt | 113 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 208 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 6d613b9f..7b30d26e 100644 --- a/README.md +++ b/README.md @@ -72,25 +72,39 @@ FFF.nvim comes with sensible defaults. Here's the complete default configuration ```lua require('fff').setup({ - prompt = 'πŸͺΏ ', - title = 'FFF Files', + prompt = 'πŸͺΏ ', -- Input prompt symbol + title = 'FFF Files', -- Window title max_results = 100, -- Maximum search results to display max_threads = 4, -- Maximum threads for fuzzy search + -- Preview configuration preview = { enabled = true, - max_lines = 100, - max_size = 1024 * 1024, -- 1MB + max_lines = 5000, -- Maximum lines to show in preview + max_size = 10 * 1024 * 1024, -- 10MB max file size for preview + imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', + line_numbers = false, -- Show line numbers in preview + wrap_lines = false, -- Wrap long lines + show_file_info = true, -- Show file information + binary_file_threshold = 1024, -- Files larger than this are treated as binary + filetypes = { + svg = { wrap_lines = true }, + markdown = { wrap_lines = true }, + text = { wrap_lines = true }, + log = { tail_lines = 100 }, -- Show last 100 lines for logs + }, }, + -- Layout configuration - supports functions for dynamic sizing layout = { - height = 0.8, -- Window height as fraction of screen - width = 0.8, -- Window width as fraction of screen - prompt_position = 'bottom', -- 'top' or 'bottom' - preview_position = 'right', -- 'left', 'right', 'top', 'bottom' - preview_size = 0.4, -- Preview size as fraction of window (width for left/right, height for top/bottom) + height = 0.8, -- Window height as fraction of screen (or function) + width = 0.8, -- Window width as fraction of screen (or function) + prompt_position = 'bottom', -- 'top' or 'bottom' (or function) + preview_position = 'right', -- 'left', 'right', 'top', 'bottom' (or function) + preview_size = 0.4, -- Preview size as fraction (width for left/right, height for top/bottom, or function) }, + -- Key mappings keymaps = { close = '', select = '', @@ -117,13 +131,93 @@ require('fff').setup({ debug = 'Comment', }, + -- Frecency scoring + frecency = { + enabled = true, + db_path = vim.fn.stdpath('cache') .. '/fff_nvim', + }, + -- Debug options debug = { + enabled = false, show_scores = false, -- Toggle with F2 or :FFFDebug }, + + -- Logging configuration + logging = { + enabled = true, + log_file = vim.fn.stdpath('log') .. '/fff.log', + log_level = 'info', + }, + + -- UI configuration + ui = { + wrap_paths = true, + wrap_indent = 2, + max_path_width = 80, + }, + + -- Image preview settings + image_preview = { + enabled = true, + max_width = 80, + max_height = 24, + }, + + -- Icon display + icons = { + enabled = true, + }, }) ``` +### Dynamic Configuration + +Layout options support dynamic functions for responsive sizing: + +```lua +require('fff').setup({ + layout = { + -- Static values + height = 0.8, + width = 0.8, + + -- Or dynamic functions that receive terminal_width, terminal_height + height = function(terminal_width, terminal_height) + return terminal_height > 50 and 0.9 or 0.7 + end, + + width = function(terminal_width, terminal_height) + return terminal_width > 120 and 0.8 or 0.95 + end, + + preview_size = function(terminal_width, terminal_height) + return terminal_width > 100 and 0.4 or 0.3 + end, + + -- Position can also be dynamic + preview_position = function(terminal_width, terminal_height) + return terminal_width > 100 and 'right' or 'bottom' + end, + + prompt_position = function(terminal_width, terminal_height) + return terminal_height > 30 and 'bottom' or 'top' + end, + } +}) +``` + +### Configuration Migration + +The plugin automatically handles deprecated configuration options with clear warnings: + +- `config.width` β†’ `config.layout.width` +- `config.height` β†’ `config.layout.height` +- `config.preview.width` β†’ `config.layout.preview_size` +- `config.layout.preview_width` β†’ `config.layout.preview_size` + +Legacy configurations are automatically migrated on startup. + ### Key Features #### Available methods diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index 93f5fa4c..61d120a5 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -83,21 +83,35 @@ configuration: title = 'FFF Files', -- Window title max_results = 100, -- Maximum search results to display max_threads = 4, -- Maximum threads for fuzzy search - + + -- Preview configuration preview = { enabled = true, - max_lines = 100, - max_size = 1024 * 1024, -- 1MB + max_lines = 5000, -- Maximum lines to show in preview + max_size = 10 * 1024 * 1024, -- 10MB max file size for preview + imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', + line_numbers = false, -- Show line numbers in preview + wrap_lines = false, -- Wrap long lines + show_file_info = true, -- Show file information + binary_file_threshold = 1024, -- Files larger than this are treated as binary + filetypes = { + svg = { wrap_lines = true }, + markdown = { wrap_lines = true }, + text = { wrap_lines = true }, + log = { tail_lines = 100 }, -- Show last 100 lines for logs + }, }, + -- Layout configuration - supports functions for dynamic sizing layout = { - height = 0.8, -- Window height as fraction of screen - width = 0.8, -- Window width as fraction of screen - prompt_position = 'bottom', -- 'top' or 'bottom' - preview_position = 'right', -- 'left', 'right', 'top', 'bottom' - preview_size = 0.4, -- Preview size as fraction of window (width for left/right, height for top/bottom) + height = 0.8, -- Window height as fraction of screen (or function) + width = 0.8, -- Window width as fraction of screen (or function) + prompt_position = 'bottom', -- 'top' or 'bottom' (or function) + preview_position = 'right', -- 'left', 'right', 'top', 'bottom' (or function) + preview_size = 0.4, -- Preview size as fraction (width for left/right, height for top/bottom, or function) }, + -- Key mappings keymaps = { close = '', select = '', @@ -124,14 +138,97 @@ configuration: debug = 'Comment', }, + -- Frecency scoring + frecency = { + enabled = true, + db_path = vim.fn.stdpath('cache') .. '/fff_nvim', + }, + -- Debug options debug = { + enabled = false, show_scores = false, -- Toggle with F2 or :FFFDebug }, + + -- Logging configuration + logging = { + enabled = true, + log_file = vim.fn.stdpath('log') .. '/fff.log', + log_level = 'info', + }, + + -- UI configuration + ui = { + wrap_paths = true, + wrap_indent = 2, + max_path_width = 80, + }, + + -- Image preview settings + image_preview = { + enabled = true, + max_width = 80, + max_height = 24, + }, + + -- Icon display + icons = { + enabled = true, + }, }) < +DYNAMIC CONFIGURATION *fff.nvim-dynamic-config* + +Layout options support dynamic functions for responsive sizing: + +>lua + require('fff').setup({ + layout = { + -- Static values + height = 0.8, + width = 0.8, + + -- Or dynamic functions that receive terminal_width, terminal_height + height = function(terminal_width, terminal_height) + return terminal_height > 50 and 0.9 or 0.7 + end, + + width = function(terminal_width, terminal_height) + return terminal_width > 120 and 0.8 or 0.95 + end, + + preview_size = function(terminal_width, terminal_height) + return terminal_width > 100 and 0.4 or 0.3 + end, + + -- Position can also be dynamic + preview_position = function(terminal_width, terminal_height) + return terminal_width > 100 and 'right' or 'bottom' + end, + + prompt_position = function(terminal_width, terminal_height) + return terminal_height > 30 and 'bottom' or 'top' + end, + } + }) +< + + +CONFIGURATION MIGRATION *fff.nvim-config-migration* + +The plugin automatically handles deprecated configuration options with clear +warnings: + +β€’ config.width β†’ config.layout.width +β€’ config.height β†’ config.layout.height +β€’ config.preview.width β†’ config.layout.preview_size +β€’ config.layout.preview_width β†’ config.layout.preview_size + +Legacy configurations are automatically migrated on startup. + + KEY FEATURES ~ From 38666c23044336eea492f28e3b199254b36ec336 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 11:22:52 +0200 Subject: [PATCH 08/19] style: align comments in documentation for improved readability Standardize comment alignment across configuration examples in README.md and doc/fff.nvim.txt to create consistent visual structure and make configuration options easier to scan and understand. --- README.md | 81 +++++++++--------------------------------- doc/fff.nvim.txt | 91 +++++++++++------------------------------------- 2 files changed, 37 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index 7b30d26e..82c4cbfb 100644 --- a/README.md +++ b/README.md @@ -72,36 +72,36 @@ FFF.nvim comes with sensible defaults. Here's the complete default configuration ```lua require('fff').setup({ - prompt = 'πŸͺΏ ', -- Input prompt symbol - title = 'FFF Files', -- Window title - max_results = 100, -- Maximum search results to display - max_threads = 4, -- Maximum threads for fuzzy search + prompt = 'πŸͺΏ ', -- Input prompt symbol + title = 'FFF Files', -- Window title + max_results = 100, -- Maximum search results to display + max_threads = 4, -- Maximum threads for fuzzy search -- Preview configuration preview = { enabled = true, - max_lines = 5000, -- Maximum lines to show in preview - max_size = 10 * 1024 * 1024, -- 10MB max file size for preview + max_lines = 5000, -- Maximum lines to show in preview + max_size = 10 * 1024 * 1024, -- 10MB max file size for preview imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', - line_numbers = false, -- Show line numbers in preview - wrap_lines = false, -- Wrap long lines - show_file_info = true, -- Show file information - binary_file_threshold = 1024, -- Files larger than this are treated as binary + line_numbers = false, -- Show line numbers in preview + wrap_lines = false, -- Wrap long lines + show_file_info = true, -- Show file information + binary_file_threshold = 1024, -- Files larger than this are treated as binary filetypes = { svg = { wrap_lines = true }, markdown = { wrap_lines = true }, text = { wrap_lines = true }, - log = { tail_lines = 100 }, -- Show last 100 lines for logs + log = { tail_lines = 100 }, -- Show last 100 lines for logs }, }, -- Layout configuration - supports functions for dynamic sizing layout = { - height = 0.8, -- Window height as fraction of screen (or function) - width = 0.8, -- Window width as fraction of screen (or function) - prompt_position = 'bottom', -- 'top' or 'bottom' (or function) - preview_position = 'right', -- 'left', 'right', 'top', 'bottom' (or function) - preview_size = 0.4, -- Preview size as fraction (width for left/right, height for top/bottom, or function) + height = 0.8, -- Window height as fraction of screen (or function) + width = 0.8, -- Window width as fraction of screen (or function) + prompt_position = 'bottom', -- 'top' or 'bottom' (or function) + preview_position = 'right', -- 'left', 'right', 'top', 'bottom' (or function) + preview_size = 0.4, -- Preview size as fraction (or function) }, -- Key mappings @@ -140,7 +140,7 @@ require('fff').setup({ -- Debug options debug = { enabled = false, - show_scores = false, -- Toggle with F2 or :FFFDebug + show_scores = false, -- Toggle with F2 or :FFFDebug }, -- Logging configuration @@ -171,53 +171,6 @@ require('fff').setup({ }) ``` -### Dynamic Configuration - -Layout options support dynamic functions for responsive sizing: - -```lua -require('fff').setup({ - layout = { - -- Static values - height = 0.8, - width = 0.8, - - -- Or dynamic functions that receive terminal_width, terminal_height - height = function(terminal_width, terminal_height) - return terminal_height > 50 and 0.9 or 0.7 - end, - - width = function(terminal_width, terminal_height) - return terminal_width > 120 and 0.8 or 0.95 - end, - - preview_size = function(terminal_width, terminal_height) - return terminal_width > 100 and 0.4 or 0.3 - end, - - -- Position can also be dynamic - preview_position = function(terminal_width, terminal_height) - return terminal_width > 100 and 'right' or 'bottom' - end, - - prompt_position = function(terminal_width, terminal_height) - return terminal_height > 30 and 'bottom' or 'top' - end, - } -}) -``` - -### Configuration Migration - -The plugin automatically handles deprecated configuration options with clear warnings: - -- `config.width` β†’ `config.layout.width` -- `config.height` β†’ `config.layout.height` -- `config.preview.width` β†’ `config.layout.preview_size` -- `config.layout.preview_width` β†’ `config.layout.preview_size` - -Legacy configurations are automatically migrated on startup. - ### Key Features #### Available methods diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index 61d120a5..ff934248 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -79,36 +79,36 @@ configuration: >lua require('fff').setup({ - prompt = 'πŸͺΏ ', -- Input prompt symbol - title = 'FFF Files', -- Window title - max_results = 100, -- Maximum search results to display - max_threads = 4, -- Maximum threads for fuzzy search - + prompt = 'πŸͺΏ ', -- Input prompt symbol + title = 'FFF Files', -- Window title + max_results = 100, -- Maximum search results to display + max_threads = 4, -- Maximum threads for fuzzy search + -- Preview configuration preview = { enabled = true, - max_lines = 5000, -- Maximum lines to show in preview - max_size = 10 * 1024 * 1024, -- 10MB max file size for preview + max_lines = 5000, -- Maximum lines to show in preview + max_size = 10 * 1024 * 1024, -- 10MB max file size for preview imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', - line_numbers = false, -- Show line numbers in preview - wrap_lines = false, -- Wrap long lines - show_file_info = true, -- Show file information - binary_file_threshold = 1024, -- Files larger than this are treated as binary + line_numbers = false, -- Show line numbers in preview + wrap_lines = false, -- Wrap long lines + show_file_info = true, -- Show file information + binary_file_threshold = 1024, -- Files larger than this are treated as binary filetypes = { svg = { wrap_lines = true }, markdown = { wrap_lines = true }, text = { wrap_lines = true }, - log = { tail_lines = 100 }, -- Show last 100 lines for logs + log = { tail_lines = 100 }, -- Show last 100 lines for logs }, }, -- Layout configuration - supports functions for dynamic sizing layout = { - height = 0.8, -- Window height as fraction of screen (or function) - width = 0.8, -- Window width as fraction of screen (or function) - prompt_position = 'bottom', -- 'top' or 'bottom' (or function) - preview_position = 'right', -- 'left', 'right', 'top', 'bottom' (or function) - preview_size = 0.4, -- Preview size as fraction (width for left/right, height for top/bottom, or function) + height = 0.8, -- Window height as fraction of screen (or function) + width = 0.8, -- Window width as fraction of screen (or function) + prompt_position = 'bottom', -- 'top' or 'bottom' (or function) + preview_position = 'right', -- 'left', 'right', 'top', 'bottom' (or function) + preview_size = 0.4, -- Preview size as fraction (or function) }, -- Key mappings @@ -147,7 +147,7 @@ configuration: -- Debug options debug = { enabled = false, - show_scores = false, -- Toggle with F2 or :FFFDebug + show_scores = false, -- Toggle with F2 or :FFFDebug }, -- Logging configuration @@ -176,57 +176,6 @@ configuration: enabled = true, }, }) -< - - -DYNAMIC CONFIGURATION *fff.nvim-dynamic-config* - -Layout options support dynamic functions for responsive sizing: - ->lua - require('fff').setup({ - layout = { - -- Static values - height = 0.8, - width = 0.8, - - -- Or dynamic functions that receive terminal_width, terminal_height - height = function(terminal_width, terminal_height) - return terminal_height > 50 and 0.9 or 0.7 - end, - - width = function(terminal_width, terminal_height) - return terminal_width > 120 and 0.8 or 0.95 - end, - - preview_size = function(terminal_width, terminal_height) - return terminal_width > 100 and 0.4 or 0.3 - end, - - -- Position can also be dynamic - preview_position = function(terminal_width, terminal_height) - return terminal_width > 100 and 'right' or 'bottom' - end, - - prompt_position = function(terminal_width, terminal_height) - return terminal_height > 30 and 'bottom' or 'top' - end, - } - }) -< - - -CONFIGURATION MIGRATION *fff.nvim-config-migration* - -The plugin automatically handles deprecated configuration options with clear -warnings: - -β€’ config.width β†’ config.layout.width -β€’ config.height β†’ config.layout.height -β€’ config.preview.width β†’ config.layout.preview_size -β€’ config.layout.preview_width β†’ config.layout.preview_size - -Legacy configurations are automatically migrated on startup. KEY FEATURES ~ @@ -273,9 +222,9 @@ Toggle scoring information display: - Enable by default with `debug.show_scores = true` > - + #### vim-plug - + ```vim Plug 'MunifTanjim/nui.nvim' Plug 'dmtrKovalenko/fff.nvim', { 'do': 'cargo build --release' } From 67a14d2dd68c1c6dae6844bf586df4c9620015e3 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 11:26:17 +0200 Subject: [PATCH 09/19] style: comprehensively align all comments in documentation Standardize comment alignment across all code blocks in both README.md and doc/fff.nvim.txt including configuration examples, method calls, and keymap examples for consistent visual structure and readability. --- README.md | 20 ++++++++++---------- doc/fff.nvim.txt | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 82c4cbfb..b7fcf0b2 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ It comes with a dedicated rust backend runtime that keep tracks of the file inde ## Installation -> [!NOTE] +> [!NOTE] > Although we'll try to make sure to keep 100% backward compatibiility, by using you should understand that silly bugs and breaking changes may happen. > And also we hope for your contributions and feedback to make this plugin ideal for everyone. @@ -81,7 +81,7 @@ require('fff').setup({ preview = { enabled = true, max_lines = 5000, -- Maximum lines to show in preview - max_size = 10 * 1024 * 1024, -- 10MB max file size for preview + max_size = 10 * 1024 * 1024, -- 10MB max file size for preview imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', line_numbers = false, -- Show line numbers in preview wrap_lines = false, -- Wrap long lines @@ -176,11 +176,11 @@ require('fff').setup({ #### Available methods ```lua -require('fff').find_files() -- Find files in current directory -require('fff').find_in_git_root() -- Find files in the current git repository -require('fff').scan_files() -- Trigger rescan of files in the current directory -require('fff').refresh_git_status() -- Refresh git status for the active file lock -require('fff').find_files_in_dir(path) -- Find files in a specific directory +require('fff').find_files() -- Find files in current directory +require('fff').find_in_git_root() -- Find files in the current git repository +require('fff').scan_files() -- Trigger rescan of files in the current directory +require('fff').refresh_git_status() -- Refresh git status for the active file lock +require('fff').find_files_in_dir(path) -- Find files in a specific directory require('fff').change_indexing_directory(new_path) -- Change the base directory for the file picker ``` @@ -190,9 +190,9 @@ You can assign multiple key combinations to the same action: ```lua keymaps = { - move_up = { '', '', '' }, -- Three ways to move up - close = { '', '' }, -- Two ways to close - select = '', -- Single binding still works + move_up = { '', '', '' }, -- Three ways to move up + close = { '', '' }, -- Two ways to close + select = '', -- Single binding still works } ``` diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index ff934248..4da5fe08 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -184,11 +184,11 @@ KEY FEATURES ~ AVAILABLE METHODS >lua - require("fff").find_files() -- Find files in current directory - require("fff").find_in_git_root() -- Find files in the current git repository - require("fff").scan_files() -- Trigger rescan of files in the current directory - require("fff").refresh_git_status() -- Refresh git status for the active file lock - require("fff").find_files_in_dir(path) -- Find files in a specific directory + require("fff").find_files() -- Find files in current directory + require("fff").find_in_git_root() -- Find files in the current git repository + require("fff").scan_files() -- Trigger rescan of files in the current directory + require("fff").refresh_git_status() -- Refresh git status for the active file lock + require("fff").find_files_in_dir(path) -- Find files in a specific directory require("fff").change_indexing_directory(new_path) -- Change the base directory for the file picker < From 6536829a016f049f19e2bb4e2179d9863af7bcbc Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 11:28:05 +0200 Subject: [PATCH 10/19] style: align comment formatting in documentation Standardize comment alignment for better readability and consistency across README and generated vimdoc files. --- README.md | 18 +++++++++--------- doc/fff.nvim.txt | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b7fcf0b2..bc259d56 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ require('fff').setup({ -- Debug options debug = { enabled = false, - show_scores = false, -- Toggle with F2 or :FFFDebug + show_scores = false, -- Toggle with F2 or :FFFDebug }, -- Logging configuration @@ -176,11 +176,11 @@ require('fff').setup({ #### Available methods ```lua -require('fff').find_files() -- Find files in current directory -require('fff').find_in_git_root() -- Find files in the current git repository -require('fff').scan_files() -- Trigger rescan of files in the current directory -require('fff').refresh_git_status() -- Refresh git status for the active file lock -require('fff').find_files_in_dir(path) -- Find files in a specific directory +require('fff').find_files() -- Find files in current directory +require('fff').find_in_git_root() -- Find files in the current git repository +require('fff').scan_files() -- Trigger rescan of files in the current directory +require('fff').refresh_git_status() -- Refresh git status for the active file lock +require('fff').find_files_in_dir(path) -- Find files in a specific directory require('fff').change_indexing_directory(new_path) -- Change the base directory for the file picker ``` @@ -190,9 +190,9 @@ You can assign multiple key combinations to the same action: ```lua keymaps = { - move_up = { '', '', '' }, -- Three ways to move up - close = { '', '' }, -- Two ways to close - select = '', -- Single binding still works + move_up = { '', '', '' }, -- Three ways to move up + close = { '', '' }, -- Two ways to close + select = '', -- Single binding still works } ``` diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index 4da5fe08..5cf740b3 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -88,7 +88,7 @@ configuration: preview = { enabled = true, max_lines = 5000, -- Maximum lines to show in preview - max_size = 10 * 1024 * 1024, -- 10MB max file size for preview + max_size = 10 * 1024 * 1024, -- 10MB max file size for preview imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', line_numbers = false, -- Show line numbers in preview wrap_lines = false, -- Wrap long lines @@ -184,11 +184,11 @@ KEY FEATURES ~ AVAILABLE METHODS >lua - require("fff").find_files() -- Find files in current directory - require("fff").find_in_git_root() -- Find files in the current git repository - require("fff").scan_files() -- Trigger rescan of files in the current directory - require("fff").refresh_git_status() -- Refresh git status for the active file lock - require("fff").find_files_in_dir(path) -- Find files in a specific directory + require("fff").find_files() -- Find files in current directory + require("fff").find_in_git_root() -- Find files in the current git repository + require("fff").scan_files() -- Trigger rescan of files in the current directory + require("fff").refresh_git_status() -- Refresh git status for the active file lock + require("fff").find_files_in_dir(path) -- Find files in a specific directory require("fff").change_indexing_directory(new_path) -- Change the base directory for the file picker < @@ -199,9 +199,9 @@ You can assign multiple key combinations to the same action: >lua keymaps = { - move_up = { '', '', '' }, -- Three ways to move up - close = { '', '' }, -- Two ways to close - select = '', -- Single binding still works + move_up = { '', '', '' }, -- Three ways to move up + close = { '', '' }, -- Two ways to close + select = '', -- Single binding still works } < From b014ee769ed1ca44a94b97db1da055c02178a710 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 11:43:35 +0200 Subject: [PATCH 11/19] chore: run stylua --- lua/fff/utils.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lua/fff/utils.lua b/lua/fff/utils.lua index 9abfdb36..f00833d1 100644 --- a/lua/fff/utils.lua +++ b/lua/fff/utils.lua @@ -28,7 +28,7 @@ end function M.resolve_config_value(config_value, terminal_width, terminal_height, validator, fallback, error_context) if type(config_value) == 'function' then local success, result = pcall(config_value, terminal_width, terminal_height) - + if success and validator(result) then return result else @@ -45,9 +45,7 @@ end --- Validate numeric ratio (0 < value <= 1) --- @param value any Value to validate --- @return boolean True if valid numeric ratio -function M.is_valid_ratio(value) - return type(value) == 'number' and value > 0 and value <= 1 -end +function M.is_valid_ratio(value) return type(value) == 'number' and value > 0 and value <= 1 end --- Validate position string --- @param value any Value to validate From 6b22967f243dae93a40f20789d012cf17e9decc2 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 6 Aug 2025 12:25:24 +0200 Subject: [PATCH 12/19] fix(ui): standardize layout coordinate calculations across preview positions Resolves inconsistent window positioning that caused preview windows to be misaligned or hidden in certain layout combinations (e.g., prompt_position='top' with preview_position='bottom'). Standardizes the coordinate calculation logic to ensure proper alignment and separation between windows for all four preview positions while maintaining the original +3 border/separator spacing. --- lua/fff/picker_ui.lua | 66 +++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index 0521e4b4..b5af8501 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -85,62 +85,54 @@ function M.calculate_layout_dimensions(cfg) -- Section 2: Calculate dimensions based on preview position if cfg.preview_position == 'left' then - -- Left preview layout local separator_width = preview_enabled and SEPARATOR_WIDTH or 0 local list_width = math.max(0, total_width - cfg.preview_width - separator_width) local list_height = total_height - -- Position main windows - layout.list_col = cfg.start_col + cfg.preview_width + separator_width + layout.list_col = cfg.start_col + cfg.preview_width + 3 -- +3 for borders and separator layout.list_width = list_width layout.list_height = list_height layout.input_col = layout.list_col layout.input_width = list_width - -- Position preview window if preview_enabled then layout.preview = { col = cfg.start_col + 1, - width = cfg.preview_width, row = cfg.start_row + 1, + width = cfg.preview_width, height = list_height, } end elseif cfg.preview_position == 'right' then - -- Right preview layout local separator_width = preview_enabled and SEPARATOR_WIDTH or 0 local list_width = math.max(0, total_width - cfg.preview_width - separator_width) local list_height = total_height - -- Position main windows layout.list_col = cfg.start_col + 1 layout.list_width = list_width layout.list_height = list_height layout.input_col = layout.list_col layout.input_width = list_width - -- Position preview window if preview_enabled then layout.preview = { - col = cfg.start_col + list_width + separator_width, - width = cfg.preview_width, + col = cfg.start_col + list_width + 3, -- +3 for borders and separator (matches original) row = cfg.start_row + 1, + width = cfg.preview_width, height = list_height, } end elseif cfg.preview_position == 'top' then - -- Top preview layout local separator_height = preview_enabled and SEPARATOR_HEIGHT or 0 local list_height = math.max(0, total_height - cfg.preview_height - separator_height) - -- Position main windows layout.list_col = cfg.start_col + 1 layout.list_width = total_width layout.list_height = list_height layout.input_col = layout.list_col layout.input_width = total_width + layout.list_start_row = cfg.start_row + (preview_enabled and (cfg.preview_height + separator_height) or 0) + 1 - -- Position preview window if preview_enabled then layout.preview = { col = cfg.start_col + 1, @@ -149,23 +141,17 @@ function M.calculate_layout_dimensions(cfg) height = cfg.preview_height, } end - - -- Adjust list position for top preview - local list_start_row = cfg.start_row + (preview_enabled and (cfg.preview_height + separator_height) or 1) - layout.list_start_row = list_start_row - else -- cfg.preview_position == 'bottom' - -- Bottom preview layout + else local separator_height = preview_enabled and SEPARATOR_HEIGHT or 0 local list_height = math.max(0, total_height - cfg.preview_height - separator_height) - -- Position main windows layout.list_col = cfg.start_col + 1 layout.list_width = total_width layout.list_height = list_height layout.input_col = layout.list_col layout.input_width = total_width + layout.list_start_row = cfg.start_row + 1 - -- Position preview window (will be positioned after list in Section 3) if preview_enabled then layout.preview = { col = cfg.start_col + 1, @@ -173,23 +159,28 @@ function M.calculate_layout_dimensions(cfg) height = cfg.preview_height, } end - - -- Set list start position - layout.list_start_row = cfg.start_row + 1 end -- Section 3: Position prompt and adjust row positions if cfg.preview_position == 'left' or cfg.preview_position == 'right' then - -- Vertical splits: prompt above or below list if cfg.prompt_position == 'top' then layout.input_row = cfg.start_row + 1 - layout.list_row = cfg.start_row + PROMPT_HEIGHT + layout.list_row = cfg.start_row + PROMPT_HEIGHT + 1 else layout.list_row = cfg.start_row + 1 layout.input_row = cfg.start_row + cfg.total_height - BORDER_SIZE end + + if layout.preview then + if cfg.prompt_position == 'top' then + layout.preview.row = cfg.start_row + 1 + layout.preview.height = cfg.total_height - BORDER_SIZE + else + layout.preview.row = cfg.start_row + 1 + layout.preview.height = cfg.total_height - BORDER_SIZE + end + end else - -- Horizontal splits: prompt positioned relative to list area local list_start_row = layout.list_start_row if cfg.prompt_position == 'top' then layout.input_row = list_start_row @@ -200,34 +191,35 @@ function M.calculate_layout_dimensions(cfg) layout.input_row = list_start_row + layout.list_height + 1 end - -- Position bottom preview after list if cfg.preview_position == 'bottom' and layout.preview then - layout.preview.row = layout.list_row + layout.list_height + PROMPT_HEIGHT + if cfg.prompt_position == 'top' then + layout.preview.row = layout.list_row + layout.list_height + 1 + else + layout.preview.row = layout.input_row + PROMPT_HEIGHT + end end end -- Section 4: Position debug panel (if enabled) if cfg.debug_enabled and preview_enabled and layout.preview then if cfg.preview_position == 'left' or cfg.preview_position == 'right' then - -- Debug panel above preview in vertical splits layout.file_info = { - width = cfg.preview_width, + width = layout.preview.width, height = cfg.file_info_height, col = layout.preview.col, - row = cfg.start_row + 1, + row = layout.preview.row, } - layout.preview.row = cfg.start_row + cfg.file_info_height + PROMPT_HEIGHT - layout.preview.height = math.max(0, layout.list_height - cfg.file_info_height) + layout.preview.row = layout.preview.row + cfg.file_info_height + SEPARATOR_HEIGHT + layout.preview.height = math.max(0, layout.preview.height - cfg.file_info_height - SEPARATOR_HEIGHT) else - -- Debug panel above preview in horizontal splits layout.file_info = { width = layout.preview.width, height = cfg.file_info_height, col = layout.preview.col, row = layout.preview.row, } - layout.preview.row = layout.preview.row + cfg.file_info_height + BORDER_SIZE - layout.preview.height = math.max(0, layout.preview.height - cfg.file_info_height - BORDER_SIZE) + layout.preview.row = layout.preview.row + cfg.file_info_height + SEPARATOR_HEIGHT + layout.preview.height = math.max(0, layout.preview.height - cfg.file_info_height - SEPARATOR_HEIGHT) end end From 099aa20417c31c8d0e37b0348e8da69a1f623b75 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Mon, 11 Aug 2025 20:34:43 +0200 Subject: [PATCH 13/19] feat: move prompt position handling to Rust layer --- lua/fff/file_picker/init.lua | 7 ++++-- lua/fff/picker_ui.lua | 42 ++++++------------------------------ lua/fff/rust/file_picker.rs | 17 +++++++++++---- lua/fff/rust/lib.rs | 3 ++- src/bin/jemalloc_profile.rs | 1 + src/bin/test_memory_leak.rs | 1 + src/bin/test_watcher.rs | 2 +- 7 files changed, 30 insertions(+), 43 deletions(-) diff --git a/lua/fff/file_picker/init.lua b/lua/fff/file_picker/init.lua index 7c061b23..181b946d 100644 --- a/lua/fff/file_picker/init.lua +++ b/lua/fff/file_picker/init.lua @@ -84,15 +84,18 @@ end --- Search files with fuzzy matching using blink.cmp's advanced algorithm --- @param query string Search query --- @param max_results number Maximum number of results (optional) +--- @param max_threads number Maximum number of threads (optional) --- @param current_file string|nil Path to current file to deprioritize (optional) +--- @param prompt_position string|nil Position of prompt ('top'|'bottom') for result ordering (optional) --- @return table List of matching files -function M.search_files(query, max_results, max_threads, current_file) +function M.search_files(query, max_results, max_threads, current_file, prompt_position) if not M.state.initialized then return {} end max_results = max_results or M.config.max_results max_threads = max_threads or M.config.max_threads - local ok, search_result = pcall(fuzzy.fuzzy_search_files, query, max_results, max_threads, current_file) + local ok, search_result = + pcall(fuzzy.fuzzy_search_files, query, max_results, max_threads, current_file, prompt_position) if not ok then vim.notify('Failed to search files: ' .. tostring(search_result), vim.log.levels.ERROR) return {} diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index dd122e09..d45fa92e 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -639,11 +639,13 @@ function M.update_results_sync() end end + local prompt_position = get_prompt_position() local results = file_picker.search_files( M.state.query, M.state.config.max_results, M.state.config.max_threads, - M.state.current_file_cache + M.state.current_file_cache, + prompt_position ) -- because the actual files could be different even with same count @@ -741,18 +743,7 @@ function M.render_list() table.insert(items_to_show, items[i]) end - local prompt_position = get_prompt_position() - local display_items = {} - - if prompt_position == 'top' then - display_items = items_to_show - else - local items_length = #items_to_show - - for i = 1, items_length do - display_items[i] = items_to_show[items_length - i + 1] - end - end + local display_items = items_to_show local line_data = {} @@ -819,14 +810,7 @@ function M.render_list() vim.api.nvim_buf_set_option(M.state.list_buf, 'modifiable', false) if #items > 0 then - local prompt_position = get_prompt_position() - local cursor_line - - if prompt_position == 'top' then - cursor_line = empty_lines_needed + M.state.cursor - else - cursor_line = empty_lines_needed + (display_count - M.state.cursor + 1) - end + local cursor_line = empty_lines_needed + M.state.cursor if cursor_line > 0 and cursor_line <= win_height then vim.api.nvim_win_set_cursor(M.state.list_win, { cursor_line, 0 }) @@ -1056,13 +1040,7 @@ end function M.move_up() if not M.state.active then return end - local prompt_position = get_prompt_position() - - if prompt_position == 'top' then - if M.state.cursor > 1 then M.state.cursor = M.state.cursor - 1 end - else - if M.state.cursor < #M.state.filtered_items then M.state.cursor = M.state.cursor + 1 end - end + if M.state.cursor > 1 then M.state.cursor = M.state.cursor - 1 end M.render_list() M.update_preview() @@ -1072,13 +1050,7 @@ end function M.move_down() if not M.state.active then return end - local prompt_position = get_prompt_position() - - if prompt_position == 'top' then - if M.state.cursor < #M.state.filtered_items then M.state.cursor = M.state.cursor + 1 end - else - if M.state.cursor > 1 then M.state.cursor = M.state.cursor - 1 end - end + if M.state.cursor < #M.state.filtered_items then M.state.cursor = M.state.cursor + 1 end M.render_list() M.update_preview() diff --git a/lua/fff/rust/file_picker.rs b/lua/fff/rust/file_picker.rs index 90c39382..2f236a45 100644 --- a/lua/fff/rust/file_picker.rs +++ b/lua/fff/rust/file_picker.rs @@ -171,11 +171,12 @@ impl FilePicker { max_results: usize, max_threads: usize, current_file: Option<&'a str>, + prompt_position: Option<&'a str>, ) -> SearchResult<'a> { let max_threads = max_threads.max(1); debug!( - "Fuzzy search: query='{}', max_results={}, max_threads={}, current_file={:?}", - query, max_results, max_threads, current_file + "Fuzzy search: query='{}', max_results={}, max_threads={}, current_file={:?}, prompt_position={:?}", + query, max_results, max_threads, current_file, prompt_position ); let total_files = files.len(); @@ -191,13 +192,21 @@ impl FilePicker { }; let time = std::time::Instant::now(); - let (items, scores) = match_and_score_files(files, &context); + let (mut items, mut scores) = match_and_score_files(files, &context); + + // Reverse results when prompt is at bottom so best matches appear closest to prompt. + if prompt_position == Some("bottom") { + items.reverse(); + scores.reverse(); + } + debug!( - "Fuzzy search completed in {:?}: found {} results for query '{}', top result {:?}", + "Fuzzy search completed in {:?}: found {} results for query '{}', top result {:?}, prompt_position={:?}", time.elapsed(), items.len(), query, items.first(), + prompt_position, ); let total_matched = items.len(); diff --git a/lua/fff/rust/lib.rs b/lua/fff/rust/lib.rs index 993597e7..5c9a58bf 100644 --- a/lua/fff/rust/lib.rs +++ b/lua/fff/rust/lib.rs @@ -95,7 +95,7 @@ pub fn scan_files(_: &Lua, _: ()) -> LuaResult<()> { pub fn fuzzy_search_files( lua: &Lua, - (query, max_results, max_threads, current_file): (String, usize, usize, Option), + (query, max_results, max_threads, current_file, prompt_position): (String, usize, usize, Option, Option), ) -> LuaResult { let Some(ref mut picker) = *FILE_PICKER.write().map_err(|_| Error::AcquireItemLock)? else { return Err(Error::FilePickerMissing)?; @@ -107,6 +107,7 @@ pub fn fuzzy_search_files( max_results, max_threads, current_file.as_deref(), + prompt_position.as_deref(), ); results.into_lua(lua) diff --git a/src/bin/jemalloc_profile.rs b/src/bin/jemalloc_profile.rs index ffd9994c..dea5cfeb 100644 --- a/src/bin/jemalloc_profile.rs +++ b/src/bin/jemalloc_profile.rs @@ -89,6 +89,7 @@ fn test_search_memory_pattern( 50 + (i % 50), // Vary result count 1 + (i % 4), // Vary thread count None, + None, // prompt_position not relevant for test ); (search_result.items.len(), search_result.total_matched) } else { diff --git a/src/bin/test_memory_leak.rs b/src/bin/test_memory_leak.rs index 5456d505..cfbc2ab3 100644 --- a/src/bin/test_memory_leak.rs +++ b/src/bin/test_memory_leak.rs @@ -205,6 +205,7 @@ fn main() -> Result<(), Box> { max_results, max_threads, None, + None, // prompt_position not relevant for test ); let duration = search_start.elapsed(); (search_result.items.len(), duration) diff --git a/src/bin/test_watcher.rs b/src/bin/test_watcher.rs index 967b7bbd..635bbb04 100644 --- a/src/bin/test_watcher.rs +++ b/src/bin/test_watcher.rs @@ -156,7 +156,7 @@ fn main() -> Result<(), Box> { let timestamp = chrono::Local::now().format("%H:%M:%S"); let file_picker = FILE_PICKER.read().unwrap(); let files = file_picker.as_ref().unwrap().get_files(); - let search_results = FilePicker::fuzzy_search(files, "rs", 5, 2, None); + let search_results = FilePicker::fuzzy_search(files, "rs", 5, 2, None, None); println!( "πŸ” [{}] Search test 'rs': {} matches", From 0f9a4193e045de11988d83e195db5b63714a6660 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Mon, 11 Aug 2025 20:36:34 +0200 Subject: [PATCH 14/19] chore: run cargo fmt --- lua/fff/rust/lib.rs | 8 +++++++- src/bin/jemalloc_profile.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/fff/rust/lib.rs b/lua/fff/rust/lib.rs index 5c9a58bf..ff4b8edb 100644 --- a/lua/fff/rust/lib.rs +++ b/lua/fff/rust/lib.rs @@ -95,7 +95,13 @@ pub fn scan_files(_: &Lua, _: ()) -> LuaResult<()> { pub fn fuzzy_search_files( lua: &Lua, - (query, max_results, max_threads, current_file, prompt_position): (String, usize, usize, Option, Option), + (query, max_results, max_threads, current_file, prompt_position): ( + String, + usize, + usize, + Option, + Option, + ), ) -> LuaResult { let Some(ref mut picker) = *FILE_PICKER.write().map_err(|_| Error::AcquireItemLock)? else { return Err(Error::FilePickerMissing)?; diff --git a/src/bin/jemalloc_profile.rs b/src/bin/jemalloc_profile.rs index dea5cfeb..410b6d9c 100644 --- a/src/bin/jemalloc_profile.rs +++ b/src/bin/jemalloc_profile.rs @@ -89,7 +89,7 @@ fn test_search_memory_pattern( 50 + (i % 50), // Vary result count 1 + (i % 4), // Vary thread count None, - None, // prompt_position not relevant for test + None, // prompt_position not relevant for test ); (search_result.items.len(), search_result.total_matched) } else { From 3c3e99fc8ed665eb554833be416ca8e77f987892 Mon Sep 17 00:00:00 2001 From: Oskar Grunning Date: Wed, 13 Aug 2025 21:24:37 +0200 Subject: [PATCH 15/19] fix: correct item selection when prompt position is bottom Fixes cursor positioning calculation to properly highlight the selected item when using bottom prompt layout. The cursor line calculation now accounts for the inverted display order in bottom mode. --- lua/fff/picker_ui.lua | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index d45fa92e..0e589eb1 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -810,7 +810,15 @@ function M.render_list() vim.api.nvim_buf_set_option(M.state.list_buf, 'modifiable', false) if #items > 0 then - local cursor_line = empty_lines_needed + M.state.cursor + local prompt_position = get_prompt_position() + local cursor_line + + if prompt_position == 'bottom' then + cursor_line = empty_lines_needed + display_count - M.state.cursor + 1 + else + cursor_line = empty_lines_needed + M.state.cursor + end + cursor_line = math.max(1, math.min(cursor_line, win_height)) if cursor_line > 0 and cursor_line <= win_height then vim.api.nvim_win_set_cursor(M.state.list_win, { cursor_line, 0 }) @@ -1039,8 +1047,15 @@ end function M.move_up() if not M.state.active then return end + if #M.state.filtered_items == 0 then return end - if M.state.cursor > 1 then M.state.cursor = M.state.cursor - 1 end + local prompt_position = get_prompt_position() + + if prompt_position == 'bottom' then + M.state.cursor = math.min(M.state.cursor + 1, #M.state.filtered_items) + else + M.state.cursor = math.max(M.state.cursor - 1, 1) + end M.render_list() M.update_preview() @@ -1049,8 +1064,15 @@ end function M.move_down() if not M.state.active then return end + if #M.state.filtered_items == 0 then return end + + local prompt_position = get_prompt_position() - if M.state.cursor < #M.state.filtered_items then M.state.cursor = M.state.cursor + 1 end + if prompt_position == 'bottom' then + M.state.cursor = math.max(M.state.cursor - 1, 1) + else + M.state.cursor = math.min(M.state.cursor + 1, #M.state.filtered_items) + end M.render_list() M.update_preview() From ee9890a584738947a4129d65df33ad68e255f6e0 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Thu, 14 Aug 2025 18:16:39 +0200 Subject: [PATCH 16/19] WIP: fix sorting --- lua/fff/file_picker/init.lua | 6 ++-- lua/fff/picker_ui.lua | 4 +-- lua/fff/rust/file_picker.rs | 28 +++++++++---------- lua/fff/rust/lib.rs | 6 ++-- lua/fff/rust/score.rs | 53 ++++++++++++++++++++++-------------- lua/fff/rust/types.rs | 1 + src/bin/jemalloc_profile.rs | 2 +- src/bin/test_memory_leak.rs | 2 +- src/bin/test_watcher.rs | 2 +- 9 files changed, 56 insertions(+), 48 deletions(-) diff --git a/lua/fff/file_picker/init.lua b/lua/fff/file_picker/init.lua index 181b946d..55ac2f8a 100644 --- a/lua/fff/file_picker/init.lua +++ b/lua/fff/file_picker/init.lua @@ -86,16 +86,16 @@ end --- @param max_results number Maximum number of results (optional) --- @param max_threads number Maximum number of threads (optional) --- @param current_file string|nil Path to current file to deprioritize (optional) ---- @param prompt_position string|nil Position of prompt ('top'|'bottom') for result ordering (optional) +--- @param reverse_order boolean Reverse order of results --- @return table List of matching files -function M.search_files(query, max_results, max_threads, current_file, prompt_position) +function M.search_files(query, max_results, max_threads, current_file, reverse_order) if not M.state.initialized then return {} end max_results = max_results or M.config.max_results max_threads = max_threads or M.config.max_threads local ok, search_result = - pcall(fuzzy.fuzzy_search_files, query, max_results, max_threads, current_file, prompt_position) + pcall(fuzzy.fuzzy_search_files, query, max_results, max_threads, current_file, reverse_order) if not ok then vim.notify('Failed to search files: ' .. tostring(search_result), vim.log.levels.ERROR) return {} diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index b5352209..b3cbea99 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -652,7 +652,7 @@ function M.update_results_sync() M.state.config.max_results, M.state.config.max_threads, M.state.current_file_cache, - prompt_position + prompt_position == 'bottom' ) -- because the actual files could be different even with same count @@ -726,14 +726,12 @@ local function format_file_display(item, max_width) return filename, display_path end ---- Render the list function M.render_list() if not M.state.active then return end local items = M.state.filtered_items local lines = {} - local main = require('fff.main') local max_path_width = main.config.ui and main.config.ui.max_path_width or 80 local debug_enabled = main.config and main.config.debug and main.config.debug.show_scores local win_height = vim.api.nvim_win_get_height(M.state.list_win) diff --git a/lua/fff/rust/file_picker.rs b/lua/fff/rust/file_picker.rs index d391fa72..7a9bc306 100644 --- a/lua/fff/rust/file_picker.rs +++ b/lua/fff/rust/file_picker.rs @@ -171,12 +171,15 @@ impl FilePicker { max_results: usize, max_threads: usize, current_file: Option<&'a str>, - prompt_position: Option<&'a str>, + reverse_order: bool, ) -> SearchResult<'a> { let max_threads = max_threads.max(1); debug!( - "Fuzzy search: query='{}', max_results={}, max_threads={}, current_file={:?}, prompt_position={:?}", - query, max_results, max_threads, current_file, prompt_position + ?query, + ?max_results, + ?max_threads, + ?current_file, + "Fuzzy search", ); let total_files = files.len(); @@ -189,24 +192,19 @@ impl FilePicker { max_threads, current_file, max_results, + reverse_order, }; let time = std::time::Instant::now(); - let (mut items, mut scores, total_matched) = match_and_score_files(files, &context); + let (items, scores, total_matched) = match_and_score_files(files, &context); - // Reverse results when prompt is at bottom so best matches appear closest to prompt. - if prompt_position == Some("bottom") { - items.reverse(); - scores.reverse(); - } debug!( - "Fuzzy search completed in {:?}: found {} results for query '{}', top result {:?}, prompt_position={:?}", - time.elapsed(), - total_matched, - query, - items.first(), - prompt_position, + ?query, + completed_in = ?time.elapsed(), + top_position = ?items.first(), + "Fuzzy search completed", ); + SearchResult { items, scores, diff --git a/lua/fff/rust/lib.rs b/lua/fff/rust/lib.rs index ff4b8edb..5e690f89 100644 --- a/lua/fff/rust/lib.rs +++ b/lua/fff/rust/lib.rs @@ -95,12 +95,12 @@ pub fn scan_files(_: &Lua, _: ()) -> LuaResult<()> { pub fn fuzzy_search_files( lua: &Lua, - (query, max_results, max_threads, current_file, prompt_position): ( + (query, max_results, max_threads, current_file, order_reverse): ( String, usize, usize, Option, - Option, + bool, ), ) -> LuaResult { let Some(ref mut picker) = *FILE_PICKER.write().map_err(|_| Error::AcquireItemLock)? else { @@ -113,7 +113,7 @@ pub fn fuzzy_search_files( max_results, max_threads, current_file.as_deref(), - prompt_position.as_deref(), + order_reverse, ); results.into_lua(lua) diff --git a/lua/fff/rust/score.rs b/lua/fff/rust/score.rs index 7a4b24db..d4283157 100644 --- a/lua/fff/rust/score.rs +++ b/lua/fff/rust/score.rs @@ -68,7 +68,7 @@ pub fn match_and_score_files<'a>( }; let mut next_filename_match_index = 0; - let mut results: Vec<_> = path_matches + let results: Vec<_> = path_matches .into_iter() .enumerate() .map(|(index, path_match)| { @@ -148,16 +148,7 @@ pub fn match_and_score_files<'a>( }) .collect(); - results.sort_by(|a, b| { - b.1.total - .cmp(&a.1.total) - .then_with(|| b.0.modified.cmp(&a.0.modified)) - }); - - let total_matched = results.len(); - results.truncate(context.max_results); - let (items, scores) = results.into_iter().unzip(); - (items, scores, total_matched) + sort_and_truncate(results, context) } /// Check if a filename is a special entry point file that deserves bonus scoring @@ -189,7 +180,7 @@ fn score_all_by_frecency<'a>( files: &'a [FileItem], context: &ScoringContext, ) -> (Vec<&'a FileItem>, Vec, usize) { - let mut results: Vec<_> = files + let results: Vec<_> = files .par_iter() .map(|file| { let total_frecency_score = file.access_frecency_score as i32 @@ -216,15 +207,7 @@ fn score_all_by_frecency<'a>( }) .collect(); - results.sort_by(|a, b| { - b.1.total - .cmp(&a.1.total) - .then_with(|| b.0.modified.cmp(&a.0.modified)) - }); - let total_matched = results.len(); - results.truncate(context.max_results); - let (items, scores) = results.into_iter().unzip(); - (items, scores, total_matched) + sort_and_truncate(results, context) } #[inline] @@ -242,3 +225,31 @@ fn calculate_file_bonus(file: &FileItem, context: &ScoringContext) -> i32 { bonus } + +fn sort_and_truncate<'a>( + mut results: Vec<(&'a FileItem, Score)>, + context: &ScoringContext, +) -> (Vec<&'a FileItem>, Vec, usize) { + let total_matched = results.len(); + if context.reverse_order { + results.sort_by(|a, b| { + b.1.total + .cmp(&a.1.total) + .then_with(|| b.0.modified.cmp(&a.0.modified)) + }); + + if results.len() > context.max_results { + results.drain((total_matched - context.max_results)..total_matched); + } + } else { + results.sort_by(|a, b| { + a.1.total + .cmp(&b.1.total) + .then_with(|| a.0.modified.cmp(&b.0.modified)) + }); + + results.truncate(context.max_results); + } + let (items, scores) = results.into_iter().unzip(); + (items, scores, total_matched) +} diff --git a/lua/fff/rust/types.rs b/lua/fff/rust/types.rs index e423b683..141708b3 100644 --- a/lua/fff/rust/types.rs +++ b/lua/fff/rust/types.rs @@ -34,6 +34,7 @@ pub struct ScoringContext<'a> { pub max_results: usize, pub max_typos: u16, pub max_threads: usize, + pub reverse_order: bool, } #[derive(Debug, Clone, Default)] diff --git a/src/bin/jemalloc_profile.rs b/src/bin/jemalloc_profile.rs index 410b6d9c..229ea2b1 100644 --- a/src/bin/jemalloc_profile.rs +++ b/src/bin/jemalloc_profile.rs @@ -89,7 +89,7 @@ fn test_search_memory_pattern( 50 + (i % 50), // Vary result count 1 + (i % 4), // Vary thread count None, - None, // prompt_position not relevant for test + false, // prompt_position not relevant for test ); (search_result.items.len(), search_result.total_matched) } else { diff --git a/src/bin/test_memory_leak.rs b/src/bin/test_memory_leak.rs index cfbc2ab3..19c77373 100644 --- a/src/bin/test_memory_leak.rs +++ b/src/bin/test_memory_leak.rs @@ -205,7 +205,7 @@ fn main() -> Result<(), Box> { max_results, max_threads, None, - None, // prompt_position not relevant for test + false, // prompt_position not relevant for test ); let duration = search_start.elapsed(); (search_result.items.len(), duration) diff --git a/src/bin/test_watcher.rs b/src/bin/test_watcher.rs index 635bbb04..945f8d46 100644 --- a/src/bin/test_watcher.rs +++ b/src/bin/test_watcher.rs @@ -156,7 +156,7 @@ fn main() -> Result<(), Box> { let timestamp = chrono::Local::now().format("%H:%M:%S"); let file_picker = FILE_PICKER.read().unwrap(); let files = file_picker.as_ref().unwrap().get_files(); - let search_results = FilePicker::fuzzy_search(files, "rs", 5, 2, None, None); + let search_results = FilePicker::fuzzy_search(files, "rs", 5, 2, None, false); println!( "πŸ” [{}] Search test 'rs': {} matches", From 5f6dbe31e2c4545a3f4e1655cd440522bb63c300 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Sun, 17 Aug 2025 15:35:06 +0200 Subject: [PATCH 17/19] feat: Fix ordering logic --- lua/fff/picker_ui.lua | 268 ++++++++++++++++++------------------------ lua/fff/rust/score.rs | 15 +-- lua/fff/utils.lua | 6 +- 3 files changed, 128 insertions(+), 161 deletions(-) diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index b3cbea99..59c8b883 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -18,7 +18,7 @@ local function get_prompt_position() config.layout.prompt_position, terminal_width, terminal_height, - function(value) return utils.is_valid_position(value, { 'top', 'bottom' }) end, + function(value) return utils.is_one_of(value, { 'top', 'bottom' }) end, 'bottom', 'layout.prompt_position' ) @@ -38,7 +38,7 @@ local function get_preview_position() config.layout.preview_position, terminal_width, terminal_height, - function(value) return utils.is_valid_position(value, { 'left', 'right', 'top', 'bottom' }) end, + function(value) return utils.is_one_of(value, { 'left', 'right', 'top', 'bottom' }) end, 'right', 'layout.preview_position' ) @@ -259,7 +259,6 @@ M.state = { last_preview_file = nil, } ---- Create the picker UI function M.create_ui() local config = M.state.config @@ -554,9 +553,7 @@ function M.focus_input_win() vim.api.nvim_win_call(M.state.input_win, function() vim.cmd('startinsert!') end) end ---- Toggle debug display function M.toggle_debug() - local main = require('fff.main') local old_debug_state = main.config.debug.show_scores main.config.debug.show_scores = not main.config.debug.show_scores local status = main.config.debug.show_scores and 'enabled' or 'disabled' @@ -647,9 +644,19 @@ function M.update_results_sync() end local prompt_position = get_prompt_position() + + -- Calculate dynamic max_results based on visible window height + local dynamic_max_results = M.state.config.max_results + if M.state.list_win and vim.api.nvim_win_is_valid(M.state.list_win) then + local win_height = vim.api.nvim_win_get_height(M.state.list_win) + dynamic_max_results = win_height + else + dynamic_max_results = M.state.config.max_results or 100 + end + local results = file_picker.search_files( M.state.query, - M.state.config.max_results, + dynamic_max_results, M.state.config.max_threads, M.state.current_file_cache, prompt_position == 'bottom' @@ -658,8 +665,13 @@ function M.update_results_sync() -- because the actual files could be different even with same count M.state.items = results M.state.filtered_items = results - M.state.cursor = 1 - M.state.top = 1 + + if prompt_position == 'bottom' then + M.state.cursor = #results > 0 and #results or 1 + else + M.state.cursor = 1 + end + M.render_debounced() end @@ -730,143 +742,131 @@ function M.render_list() if not M.state.active then return end local items = M.state.filtered_items - local lines = {} - local max_path_width = main.config.ui and main.config.ui.max_path_width or 80 local debug_enabled = main.config and main.config.debug and main.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 display_count = math.min(#items, win_height) - local empty_lines_needed = win_height - display_count + local empty_lines_needed = 0 - for i = 1, empty_lines_needed do - table.insert(lines, '') + local prompt_position = get_prompt_position() + local cursor_line = 0 + if #items > 0 then + if prompt_position == 'bottom' then + empty_lines_needed = win_height - display_count + cursor_line = empty_lines_needed + M.state.cursor + else + cursor_line = M.state.cursor + end + cursor_line = math.max(1, math.min(cursor_line, win_height)) end - local end_idx = math.min(#items, display_count) - local items_to_show = {} - for i = 1, end_idx do - table.insert(items_to_show, items[i]) + local padded_lines = {} + if prompt_position == 'bottom' then + for _ = 1, empty_lines_needed do + table.insert(padded_lines, string.rep(' ', win_width + 5)) + end end - local display_items = items_to_show + local icon_data = {} + local path_data = {} - local line_data = {} + for i = 1, display_count do + local item = items[i] - for i, item in ipairs(display_items) do local icon, icon_hl_group = icons.get_icon_display(item.name, item.extension, false) + icon_data[i] = { icon, icon_hl_group } + local frecency = '' - 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) - - if total_frecency > 0 and debug_enabled then - local indicator = '' - if mod_frecency >= 6 then -- High modification frecency (recently modified git file) - indicator = 'πŸ”₯' -- Fire for recently modified - elseif access_frecency >= 4 then -- High access frecency (recently accessed) - indicator = '⭐' -- Star for frequently accessed - elseif total_frecency >= 3 then -- Medium total frecency - indicator = '✨' -- Sparkle for moderate activity - elseif total_frecency >= 1 then -- Low frecency - indicator = 'β€’' -- Dot for minimal activity + 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) + + 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 - frecency = string.format(' %s%d', indicator, total_frecency) end - local suffix = frecency - local current_indicator = '' - if item.is_current_file then current_indicator = ' (current)' end + local current_indicator = item.is_current_file and ' (current)' or '' + local available_width = math.max(max_path_width - #icon - 1 - #frecency - #current_indicator, 40) - local available_width = math.max(max_path_width - #icon - 1 - #suffix - #current_indicator, 40) local filename, dir_path = format_file_display(item, available_width) + path_data[i] = { filename, dir_path } - local line - if dir_path ~= '' then - line = string.format('%s %s %s%s%s', icon, filename, dir_path, suffix, current_indicator) - else - line = string.format('%s %s%s%s', icon, filename, suffix, current_indicator) - end - + local line = string.format('%s %s %s%s%s', icon, filename, dir_path, frecency, current_indicator) if item.is_current_file then line = string.format('\027[90m%s\027[0m', line) end - table.insert(lines, line) - line_data[i] = { - filename_len = #filename, - dir_path_len = #dir_path, - icon_highlight = { - hl_group = icon_hl_group, - icon_length = vim.fn.strdisplaywidth(icon), - git_status = item.git_status, - }, - } - end - - local win_width = vim.api.nvim_win_get_width(M.state.list_win) - local padded_lines = {} - for _, line in ipairs(lines) do local line_len = vim.fn.strdisplaywidth(line) - local padding = math.max(0, win_width - line_len + 5) -- +5 extra to ensure full coverage - local padded_line = line .. string.rep(' ', padding) - table.insert(padded_lines, padded_line) + local padding = math.max(0, win_width - line_len + 5) + table.insert(padded_lines, line .. string.rep(' ', padding)) end 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_option(M.state.list_buf, 'modifiable', false) - if #items > 0 then - local prompt_position = get_prompt_position() - local cursor_line + vim.api.nvim_buf_clear_namespace(M.state.list_buf, M.state.ns_id, 0, -1) - if prompt_position == 'bottom' then - cursor_line = empty_lines_needed + display_count - M.state.cursor + 1 - else - cursor_line = empty_lines_needed + M.state.cursor - end - cursor_line = math.max(1, math.min(cursor_line, win_height)) + if #items > 0 and cursor_line > 0 and cursor_line <= win_height then + vim.api.nvim_win_set_cursor(M.state.list_win, { cursor_line, 0 }) - if cursor_line > 0 and cursor_line <= win_height then - vim.api.nvim_win_set_cursor(M.state.list_win, { cursor_line, 0 }) - - vim.api.nvim_buf_clear_namespace(M.state.list_buf, M.state.ns_id, 0, -1) - - vim.api.nvim_buf_add_highlight( - M.state.list_buf, - M.state.ns_id, - M.state.config.hl.active_file, - cursor_line - 1, - 0, - -1 - ) - - local current_line = vim.api.nvim_buf_get_lines(M.state.list_buf, cursor_line - 1, cursor_line, false)[1] or '' - local line_len = vim.fn.strdisplaywidth(current_line) - local remaining_width = math.max(0, vim.api.nvim_win_get_width(M.state.list_win) - line_len) - - if remaining_width > 0 then - vim.api.nvim_buf_set_extmark(M.state.list_buf, M.state.ns_id, cursor_line - 1, -1, { - virt_text = { { string.rep(' ', remaining_width), M.state.config.hl.active_file } }, - virt_text_pos = 'eol', - }) - end + -- Cursor line highlighting + vim.api.nvim_buf_add_highlight( + M.state.list_buf, + M.state.ns_id, + M.state.config.hl.active_file, + cursor_line - 1, + 0, + -1 + ) + + -- Fill remaining width for cursor line + local current_line = padded_lines[cursor_line] or '' + local line_len = vim.fn.strdisplaywidth(current_line) + local remaining_width = math.max(0, win_width - line_len) + + if remaining_width > 0 then + vim.api.nvim_buf_set_extmark(M.state.list_buf, M.state.ns_id, cursor_line - 1, -1, { + virt_text = { { string.rep(' ', remaining_width), M.state.config.hl.active_file } }, + virt_text_pos = 'eol', + }) end - for line_idx, line_content in ipairs(lines) do - if line_content ~= '' then -- Skip empty lines - local content_line_idx = line_idx - empty_lines_needed + for i = 1, display_count do + local item = items[i] + + local line_idx = empty_lines_needed + i + local is_cursor_line = line_idx == cursor_line + local line_content = padded_lines[line_idx] - local icon_info = line_data[content_line_idx].icon_highlight - if icon_info and icon_info.hl_group and icon_info.icon_length > 0 then + if line_content then + local icon, icon_hl_group = unpack(icon_data[i]) + local filename, dir_path = unpack(path_data[i]) + + -- Icon highlighting + if icon_hl_group and vim.fn.strdisplaywidth(icon) > 0 then vim.api.nvim_buf_add_highlight( M.state.list_buf, M.state.ns_id, - icon_info.hl_group, + icon_hl_group, line_idx - 1, 0, - icon_info.icon_length + vim.fn.strdisplaywidth(icon) ) end + -- Frecency highlighting if debug_enabled then local star_start, star_end = line_content:find('⭐%d+') if star_start then @@ -881,47 +881,28 @@ function M.render_list() end end - local debug_start, debug_end = line_content:find('%[%d+|[^%]]*%]') - if debug_start then + 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, - M.state.config.hl.debug, + 'Comment', line_idx - 1, - debug_start - 1, - debug_end + prefix_len, + prefix_len + #dir_path ) end - local icon_match = line_content:match('^%S+') -- First non-space sequence (icon) - if icon_match then - local filename_len = line_data[content_line_idx].filename_len - local dir_path_len = line_data[content_line_idx].dir_path_len - - if filename_len > 0 and dir_path_len > 0 then - local prefix_len = #icon_match + 1 + filename_len + 1 -- icon + space + filename + space - - vim.api.nvim_buf_add_highlight( - M.state.list_buf, - M.state.ns_id, - 'Comment', - line_idx - 1, - prefix_len, - prefix_len + dir_path_len - ) - end - end - - local is_cursor_line = line_idx == cursor_line - local border_char = ' ' -- render space so it is highlighted + local border_char = ' ' local border_hl = nil - if icon_info and icon_info.git_status and git_utils.should_show_border(icon_info.git_status) then - border_char = git_utils.get_border_char(icon_info.git_status) + if item.git_status and git_utils.should_show_border(item.git_status) then + border_char = git_utils.get_border_char(item.git_status) if is_cursor_line then - border_hl = git_utils.get_border_highlight_selected(icon_info.git_status) + border_hl = git_utils.get_border_highlight_selected(item.git_status) else - border_hl = git_utils.get_border_highlight(icon_info.git_status) + border_hl = git_utils.get_border_highlight(item.git_status) end end @@ -959,8 +940,6 @@ function M.update_preview() end if M.state.last_preview_file == item.path then return end - - local preview = require('fff.file_picker.preview') preview.clear() M.state.last_preview_file = item.path @@ -1067,13 +1046,7 @@ function M.move_up() if not M.state.active then return end if #M.state.filtered_items == 0 then return end - local prompt_position = get_prompt_position() - - if prompt_position == 'bottom' then - M.state.cursor = math.min(M.state.cursor + 1, #M.state.filtered_items) - else - M.state.cursor = math.max(M.state.cursor - 1, 1) - end + M.state.cursor = math.max(M.state.cursor - 1, 1) M.render_list() M.update_preview() @@ -1084,13 +1057,7 @@ function M.move_down() if not M.state.active then return end if #M.state.filtered_items == 0 then return end - local prompt_position = get_prompt_position() - - if prompt_position == 'bottom' then - M.state.cursor = math.max(M.state.cursor - 1, 1) - else - M.state.cursor = math.min(M.state.cursor + 1, #M.state.filtered_items) - end + M.state.cursor = math.min(M.state.cursor + 1, #M.state.filtered_items) M.render_list() M.update_preview() @@ -1191,7 +1158,6 @@ function M.close() M.state.items = {} M.state.filtered_items = {} M.state.cursor = 1 - M.state.top = 1 M.state.query = '' M.state.ns_id = nil M.state.last_preview_file = nil diff --git a/lua/fff/rust/score.rs b/lua/fff/rust/score.rs index d4283157..a02eca18 100644 --- a/lua/fff/rust/score.rs +++ b/lua/fff/rust/score.rs @@ -226,6 +226,7 @@ fn calculate_file_bonus(file: &FileItem, context: &ScoringContext) -> i32 { bonus } +/// Dynamically sorts and returns the top results either in ascending or descending order fn sort_and_truncate<'a>( mut results: Vec<(&'a FileItem, Score)>, context: &ScoringContext, @@ -233,19 +234,19 @@ fn sort_and_truncate<'a>( let total_matched = results.len(); if context.reverse_order { results.sort_by(|a, b| { - b.1.total - .cmp(&a.1.total) - .then_with(|| b.0.modified.cmp(&a.0.modified)) + a.1.total + .cmp(&b.1.total) + .then_with(|| a.0.modified.cmp(&b.0.modified)) }); if results.len() > context.max_results { - results.drain((total_matched - context.max_results)..total_matched); + results.drain(0..(total_matched - context.max_results)); } } else { results.sort_by(|a, b| { - a.1.total - .cmp(&b.1.total) - .then_with(|| a.0.modified.cmp(&b.0.modified)) + b.1.total + .cmp(&a.1.total) + .then_with(|| b.0.modified.cmp(&a.0.modified)) }); results.truncate(context.max_results); diff --git a/lua/fff/utils.lua b/lua/fff/utils.lua index f00833d1..41188784 100644 --- a/lua/fff/utils.lua +++ b/lua/fff/utils.lua @@ -49,11 +49,11 @@ function M.is_valid_ratio(value) return type(value) == 'number' and value > 0 an --- Validate position string --- @param value any Value to validate ---- @param valid_positions table List of valid position strings +--- @param values table List of valid values strings --- @return boolean True if valid position -function M.is_valid_position(value, valid_positions) +function M.is_one_of(value, values) if type(value) ~= 'string' then return false end - for _, pos in ipairs(valid_positions) do + for _, pos in ipairs(values) do if value == pos then return true end end return false From c209e37c02499e5455cb7143646f28d39866163d Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Sun, 17 Aug 2025 17:11:01 +0200 Subject: [PATCH 18/19] fix configuration --- README.md | 165 ++++++++++++-------------------- doc/fff.nvim.txt | 2 +- lua/fff/file_picker/preview.lua | 6 +- lua/fff/main.lua | 41 +++----- lua/fff/picker_ui.lua | 99 ++++++++++++++----- 5 files changed, 155 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index bb2c8679..19fd4670 100644 --- a/README.md +++ b/README.md @@ -72,110 +72,69 @@ FFF.nvim comes with sensible defaults. Here's the complete configuration with al ```lua require('fff').setup({ - -- Core settings - base_path = vim.fn.getcwd(), -- Base directory for file indexing - max_results = 100, -- Maximum search results to display - max_threads = 4, -- Maximum threads for fuzzy search - prompt = 'πŸͺΏ ', -- Input prompt symbol - title = 'FFF Files', -- Window title - ui_enabled = true, -- Enable UI (default: true) - - -- Window dimensions - width = 0.8, -- Window width as fraction of screen - height = 0.8, -- Window height as fraction of screen - - -- Preview configuration - preview = { - enabled = true, -- Enable preview pane - width = 0.5, -- Preview width as fraction of window - max_lines = 5000, -- Maximum lines to load - max_size = 10 * 1024 * 1024, -- Maximum file size (10MB) - imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', -- ImageMagick info format - line_numbers = false, -- Show line numbers in preview - wrap_lines = false, -- Wrap long lines - show_file_info = true, -- Show file info header - binary_file_threshold = 1024, -- Bytes to check for binary detection - filetypes = { -- Per-filetype settings - svg = { wrap_lines = true }, - markdown = { wrap_lines = true }, - text = { wrap_lines = true }, - log = { tail_lines = 100 }, + base_path = vim.fn.getcwd(), + prompt = 'πŸͺΏ ', + title = 'FFFiles', + max_results = 100, + max_threads = 4, + layout = { + height = 0.8, + width = 0.8, + prompt_position = 'bottom', -- or 'top' + preview_position = 'right', -- or 'left', 'right', 'top', 'bottom' + preview_size = 0.5, }, - }, - - -- Layout configuration (alternative to width/height) - layout = { - prompt_position = 'top', -- Position of prompt ('top' or 'bottom') - preview_position = 'right', -- Position of preview ('right' or 'left') - preview_width = 0.4, -- Width of preview pane - height = 0.8, -- Window height - width = 0.8, -- Window width - }, - - -- Keymaps - keymaps = { - close = '', - select = '', - select_split = '', - select_vsplit = '', - select_tab = '', - move_up = { '', '' }, -- Multiple bindings supported - move_down = { '', '' }, - preview_scroll_up = '', - preview_scroll_down = '', - toggle_debug = '', -- Toggle debug scores display - }, - - -- Highlight groups - hl = { - border = 'FloatBorder', - normal = 'Normal', - cursor = 'CursorLine', - matched = 'IncSearch', - title = 'Title', - prompt = 'Question', - active_file = 'Visual', - frecency = 'Number', - debug = 'Comment', - }, - - -- Frecency tracking (track file access patterns) - frecency = { - enabled = true, -- Enable frecency tracking - db_path = vim.fn.stdpath('cache') .. '/fff_nvim', -- Database location - }, - - -- Logging configuration - logging = { - enabled = true, -- Enable logging - log_file = vim.fn.stdpath('log') .. '/fff.log', -- Log file location - log_level = 'info', -- Log level (debug, info, warn, error) - }, - - -- UI appearance - ui = { - wrap_paths = true, -- Wrap long file paths in list - wrap_indent = 2, -- Indentation for wrapped paths - max_path_width = 80, -- Maximum path width before wrapping - }, - - -- Image preview (requires terminal with image support) - image_preview = { - enabled = true, -- Enable image previews - max_width = 80, -- Maximum image width in columns - max_height = 24, -- Maximum image height in lines - }, - - -- Icons - icons = { - enabled = true, -- Enable file icons - }, - - -- Debug options - debug = { - enabled = false, -- Enable debug mode - show_scores = false, -- Show scoring information (toggle with F2) - }, + preview = { + enabled = true, + max_size = 10 * 1024 * 1024, -- Do not try to read files larger than 10MB + chunk_size = 8192, -- Bytes per chunk for dynamic loading (8kb - fits ~100-200 lines) + binary_file_threshold = 1024, -- amount of bytes to scan for binary content (set 0 to disable) + imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', + line_numbers = false, + wrap_lines = false, + show_file_info = true, + filetypes = { + svg = { wrap_lines = true }, + markdown = { wrap_lines = true }, + text = { wrap_lines = true }, + }, + }, + keymaps = { + close = '', + select = '', + select_split = '', + select_vsplit = '', + select_tab = '', + move_up = { '', '' }, + move_down = { '', '' }, + preview_scroll_up = '', + preview_scroll_down = '', + toggle_debug = '', + }, + hl = { + border = 'FloatBorder', + normal = 'Normal', + cursor = 'CursorLine', + matched = 'IncSearch', + title = 'Title', + prompt = 'Question', + active_file = 'Visual', + frecency = 'Number', + debug = 'Comment', + }, + frecency = { + enabled = true, + db_path = vim.fn.stdpath('cache') .. '/fff_nvim', + }, + debug = { + enabled = false, -- Set to true to show scores in the UI + show_scores = false, + }, + logging = { + enabled = true, + log_file = vim.fn.stdpath('log') .. '/fff.log', + log_level = 'info', + } }) ``` diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index d7819f97..5c200d94 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -114,7 +114,7 @@ all available options: layout = { prompt_position = 'top', -- Position of prompt ('top' or 'bottom') preview_position = 'right', -- Position of preview ('right' or 'left') - preview_width = 0.4, -- Width of preview pane + preview_width = 0.5, -- Width of preview pane height = 0.8, -- Window height width = 0.8, -- Window width }, diff --git a/lua/fff/file_picker/preview.lua b/lua/fff/file_picker/preview.lua index d64f1603..7cfe5cf1 100644 --- a/lua/fff/file_picker/preview.lua +++ b/lua/fff/file_picker/preview.lua @@ -288,7 +288,11 @@ function M.is_binary_file_async(file_path, callback) end end - -- Check file content asynchronously + if M.config.binary_file_threshold <= 0 then + callback(false) + return + end + vim.uv.fs_open(file_path, 'r', 438, function(err, fd) if err or not fd then callback(false) diff --git a/lua/fff/main.lua b/lua/fff/main.lua index b3cbe477..8bd8b026 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -105,19 +105,25 @@ function M.setup(config) local default_config = { base_path = vim.fn.getcwd(), prompt = 'πŸͺΏ ', - title = 'FFF Files', + title = 'FFFiles', max_results = 100, max_threads = 4, + layout = { + height = 0.8, + width = 0.8, + prompt_position = 'bottom', -- or 'top' + preview_position = 'right', -- or 'left', 'right', 'top', 'bottom' + preview_size = 0.5, + }, preview = { enabled = true, - max_size = 1 * 1024 * 1024, -- Keep file size limit for early detection - max_line_length = 1000, -- Keep line length limit for memory safety - chunk_size = 16384, -- Bytes per chunk for dynamic loading (16KB - fits ~100-200 lines) + max_size = 10 * 1024 * 1024, -- Do not try to read files larger than 10MB + chunk_size = 8192, -- Bytes per chunk for dynamic loading (8kb - fits ~100-200 lines) + binary_file_threshold = 1024, -- amount of bytes to scan for binary content (set 0 to disable) imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', line_numbers = false, wrap_lines = false, show_file_info = true, - binary_file_threshold = 1024, filetypes = { svg = { wrap_lines = true }, markdown = { wrap_lines = true }, @@ -147,40 +153,19 @@ function M.setup(config) frecency = 'Number', debug = 'Comment', }, - layout = { - height = 0.8, - width = 0.8, - prompt_position = 'bottom', - preview_position = 'right', -- 'left', 'right', 'top', 'bottom' - preview_size = 0.4, - }, frecency = { enabled = true, db_path = vim.fn.stdpath('cache') .. '/fff_nvim', }, debug = { - enabled = false, + enabled = false, -- Set to true to show scores in the UI show_scores = false, }, logging = { enabled = true, log_file = vim.fn.stdpath('log') .. '/fff.log', log_level = 'info', - }, - ui = { - wrap_paths = true, - wrap_indent = 2, - max_path_width = 80, - }, - image_preview = { - enabled = true, - max_width = 80, - max_height = 24, - }, - icons = { - enabled = true, - }, - ui_enabled = true, + } } local migrated_user_config = handle_deprecated_config(config) diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index 59c8b883..a515dfde 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -72,10 +72,14 @@ end --- @return table Layout configuration function M.calculate_layout_dimensions(cfg) local BORDER_SIZE = 2 - local PROMPT_HEIGHT = 3 + local PROMPT_HEIGHT = 2 local SEPARATOR_WIDTH = 1 local SEPARATOR_HEIGHT = 1 + if not utils.is_one_of(cfg.preview_position, { 'left', 'right', 'top', 'bottom' }) then + error('Invalid preview position: ' .. tostring(cfg.preview_position)) + end + local layout = {} local preview_enabled = M.enabled_preview() @@ -209,8 +213,8 @@ function M.calculate_layout_dimensions(cfg) col = layout.preview.col, row = layout.preview.row, } - layout.preview.row = layout.preview.row + cfg.file_info_height + SEPARATOR_HEIGHT - layout.preview.height = math.max(0, layout.preview.height - cfg.file_info_height - SEPARATOR_HEIGHT) + layout.preview.row = layout.preview.row + cfg.file_info_height + SEPARATOR_HEIGHT + 1 + layout.preview.height = math.max(3, layout.preview.height - cfg.file_info_height - SEPARATOR_HEIGHT - 1) else layout.file_info = { width = layout.preview.width, @@ -218,8 +222,8 @@ function M.calculate_layout_dimensions(cfg) col = layout.preview.col, row = layout.preview.row, } - layout.preview.row = layout.preview.row + cfg.file_info_height + SEPARATOR_HEIGHT - layout.preview.height = math.max(0, layout.preview.height - cfg.file_info_height - SEPARATOR_HEIGHT) + layout.preview.row = layout.preview.row + cfg.file_info_height + SEPARATOR_HEIGHT + 1 + layout.preview.height = math.max(3, layout.preview.height - cfg.file_info_height - SEPARATOR_HEIGHT - 1) end end @@ -341,8 +345,8 @@ function M.create_ui() M.state.file_info_buf = nil end - -- Create list window - M.state.list_win = vim.api.nvim_open_win(M.state.list_buf, false, { + -- Create list window with conditional title based on prompt position + local list_window_config = { relative = 'editor', width = layout.list_width, height = layout.list_height, @@ -350,9 +354,16 @@ function M.create_ui() row = layout.list_row, border = 'single', style = 'minimal', - title = ' Files ', - title_pos = 'left', - }) + } + + local title = ' ' .. (M.state.config.title or 'FFFiles') .. ' ' + -- Only add title if prompt is at bottom - when prompt is top, title should be on input + if prompt_position == 'bottom' then + list_window_config.title = title + list_window_config.title_pos = 'left' + end + + M.state.list_win = vim.api.nvim_open_win(M.state.list_buf, false, list_window_config) -- Create file info window if debug enabled if debug_enabled_in_preview and layout.file_info then @@ -386,8 +397,8 @@ function M.create_ui() }) end - -- Create input window - M.state.input_win = vim.api.nvim_open_win(M.state.input_buf, false, { + -- Create input window with conditional title based on prompt position + local input_window_config = { relative = 'editor', width = layout.input_width, height = 1, @@ -395,7 +406,15 @@ function M.create_ui() row = layout.input_row, border = 'single', 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' + end + + M.state.input_win = vim.api.nvim_open_win(M.state.input_buf, false, input_window_config) M.setup_buffers() M.setup_windows() @@ -944,24 +963,54 @@ function M.update_preview() M.state.last_preview_file = item.path - local relative_path = item.relative_path or item.path -- Use relative path if available - local win_width = vim.api.nvim_win_get_width(M.state.preview_win) - local max_title_width = win_width - 4 -- Account for border and padding + local relative_path = item.relative_path or item.path + local max_title_width = vim.api.nvim_win_get_width(M.state.preview_win) local title - if #relative_path <= max_title_width then + local target_length = max_title_width + + if #relative_path + 2 <= target_length then title = string.format(' %s ', relative_path) else - local filename = vim.fn.fnamemodify(relative_path, ':t') - local dirname = vim.fn.fnamemodify(relative_path, ':h') - local available_dir_width = max_title_width - #filename - 6 -- Account for '.../' and spaces + local available_chars = target_length - 2 - if available_dir_width > 10 then - local truncated_dir = '...' .. dirname:sub(-available_dir_width + 3) - title = string.format(' %s/%s ', truncated_dir, filename) + local filename = vim.fn.fnamemodify(relative_path, ':t') + if available_chars <= 3 then + title = filename else - if #filename > max_title_width - 4 then filename = filename:sub(1, max_title_width - 7) .. '...' end - title = string.format(' %s ', filename) + if #filename + 5 <= available_chars then + local normalized_path = vim.fs.normalize(relative_path) + local path_parts = vim.split(normalized_path, '[/\\]', { plain = false }) + + local segments = {} + for _, part in ipairs(path_parts) do + if part ~= '' then table.insert(segments, part) end + end + + local segments_to_show = { filename } + local current_length = #filename + 4 -- 4 for '../' prefix and spaces + + for i = #segments - 1, 1, -1 do + local segment = segments[i] + local new_length = current_length + #segment + 1 -- +1 for '/' + + if new_length <= available_chars then + table.insert(segments_to_show, 1, segment) + current_length = new_length + else + break + end + end + + if #segments_to_show == #segments then + title = string.format(' %s ', table.concat(segments_to_show, '/')) + else + title = string.format(' ../%s ', table.concat(segments_to_show, '/')) + end + else + local truncated_filename = filename:sub(1, available_chars - 3) .. '...' + title = string.format(' %s ', truncated_filename) + end end end From 1c63f7e4cdca5adeeb6d105d0ee380d76b36f9bf Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Sun, 17 Aug 2025 17:15:26 +0200 Subject: [PATCH 19/19] stylua --- lua/fff/main.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/fff/main.lua b/lua/fff/main.lua index 44e8a52d..988c291c 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -165,7 +165,7 @@ function M.setup(config) enabled = true, log_file = vim.fn.stdpath('log') .. '/fff.log', log_level = 'info', - } + }, } local migrated_user_config = handle_deprecated_config(config)