Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions lua/fff/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,39 @@ 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
vim.notify('Failed to load picker UI: ' .. picker_ui, vim.log.levels.ERROR)
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')

Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions lua/fff/picker_ui/layout_manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
184 changes: 183 additions & 1 deletion lua/fff/picker_ui/picker_ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Owner

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

--- @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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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()
Expand Down
4 changes: 4 additions & 0 deletions plugin/fff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading