diff --git a/lua/fff/main.lua b/lua/fff/main.lua index 24241576..f8d48945 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -6,19 +6,27 @@ 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.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 + picker_ui.resume_find_files(opts) + 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.picker_ui') if not picker_ok then @@ -26,6 +34,11 @@ function M.live_grep(opts) return end + if opts and opts.resume then + picker_ui.resume_live_grep(opts) + return + end + local config = require('fff.conf').get() local grep_renderer = require('fff.picker_ui.grep_renderer') @@ -427,6 +440,18 @@ end --- @return boolean `true` if successful, `false` otherwise function M.change_indexing_directory(new_path) return require('fff.core').change_indexing_directory(new_path) 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.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 + -- Strip wrapper punctuation that frequently surrounds paths in prose: leading -- markdown-link `[`, parens `(`, brackets `<`, quotes; trailing sentence -- punctuation. We additionally truncate at the first closing wrapper so a diff --git a/lua/fff/picker_ui/layout_manager.lua b/lua/fff/picker_ui/layout_manager.lua index f847f953..541dcc4a 100644 --- a/lua/fff/picker_ui/layout_manager.lua +++ b/lua/fff/picker_ui/layout_manager.lua @@ -102,9 +102,7 @@ function M.close() for _, buf in ipairs(buffers) do if buf and vim.api.nvim_buf_is_valid(buf) then vim.api.nvim_buf_clear_namespace(buf, -1, 0, -1) - if buf == S.preview_buf then preview.clear_buffer(buf) end - vim.api.nvim_buf_delete(buf, { force = true }) end end diff --git a/lua/fff/picker_ui/picker_ui.lua b/lua/fff/picker_ui/picker_ui.lua index fce04d8c..f2659bc4 100644 --- a/lua/fff/picker_ui/picker_ui.lua +++ b/lua/fff/picker_ui/picker_ui.lua @@ -83,7 +83,189 @@ M.scroll_to_bottom = renderer.scroll_to_bottom -- Wire layout_manager module (relayout, close) layout_manager.init(M) M.relayout = layout_manager.relayout -M.close = layout_manager.close + +-- Resume state: saved snapshots of closed pickers for the resume feature. +--- @type table|nil Saved state from last file picker (find_files) session +local last_file_picker_state = nil +--- @type table|nil Saved state from last grep session +local last_grep_picker_state = nil +--- @type string|nil 'files' or 'grep' — which mode was most recently closed +local last_closed_mode = nil + +--- Save the current picker state for later resume, then close. +function M.close() + if M.state.query == '' then + layout_manager.close() + return + end + if not M.state.active then return end + + local snapshot = vim.deepcopy(M.state) + + 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 + + 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 + + layout_manager.close() +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 require('fff.core').change_indexing_directory(state.base_path) end + + -- 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) + 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) + + return true +end + +---@return boolean|nil 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) + 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|nil +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.picker_ui.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.toggle_debug() local config_changed = conf.toggle_debug() diff --git a/plugin/fff.lua b/plugin/fff.lua index fb14fcce..428746c8 100644 --- a/plugin/fff.lua +++ b/plugin/fff.lua @@ -22,6 +22,10 @@ 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