-
Notifications
You must be signed in to change notification settings - Fork 335
feat: resume last picker (#405) #438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should be using state from config here but it's okay |
||
| 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) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we do this? Can we enforce recreation it if the user simply wants to get previous results?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This's used to prevent user from open a new session while they are in an active one. Seems like an unrealistic scenario. |
||
| 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() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we put this to a one object