From 9d6b28790eb187f2dedcc0251560fc864b15d6e6 Mon Sep 17 00:00:00 2001 From: thuan do Date: Sat, 2 May 2026 21:02:34 +0700 Subject: [PATCH 1/9] feat: resume last picker (#405) Add ability to resume the last closed picker with full state (query, results, cursor, mode). Supports both find_files and live_grep modes with independent saved states. - Save picker state (query, results, cursor, mode, pagination, etc.) before closing - Resume via :FFFResume (last closed picker, regardless of mode) - Resume via find_files({ resume = true }) or live_grep({ resume = true }) for mode-specific resume - Falls back to opening a new picker if nothing to resume --- lua/fff/main.lua | 45 +++++++-- lua/fff/picker_ui.lua | 226 +++++++++++++++++++++++++++++++++++++++++- plugin/fff.lua | 6 ++ 3 files changed, 268 insertions(+), 9 deletions(-) diff --git a/lua/fff/main.lua b/lua/fff/main.lua index 92d9a746..e66ceb19 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -6,19 +6,29 @@ M.state = { initialized = false } --- @param config table Configuration options function M.setup(config) vim.g.fff = config end ---- Find files in current directory ---- @param opts? table Optional configuration {renderer = custom_renderer} +--- Find files in current directory. +--- When opts.resume is true, resumes the last find_files picker (or opens a new one if none saved). +--- @param opts? table Optional configuration {renderer = custom_renderer, resume = boolean} function M.find_files(opts) local picker_ok, picker_ui = pcall(require, 'fff.picker_ui') - if picker_ok then - picker_ui.open(opts) - else + if not picker_ok then vim.notify('Failed to load picker UI: ' .. picker_ui, vim.log.levels.ERROR) + return + end + + if opts and opts.resume then + local cleaned = vim.deepcopy(opts) + cleaned.resume = nil + picker_ui.resume_find_files(cleaned) + return end + + picker_ui.open(opts) end ---- Live grep: search file contents in the current directory ---- @param opts? {cwd?: string, title?: string, prompt?: string, layout?: table, grep?: {max_file_size?: number, smart_case?: boolean, max_matches_per_file?: number, modes?: string[]}, query?: string} Optional configuration overrides +--- Live grep: search file contents in the current directory. +--- When opts.resume is true, resumes the last live_grep picker (or opens a new one if none saved). +--- @param opts? {cwd?: string, title?: string, prompt?: string, layout?: table, grep?: {max_file_size?: number, smart_case?: boolean, max_matches_per_file?: number, modes?: string[]}, query?: string, resume?: boolean} Optional configuration overrides function M.live_grep(opts) local picker_ok, picker_ui = pcall(require, 'fff.picker_ui') if not picker_ok then @@ -26,6 +36,13 @@ function M.live_grep(opts) return end + if opts and opts.resume then + local cleaned = vim.deepcopy(opts) + cleaned.resume = nil + picker_ui.resume_live_grep(cleaned) + return + end + local config = require('fff.conf').get() local grep_renderer = require('fff.grep.grep_renderer') @@ -215,6 +232,20 @@ function M.change_indexing_directory(new_path) return false end +--- Resume the most recently closed picker (find_files or live_grep). +--- Similar to Telescope's `require('telescope.builtin').resume()`. +---@return boolean true if a picker was resumed, false if there is nothing to resume +function M.resume() + local picker_ok, picker_ui = pcall(require, 'fff.picker_ui') + if not picker_ok then + vim.notify('Failed to load picker UI: ' .. picker_ui, vim.log.levels.ERROR) + return false + end + return picker_ui.resume() +end + + + --- Opens the file under the cursor with an optional callback if the only file --- is found and we are about to inline open it --- @param open_cb function|nil Optional callback function to execute after opening the file diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index a036990f..ea54e0f8 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -10,6 +10,16 @@ local list_renderer = require('fff.list_renderer') local scrollbar = require('fff.scrollbar') local rust = require('fff.rust') +--- Saved state from the last closed file picker, used by resume(). +--- Populated in close() just before state is cleared. +local last_file_picker_state = nil +--- Saved state from the last closed grep picker, used by resume(). +--- Populated in close() just before state is cleared. +local last_grep_picker_state = nil +--- Tracks which mode was closed most recently ('files' or 'grep'). +--- Used by resume() to pick which state to restore. +local last_closed_mode = nil + --- Base path of picker can change that's why we can not rely on relative --- path for reading/opening files. This function resolves correct absolute path --- @param relative_path string|nil @@ -2557,12 +2567,220 @@ function M.relayout() M.update_status() end -function M.close() +--- Save the current picker state for later resume, then close. +local function save_state_and_close() + if not M.state.active then return end + + -- Build the state snapshot + local snapshot = { + config = M.state.config, + mode = M.state.mode, + renderer = M.state.renderer, + grep_config = M.state.grep_config, + grep_mode = M.state.grep_mode, + query = M.state.query or '', + current_file_cache = M.state.current_file_cache, + pagination = vim.deepcopy(M.state.pagination), + cursor = M.state.cursor, + location = vim.deepcopy(M.state.location), + items = vim.deepcopy(M.state.items), + filtered_items = vim.deepcopy(M.state.filtered_items), + combo_visible = M.state.combo_visible, + combo_initial_cursor = M.state.combo_initial_cursor, + suggestion_items = vim.deepcopy(M.state.suggestion_items), + suggestion_source = M.state.suggestion_source, + selected_files = vim.deepcopy(M.state.selected_files), + selected_items = vim.deepcopy(M.state.selected_items), + } + + -- Get base path from the file picker or current config + local fuzzy = require('fff.core').ensure_initialized() + local ok, base_path = pcall(fuzzy.get_base_path) + if ok and base_path then + snapshot.base_path = base_path + else + snapshot.base_path = M.state.config and M.state.config.base_path or nil + end + + -- Save to the mode-specific slot + if M.state.mode == 'grep' then + last_grep_picker_state = snapshot + last_closed_mode = 'grep' + else + last_file_picker_state = snapshot + last_closed_mode = 'files' + end + + M.close_windows() +end + +--- Internal: restore picker from a saved state snapshot. +---@param state table The saved state table +---@param source_label string Label for error messages +---@return boolean +local function restore_from_state(state, source_label) + -- Ensure the file picker is initialized + if not file_picker.is_initialized() then + if not file_picker.setup() then + vim.notify('Failed to initialize file picker', vim.log.levels.ERROR) + return false + end + end + + -- Restore the picker with the saved config and mode + M.state.renderer = state.renderer + M.state.mode = state.mode + M.state.grep_config = state.grep_config + M.state.grep_mode = state.grep_mode + M.state.selected_files = vim.deepcopy(state.selected_files or {}) + M.state.selected_items = vim.deepcopy(state.selected_items or {}) + + -- Restore the saved base_path for the indexer if it differs from the current CWD + if state.base_path then + M.change_indexing_directory(state.base_path) + end + + -- Merge saved config with current defaults (to pick up any user config changes since last close) + local config = conf.get() + local merged_config = vim.tbl_deep_extend('force', config, { + renderer = state.renderer, + mode = state.mode, + grep_config = state.grep_config, + }) + M.state.config = merged_config + + if not M.create_ui() then + vim.notify('FFF: failed to create picker UI for ' .. source_label, vim.log.levels.ERROR) + return false + end + + M.state.active = true + M.state.current_file_cache = state.current_file_cache + + -- Restore the full picker state + M.state.query = state.query + M.state.items = state.items or {} + M.state.filtered_items = state.filtered_items or {} + M.state.cursor = math.min(state.cursor or 1, #(state.filtered_items or {})) + M.state.cursor = math.max(M.state.cursor, 1) + M.state.location = state.location + M.state.pagination = vim.deepcopy(state.pagination or { + page_index = 0, + page_size = 20, + total_matched = 0, + prefetch_margin = 5, + grep_file_offsets = {}, + grep_next_file_offset = 0, + }) + M.state.combo_visible = state.combo_visible ~= false + M.state.combo_initial_cursor = state.combo_initial_cursor + M.state.suggestion_items = state.suggestion_items + M.state.suggestion_source = state.suggestion_source + + -- Set the query text in the input buffer + if state.query and state.query ~= '' then + vim.api.nvim_buf_set_lines(M.state.input_buf, 0, -1, false, { M.state.config.prompt .. state.query }) + end + + -- Render the restored state + M.render_list() + M.update_preview() + M.update_status() + + vim.api.nvim_set_current_win(M.state.input_win) + + -- Position cursor at end of query + vim.schedule(function() + if M.state.active and M.state.input_win and vim.api.nvim_win_is_valid(M.state.input_win) then + local prompt_len = #M.state.config.prompt + vim.api.nvim_win_set_cursor(M.state.input_win, { 1, prompt_len + #state.query }) + vim.cmd('startinsert!') + end + end) + + M.monitor_scan_progress(0) + return true +end + +--- Resume the most recently closed picker, regardless of mode. +---@return boolean true if a picker was resumed, false if there is nothing to resume +function M.resume() + if M.state.active then + vim.notify('FFF: close the current picker before resuming', vim.log.levels.INFO) + return false + end + + -- Pick the most recently closed mode + if last_closed_mode == 'grep' then + return M.resume_live_grep() + elseif last_closed_mode == 'files' then + return M.resume_find_files() + end + + -- Fallback: try grep state, then file state, then open an empty find_files picker + if last_grep_picker_state then + return restore_from_state(last_grep_picker_state, 'grep resume') + end + if last_file_picker_state then + return restore_from_state(last_file_picker_state, 'files resume') + end + + -- Nothing saved: open an empty find_files picker + return M.open() +end + +--- Resume the last file picker (find_files mode). +--- Falls back to opening a new find_files picker if nothing to resume. +---@param opts? table Optional config overrides for fallback open +---@return boolean +function M.resume_find_files(opts) + if M.state.active then + vim.notify('FFF: close the current picker before resuming', vim.log.levels.INFO) + return false + end + + if not last_file_picker_state then + -- Nothing saved: open a new find_files picker + return M.open(opts) + end + + return restore_from_state(last_file_picker_state, 'find_files resume') +end + +--- Resume the last live_grep picker. +--- Falls back to opening a new live_grep picker if nothing to resume. +---@param opts? table Optional config overrides for fallback open +---@return boolean +function M.resume_live_grep(opts) + if M.state.active then + vim.notify('FFF: close the current picker before resuming', vim.log.levels.INFO) + return false + end + + if not last_grep_picker_state then + -- Nothing saved: open a new live_grep picker + local config = conf.get() + local grep_renderer = require('fff.grep.grep_renderer') + local grep_config = vim.tbl_deep_extend('force', config.grep or {}, (opts and opts.grep) or {}) + M.open(vim.tbl_deep_extend('force', { + mode = 'grep', + renderer = grep_renderer, + grep_config = grep_config, + title = 'Live Grep', + }, opts or {})) + return true + end + + return restore_from_state(last_grep_picker_state, 'live_grep resume') +end + +function M.close_windows() if not M.state.active then return end - vim.cmd('stopinsert') M.state.active = false + vim.cmd('stopinsert') + restore_paste(M.state.restore_paste) combo_renderer.cleanup() @@ -2640,6 +2858,10 @@ function M.close() pcall(vim.api.nvim_del_augroup_by_name, 'fff_picker_focus') end +function M.close() + save_state_and_close() +end + --- Helper function to determine current file cache for deprioritization --- @param base_path string|nil Base path for relative path calculation --- @return string|nil Current file cache path diff --git a/plugin/fff.lua b/plugin/fff.lua index 053017ca..48ec023f 100644 --- a/plugin/fff.lua +++ b/plugin/fff.lua @@ -22,6 +22,12 @@ else }) end +vim.api.nvim_create_user_command('FFFResume', function() + require('fff').resume() +end, { + desc = 'Resume the last FFF picker (restores query, results, cursor, and mode)', +}) + vim.api.nvim_create_user_command('FFFFind', function(opts) local fff = require('fff') if opts.args and opts.args ~= '' then From 61ac5ec2d9505f221dfe6ae2434ab16e9d574cb2 Mon Sep 17 00:00:00 2001 From: thuan do Date: Sun, 3 May 2026 08:17:32 +0700 Subject: [PATCH 2/9] chore: update doc --- README.md | 6 ++++++ doc/fff.nvim.txt | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index be44aa16..d54391d9 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb lazy = false, -- the plugin lazy-initialises itself keys = { { "ff", function() require('fff').find_files() end, desc = 'FFFind files' }, + { "fr", function() require('fff').resume() end, desc = 'FFFind resume' }, { "fg", function() require('fff').live_grep() end, desc = 'LiFFFe grep' }, { "fz", function() require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } }) end, @@ -157,13 +158,17 @@ vim.g.fff = { } vim.keymap.set('n', 'ff', function() require('fff').find_files() end, { desc = 'FFFind files' }) +vim.keymap.set('n', 'fr', function() require('fff').resume() end, { desc = 'FFFind resume' }) ``` ### Public API ```lua require('fff').find_files() -- find files in current repo +require('fff').find_files({ resume = true }) -- resume last find_files picker require('fff').live_grep() -- live content grep +require('fff').live_grep({ resume = true }) -- resume last live_grep picker +require('fff').resume() -- resume last closed picker (files or grep) require('fff').scan_files() -- force rescan require('fff').refresh_git_status() -- refresh git status require('fff').find_files_in_dir(path) -- find in a specific dir @@ -172,6 +177,7 @@ require('fff').change_indexing_directory(new_path) -- change root ### Commands +- `:FFFResume`. Resume the last closed picker (restores query, results, cursor, and mode). - `:FFFScan`. Rescan files. - `:FFFRefreshGit`. Refresh git status. - `:FFFClearCache [all|frecency|files]`. Clear caches. diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index 36b6f737..98841b76 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -143,6 +143,7 @@ LAZY.NVIM lazy = false, -- the plugin lazy-initialises itself keys = { { "ff", function() require('fff').find_files() end, desc = 'FFFind files' }, + { "fr", function() require('fff').resume() end, desc = 'FFFind resume' }, { "fg", function() require('fff').live_grep() end, desc = 'LiFFFe grep' }, { "fz", function() require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } }) end, @@ -178,6 +179,7 @@ VIM.PACK } vim.keymap.set('n', 'ff', function() require('fff').find_files() end, { desc = 'FFFind files' }) + vim.keymap.set('n', 'fr', function() require('fff').resume() end, { desc = 'FFFind resume' }) < @@ -185,7 +187,10 @@ PUBLIC API ~ >lua require('fff').find_files() -- find files in current repo + require('fff').find_files({ resume = true }) -- resume last find_files picker require('fff').live_grep() -- live content grep + require('fff').live_grep({ resume = true }) -- resume last live_grep picker + require('fff').resume() -- resume last closed picker (files or grep) require('fff').scan_files() -- force rescan require('fff').refresh_git_status() -- refresh git status require('fff').find_files_in_dir(path) -- find in a specific dir @@ -195,6 +200,7 @@ PUBLIC API ~ COMMANDS ~ +- `:FFFResume`. Resume the last closed picker (restores query, results, cursor, and mode). - `:FFFScan`. Rescan files. - `:FFFRefreshGit`. Refresh git status. - `:FFFClearCache [all|frecency|files]`. Clear caches. From 90ceaccb23e56666a1690ddfffe1c88930089dbd Mon Sep 17 00:00:00 2001 From: thuan do Date: Sun, 3 May 2026 08:23:18 +0700 Subject: [PATCH 3/9] fix: address CI failures for resume feature - stylua: collapse simple statements to single line per .stylua.toml (FFFResume command in plugin/fff.lua, simple if/function blocks in picker_ui.lua) - stylua: remove extra blank lines in main.lua - lua-language-server: fix return type annotations on M.open(), M.resume(), M.resume_find_files(), M.resume_live_grep() to consistently return boolean --- lua/fff/main.lua | 2 -- lua/fff/picker_ui.lua | 25 ++++++++----------------- plugin/fff.lua | 4 +--- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/lua/fff/main.lua b/lua/fff/main.lua index e66ceb19..e44a7b4b 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -244,8 +244,6 @@ function M.resume() return picker_ui.resume() end - - --- Opens the file under the cursor with an optional callback if the only file --- is found and we are about to inline open it --- @param open_cb function|nil Optional callback function to execute after opening the file diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index ea54e0f8..6cf09567 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -2636,9 +2636,7 @@ local function restore_from_state(state, source_label) M.state.selected_items = vim.deepcopy(state.selected_items or {}) -- Restore the saved base_path for the indexer if it differs from the current CWD - if state.base_path then - M.change_indexing_directory(state.base_path) - end + if state.base_path then M.change_indexing_directory(state.base_path) end -- Merge saved config with current defaults (to pick up any user config changes since last close) local config = conf.get() @@ -2702,8 +2700,7 @@ local function restore_from_state(state, source_label) return true end ---- Resume the most recently closed picker, regardless of mode. ----@return boolean true if a picker was resumed, false if there is nothing to resume +---@return boolean true if a picker was resumed, false otherwise function M.resume() if M.state.active then vim.notify('FFF: close the current picker before resuming', vim.log.levels.INFO) @@ -2718,12 +2715,8 @@ function M.resume() end -- Fallback: try grep state, then file state, then open an empty find_files picker - if last_grep_picker_state then - return restore_from_state(last_grep_picker_state, 'grep resume') - end - if last_file_picker_state then - return restore_from_state(last_file_picker_state, 'files resume') - end + if last_grep_picker_state then return restore_from_state(last_grep_picker_state, 'grep resume') end + if last_file_picker_state then return restore_from_state(last_file_picker_state, 'files resume') end -- Nothing saved: open an empty find_files picker return M.open() @@ -2733,7 +2726,6 @@ end --- Falls back to opening a new find_files picker if nothing to resume. ---@param opts? table Optional config overrides for fallback open ---@return boolean -function M.resume_find_files(opts) if M.state.active then vim.notify('FFF: close the current picker before resuming', vim.log.levels.INFO) return false @@ -2858,9 +2850,7 @@ function M.close_windows() pcall(vim.api.nvim_del_augroup_by_name, 'fff_picker_focus') end -function M.close() - save_state_and_close() -end +function M.close() save_state_and_close() end --- Helper function to determine current file cache for deprioritization --- @param base_path string|nil Base path for relative path calculation @@ -3003,8 +2993,9 @@ end --- Open the file picker UI --- @param opts? {cwd?: string, title?: string, prompt?: string, max_results?: number, max_threads?: number, layout?: {width?: number|function, height?: number|function, prompt_position?: string|function, preview_position?: string|function, preview_size?: number|function}, renderer?: table, mode?: string, grep_config?: table, query?: string} Optional configuration to override defaults +---@return boolean true if the picker was opened, false if it was already active or initialization failed function M.open(opts) - if M.state.active then return end + if M.state.active then return false end M.state.selected_files = {} M.state.selected_items = {} @@ -3013,7 +3004,7 @@ function M.open(opts) M.state.grep_config = opts and opts.grep_config or nil local merged_config, base_path = initialize_picker(opts) - if not merged_config then return end + if not merged_config then return false end if base_path then M.change_indexing_directory(base_path) end diff --git a/plugin/fff.lua b/plugin/fff.lua index 48ec023f..abd2f42a 100644 --- a/plugin/fff.lua +++ b/plugin/fff.lua @@ -22,9 +22,7 @@ else }) end -vim.api.nvim_create_user_command('FFFResume', function() - require('fff').resume() -end, { +vim.api.nvim_create_user_command('FFFResume', function() require('fff').resume() end, { desc = 'Resume the last FFF picker (restores query, results, cursor, and mode)', }) From ccf9ad5d35ef1baeef94ef93eca105c178d08669 Mon Sep 17 00:00:00 2001 From: thuan do Date: Sun, 3 May 2026 08:27:32 +0700 Subject: [PATCH 4/9] chore: fix lint --- lua/fff/picker_ui.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index 6cf09567..a848da2c 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -2726,6 +2726,7 @@ end --- Falls back to opening a new find_files picker if nothing to resume. ---@param opts? table Optional config overrides for fallback open ---@return boolean +function M.resume_find_files(opts) if M.state.active then vim.notify('FFF: close the current picker before resuming', vim.log.levels.INFO) return false From efebb0083c65d4ea67de969bf51c9f67559638bc Mon Sep 17 00:00:00 2001 From: thuan do Date: Sun, 3 May 2026 08:59:14 +0700 Subject: [PATCH 5/9] chore: trigger pipeline From 9070cebd2a025ac32a690a7dc65120d2bdb8ac6c Mon Sep 17 00:00:00 2001 From: thuan do Date: Sun, 3 May 2026 02:00:36 +0000 Subject: [PATCH 6/9] chore: Update docs for - chore: trigger pipeline --- doc/fff.nvim.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index 98841b76..10a0b256 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -1,5 +1,5 @@ *fff.nvim.txt* - For Neovim >= 0.10.0 Last change: 2026 April 30 + For Neovim >= 0.10.0 Last change: 2026 May 03 ============================================================================== Table of Contents *fff.nvim-table-of-contents* From e656b5446f3d920f7612e16dcc16b1ec2ad98a3e Mon Sep 17 00:00:00 2001 From: thuan do Date: Mon, 4 May 2026 17:29:07 +0700 Subject: [PATCH 7/9] chore: address cmts --- lua/fff/picker_ui.lua | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index f851db08..870917db 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -2613,29 +2613,11 @@ end local function save_state_and_close() if not M.state.active then return end - -- Build the state snapshot - local snapshot = { - config = M.state.config, - mode = M.state.mode, - renderer = M.state.renderer, - grep_config = M.state.grep_config, - grep_mode = M.state.grep_mode, - query = M.state.query or '', - current_file_cache = M.state.current_file_cache, - pagination = vim.deepcopy(M.state.pagination), - cursor = M.state.cursor, - location = vim.deepcopy(M.state.location), - items = vim.deepcopy(M.state.items), - filtered_items = vim.deepcopy(M.state.filtered_items), - combo_visible = M.state.combo_visible, - combo_initial_cursor = M.state.combo_initial_cursor, - suggestion_items = vim.deepcopy(M.state.suggestion_items), - suggestion_source = M.state.suggestion_source, - selected_files = vim.deepcopy(M.state.selected_files), - selected_items = vim.deepcopy(M.state.selected_items), - } + -- Deep copy the full state to capture all data fields automatically (future-proof). + -- Window/buffer handles are also copied but are ignored during restore since UI is recreated. + local snapshot = vim.deepcopy(M.state) - -- Get base path from the file picker or current config + -- Capture the base_path from the Rust file indexer (not part of M.state) local fuzzy = require('fff.core').ensure_initialized() local ok, base_path = pcall(fuzzy.get_base_path) if ok and base_path then @@ -2680,14 +2662,8 @@ local function restore_from_state(state, source_label) -- Restore the saved base_path for the indexer if it differs from the current CWD if state.base_path then M.change_indexing_directory(state.base_path) end - -- Merge saved config with current defaults (to pick up any user config changes since last close) - local config = conf.get() - local merged_config = vim.tbl_deep_extend('force', config, { - renderer = state.renderer, - mode = state.mode, - grep_config = state.grep_config, - }) - M.state.config = merged_config + -- Use the saved config directly to restore the exact picker state + M.state.config = state.config if not M.create_ui() then vim.notify('FFF: failed to create picker UI for ' .. source_label, vim.log.levels.ERROR) @@ -2738,7 +2714,7 @@ local function restore_from_state(state, source_label) end end) - M.monitor_scan_progress(0) + -- Scan already completed when the original picker was open, no need to monitor again return true end From b51f0670cbbca21c95ab1a1cb8e546158cc2cbf2 Mon Sep 17 00:00:00 2001 From: thuan do Date: Fri, 15 May 2026 19:56:27 +0700 Subject: [PATCH 8/9] chore: not save the state when query is empty --- lua/fff/picker_ui.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/fff/picker_ui.lua b/lua/fff/picker_ui.lua index 870917db..c81a7f04 100644 --- a/lua/fff/picker_ui.lua +++ b/lua/fff/picker_ui.lua @@ -2611,6 +2611,11 @@ end --- Save the current picker state for later resume, then close. local function save_state_and_close() + if M.state.query == '' then + -- Don't save empty state to avoid confusion on resume + M.close_windows() + return + end if not M.state.active then return end -- Deep copy the full state to capture all data fields automatically (future-proof). From 4be3f1cf32897a3fccc092701d66609822456cde Mon Sep 17 00:00:00 2001 From: thuan do Date: Fri, 15 May 2026 12:56:53 +0000 Subject: [PATCH 9/9] chore: Update docs for - chore: not save the state when query is empty --- doc/fff.nvim.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index f7681227..fdcd30bb 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -1,5 +1,5 @@ *fff.nvim.txt* - For Neovim >= 0.10.0 Last change: 2026 May 04 + For Neovim >= 0.10.0 Last change: 2026 May 15 ============================================================================== Table of Contents *fff.nvim-table-of-contents*