local M = {} local utils = require('fff.utils') local canonicalize_fff_path = utils.canonicalize_fff_path -- State structure definitions M.state = { -- UI state active = false, layout = nil, input_win = nil, input_buf = nil, list_win = nil, list_buf = nil, file_info_win = nil, file_info_buf = nil, preview_win = nil, preview_buf = nil, preview_visible = false, -- Data state items = {}, filtered_items = {}, cursor = 1, top = 1, query = '', line_to_item = {}, item_to_lines = {}, last_render_ctx = nil, location = nil, -- Cursor index to restore after the next search completes (set on resume). -- Lets the re-search run for fresh results while keeping the saved position. pending_restore_cursor = nil, -- History cycling state history_offset = nil, next_search_force_combo_boost = false, -- Combo state combo_visible = true, combo_initial_cursor = nil, -- History cycling state (tracked alongside combo state) updating_from_history = false, -- Pagination state pagination = { page_index = 0, page_size = 20, total_matched = 0, prefetch_margin = 5, grep_file_offsets = {}, grep_next_file_offset = 0, }, -- Configuration and mode config = nil, renderer = nil, mode = nil, grep_mode = 'plain', grep_config = nil, grep_regex_fallback_error = nil, -- Selection state selected_files = {}, selected_file_order = {}, selected_items = {}, -- Cross-mode suggestion state suggestion_items = nil, suggestion_source = nil, -- Preview state last_preview_file = nil, last_preview_location = nil, preview_timer = nil, preview_debounce_ms = 10, -- Misc ns_id = nil, last_status_info = nil, restore_paste = false, current_file_cache = nil, } -- Helper function to generate grep item keys local function grep_item_key(item) return string.format('%s:%d:%d', item.relative_path, item.line_number or 1, item.col or 0) end -- Reset history-related state function M.reset_history_state() M.state.history_offset = nil M.state.next_search_force_combo_boost = false M.state.combo_visible = true M.state.combo_initial_cursor = nil M.state.updating_from_history = false end -- Complete state reset (called on close) function M.reset_state() M.state.active = false M.state.layout = nil M.state.input_win = nil M.state.input_buf = nil M.state.list_win = nil M.state.list_buf = nil M.state.file_info_win = nil M.state.file_info_buf = nil M.state.preview_win = nil M.state.preview_buf = nil M.state.preview_visible = false M.state.items = {} M.state.filtered_items = {} M.state.cursor = 1 M.state.top = 1 M.state.query = '' M.state.line_to_item = {} M.state.item_to_lines = {} M.state.last_render_ctx = nil M.state.location = nil M.state.pending_restore_cursor = nil M.reset_history_state() M.state.pagination = { page_index = 0, page_size = 20, total_matched = 0, prefetch_margin = 5, grep_file_offsets = {}, grep_next_file_offset = 0, } M.state.config = nil M.state.renderer = nil M.state.mode = nil M.state.grep_mode = 'plain' M.state.grep_config = nil M.state.grep_regex_fallback_error = nil M.state.selected_files = {} M.state.selected_file_order = {} M.state.selected_items = {} M.state.suggestion_items = nil M.state.suggestion_source = nil M.state.last_preview_file = nil M.state.last_preview_location = nil M.state.preview_timer = nil M.state.current_file_cache = nil M.state.ns_id = nil M.state.last_status_info = nil M.state.restore_paste = false M.state.combo_visible = true M.state.combo_initial_cursor = nil M.state.updating_from_history = false end -- Clear all selections function M.clear_selections() M.state.selected_files = {} M.state.selected_file_order = {} M.state.selected_items = {} end -- Get selected items for quickfix list function M.get_selected_items() local selected = {} if M.state.mode == 'grep' then -- Grep mode: return selected items for _, item in pairs(M.state.selected_items) do table.insert(selected, item) end else -- Normal file mode: return selected files for relative_path, _ in pairs(M.state.selected_files) do local abs_path = canonicalize_fff_path(relative_path) if abs_path then table.insert(selected, { relative_path = relative_path }) end end end return selected end -- Toggle item selection (called from keymaps via M.toggle_select) function M.toggle_selection() if not M.state.active then return end local items = M.state.filtered_items if #items == 0 or M.state.cursor > #items then return end local item = items[M.state.cursor] if not item or not item.relative_path then return end local was_selected if M.state.mode == 'grep' then -- Per-occurrence selection for grep mode local key = grep_item_key(item) was_selected = M.state.selected_items[key] ~= nil if was_selected then M.state.selected_items[key] = nil else M.state.selected_items[key] = item end else -- Per-file selection for normal file mode was_selected = M.state.selected_files[item.relative_path] if was_selected then M.state.selected_files[item.relative_path] = nil M.state.selected_file_order = vim.tbl_filter( function(path) return path ~= item.relative_path end, M.state.selected_file_order ) else M.state.selected_files[item.relative_path] = true table.insert(M.state.selected_file_order, item.relative_path) end end return was_selected end -- Ordered, deduped selected file entries for opening (file mode only). -- Each entry has the raw fff relative_path and a cwd-relative edit_path. function M.get_selected_file_entries() if not next(M.state.selected_files) then return {} end local entries = {} local seen = {} local function add(relative_path) if not relative_path or seen[relative_path] or not M.state.selected_files[relative_path] then return end local abs_path = canonicalize_fff_path(relative_path) if not abs_path then return end seen[relative_path] = true table.insert(entries, { relative_path = relative_path, edit_path = vim.fn.fnamemodify(abs_path, ':.'), }) end for _, relative_path in ipairs(M.state.selected_file_order) do add(relative_path) end for relative_path, _ in pairs(M.state.selected_files) do add(relative_path) end return entries end return M