local M = {} local list_separator = require('fff.list_separator') local preview = require('fff.file_picker.preview') local layout = require('fff.layout') local picker_ui_state = require('fff.picker_ui.picker_ui_state') -- Parent module reference (set by picker_ui.lua during initialization). ---@type table local P = nil function M.init(parent_module) P = parent_module end local S = picker_ui_state.state local function get_prompt_position() return layout.resolve_prompt_position(S.config) end --- After cursor moves, decide whether the combo separator should hide. local function maybe_hide_combo_separator() if not (S.combo_initial_cursor and S.combo_visible) then return end local distance_past = S.cursor - S.combo_initial_cursor if distance_past == 0 then return end local half_page = math.floor(S.pagination.page_size * 0.5) if math.abs(distance_past) <= half_page then return end S.combo_visible = false list_separator.hide() P.render_list() if get_prompt_position() == 'bottom' then P.scroll_to_bottom() end end function M.wrap_to_first() if S.pagination.page_index == 0 then S.cursor = 1 return true end if S.mode ~= 'grep' then return P.load_page_at_index(0, function() S.cursor = 1 end) end if S.pagination.grep_file_offsets[1] ~= nil then return P.load_page_at_index(0, function() S.cursor = 1 end) end S.cursor = 1 return true end function M.wrap_to_last() local page_size = S.pagination.page_size if page_size == 0 then return false end if S.mode ~= 'grep' then local total = S.pagination.total_matched if total == 0 then return false end local max_page_index = math.max(0, math.ceil(total / page_size) - 1) if S.pagination.page_index == max_page_index then S.cursor = #S.filtered_items return true end return P.load_page_at_index(max_page_index, function(result_count) S.cursor = result_count end) end S.cursor = #S.filtered_items return true end function M.move_up() if not P.state.active then return end if #S.filtered_items == 0 then return end local prompt_position = get_prompt_position() local items_count = #S.filtered_items local old_cursor = S.cursor local wrap_around = S.config and S.config.wrap_around or false if prompt_position == 'bottom' then local near_bottom = S.cursor >= (items_count - S.pagination.prefetch_margin) local at_last_item = S.cursor >= items_count if near_bottom and at_last_item then local page_size = S.pagination.page_size local has_more = false if page_size > 0 then if S.mode == 'grep' then has_more = S.pagination.grep_next_file_offset > 0 else local max_page = math.max(0, math.ceil(S.pagination.total_matched / page_size) - 1) has_more = S.pagination.page_index < max_page end end if has_more then P.load_next_page() return elseif wrap_around then M.wrap_to_first() end else S.cursor = math.min(S.cursor + 1, items_count) end else if S.cursor <= S.pagination.prefetch_margin + 1 and S.cursor <= 1 then if S.pagination.page_index > 0 then vim.schedule(P.load_previous_page) return elseif wrap_around then M.wrap_to_last() end else S.cursor = math.max(S.cursor - 1, 1) end end if not P.render_after_cursor_move(old_cursor) then return end P.update_status() pcall(vim.cmd, 'redraw') P.update_preview_debounced() maybe_hide_combo_separator() end function M.move_down() if not P.state.active then return end if #S.filtered_items == 0 then return end local prompt_position = get_prompt_position() local items_count = #S.filtered_items local old_cursor = S.cursor local wrap_around = S.config and S.config.wrap_around or false if prompt_position == 'bottom' then if S.cursor <= S.pagination.prefetch_margin + 1 and S.cursor <= 1 then if S.pagination.page_index > 0 then vim.schedule(P.load_previous_page) return elseif wrap_around then M.wrap_to_last() end else S.cursor = math.max(S.cursor - 1, 1) end else local near_bottom = S.cursor >= (items_count - S.pagination.prefetch_margin) local at_last_item = S.cursor >= items_count if near_bottom and at_last_item then local page_size = S.pagination.page_size local has_more = false if page_size > 0 then if S.mode == 'grep' then has_more = S.pagination.grep_next_file_offset > 0 else local max_page = math.max(0, math.ceil(S.pagination.total_matched / page_size) - 1) has_more = S.pagination.page_index < max_page end end if has_more then P.load_next_page() return elseif wrap_around then M.wrap_to_first() end else S.cursor = math.min(S.cursor + 1, items_count) end end if not P.render_after_cursor_move(old_cursor) then return end P.update_status() pcall(vim.cmd, 'redraw') P.update_preview_debounced() maybe_hide_combo_separator() end function M.scroll_preview_up() if not P.state.active or not S.preview_win then return end local win_height = vim.api.nvim_win_get_height(S.preview_win) local scroll_lines = math.floor(win_height / 2) preview.scroll(-scroll_lines) end function M.scroll_preview_down() if not P.state.active or not S.preview_win then return end local win_height = vim.api.nvim_win_get_height(S.preview_win) local scroll_lines = math.floor(win_height / 2) preview.scroll(scroll_lines) end -- Helper function to eliminate UI update redundancy local function update_ui_after_jump(old_cursor) if not P.render_after_cursor_move(old_cursor) then return false end P.update_status() pcall(vim.cmd, 'redraw') P.update_preview_debounced() return true end function M.grep_jump_to_next_file() if not P.state.active or S.mode ~= 'grep' then return end local items = S.filtered_items if not items or #items == 0 then return end local old_cursor = S.cursor local current_path = items[S.cursor] and items[S.cursor].relative_path for i = S.cursor + 1, #items do if items[i].relative_path ~= current_path then S.cursor = i update_ui_after_jump(old_cursor) return end end if P.load_next_page and P.load_next_page() then local new_items = S.filtered_items if new_items and #new_items > 0 then local idx = 1 if new_items[1].relative_path == current_path then for i = 2, #new_items do if new_items[i].relative_path ~= current_path then idx = i break end end end S.cursor = idx update_ui_after_jump(old_cursor) end end end function M.grep_jump_to_prev_file() if not P.state.active or S.mode ~= 'grep' then return end local items = S.filtered_items if not items or #items == 0 then return end local old_cursor = S.cursor local current_path = items[S.cursor] and items[S.cursor].relative_path for i = S.cursor - 1, 1, -1 do if items[i].relative_path ~= current_path then local target_idx = i while target_idx > 1 and items[target_idx - 1].relative_path == items[i].relative_path do target_idx = target_idx - 1 end S.cursor = target_idx update_ui_after_jump(old_cursor) return end end if P.load_previous_page and P.load_previous_page() then local new_items = S.filtered_items if not new_items or #new_items == 0 then return end local target_path = nil for i = #new_items, 1, -1 do if new_items[i].relative_path ~= current_path then target_path = new_items[i].relative_path break end end target_path = target_path or new_items[#new_items].relative_path local first = 1 for i = 1, #new_items do if new_items[i].relative_path == target_path then first = i break end end S.cursor = first update_ui_after_jump(old_cursor) end end return M