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
1 change: 1 addition & 0 deletions lua/fff/picker_ui/layout_manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ function M.close()
S.current_file_cache = nil
S.location = nil
S.selected_files = {}
S.selected_file_order = {}
S.selected_items = {}
S.mode = nil
S.grep_config = nil
Expand Down
30 changes: 28 additions & 2 deletions lua/fff/picker_ui/picker_ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ local function restore_from_state(state, source_label)
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_file_order = vim.deepcopy(state.selected_file_order 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
Expand Down Expand Up @@ -266,6 +267,7 @@ function M.toggle_debug()
local current_grep_config = M.state.grep_config
local current_filtered_items = M.state.filtered_items
local current_selected_files = M.state.selected_files
local current_selected_file_order = M.state.selected_file_order
local current_selected_items = M.state.selected_items

M.close()
Expand All @@ -281,6 +283,7 @@ function M.toggle_debug()
M.state.grep_mode = current_grep_mode
M.state.filtered_items = current_filtered_items
M.state.selected_files = current_selected_files
M.state.selected_file_order = current_selected_file_order
M.state.selected_items = current_selected_items
M.render_list()
M.update_preview()
Expand Down Expand Up @@ -461,6 +464,12 @@ function M.select(action)

-- In grep mode (or when selecting a grep suggestion), derive location from the match item
local is_grep_item = mode == 'grep' or suggestion_source == 'grep'

-- When opening with selections active, open every selected file. The first
-- selected file is focused; the rest are added as listed buffers.
local selected_file_entries = {}
if action == 'edit' and not is_grep_item then selected_file_entries = picker_ui_state.get_selected_file_entries() end

if is_grep_item and item.line_number and item.line_number > 0 then
location = { line = item.line_number }
if item.col and item.col > 0 then
Expand All @@ -485,6 +494,10 @@ function M.select(action)
end
end

-- The focused file is the first selection, which may differ from the cursor
-- item; a cursor-derived location no longer applies, so drop it.
if #selected_file_entries > 0 and selected_file_entries[1].relative_path ~= item.relative_path then location = nil end

vim.cmd('stopinsert')
M.close()

Expand Down Expand Up @@ -514,15 +527,23 @@ function M.select(action)
end

if action == 'edit' then
-- Add every additional selection as a listed buffer before focusing one.
for _, entry in ipairs(selected_file_entries) do
local buf = vim.fn.bufadd(entry.edit_path)
vim.bo[buf].buflisted = true
end

local edit_path = #selected_file_entries > 0 and selected_file_entries[1].edit_path or relative_path

-- Hard guard against E1513 ("Cannot switch buffer. 'winfixbuf' is enabled"):
-- if the (post-hook) current window is pinned, fall back to :split.
local opened_via_split = false
if window_has_winfixbuf(vim.api.nvim_get_current_win()) then
vim.cmd('split ' .. vim.fn.fnameescape(relative_path))
vim.cmd('split ' .. vim.fn.fnameescape(edit_path))
opened_via_split = true
end

if not opened_via_split then vim.cmd('edit ' .. vim.fn.fnameescape(relative_path)) end
if not opened_via_split then vim.cmd('edit ' .. vim.fn.fnameescape(edit_path)) end
elseif action == 'split' then
vim.cmd('split ' .. vim.fn.fnameescape(relative_path))
elseif action == 'vsplit' then
Expand All @@ -541,6 +562,10 @@ function M.select(action)
-- Track in background thread (non-blocking, handled by Rust)
if mode == 'grep' then
pcall(fff.track_grep_query, query)
elseif #selected_file_entries > 0 then
for _, entry in ipairs(selected_file_entries) do
pcall(fff.track_query_completion, query, entry.relative_path)
end
else
pcall(fff.track_query_completion, query, item.relative_path)
end
Expand Down Expand Up @@ -700,6 +725,7 @@ function M.open(opts)
if M.state.active then return end

M.state.selected_files = {}
M.state.selected_file_order = {}
M.state.selected_items = {}
M.state.renderer = opts and opts.renderer or nil
M.state.mode = opts and opts.mode or nil
Expand Down
39 changes: 39 additions & 0 deletions lua/fff/picker_ui/picker_ui_state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ M.state = {

-- Selection state
selected_files = {},
selected_file_order = {},
selected_items = {},

-- Cross-mode suggestion state
Expand Down Expand Up @@ -142,6 +143,7 @@ function M.reset_state()
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
Expand All @@ -161,6 +163,7 @@ end
-- Clear all selections
function M.clear_selections()
M.state.selected_files = {}
M.state.selected_file_order = {}
M.state.selected_items = {}
end

Expand Down Expand Up @@ -210,12 +213,48 @@ function M.toggle_selection()
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
49 changes: 48 additions & 1 deletion tests/picker_dir_resolution_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ local function wait_for_scan(expected_dir, timeout_ms)
end

describe('picker find_files_in_dir path resolution (issue #389)', function()
local sandbox_root, target_dir, other_cwd, target_filename
local sandbox_root, target_dir, other_cwd, target_filename, second_filename

before_each(function()
sandbox_root = vim.fn.tempname()
Expand All @@ -66,6 +66,11 @@ describe('picker find_files_in_dir path resolution (issue #389)', function()
fd:write('-- issue #389 regression fixture\nreturn true\n')
fd:close()

second_filename = 'issue389_second.lua'
fd = assert(io.open(target_dir .. '/' .. second_filename, 'w'))
fd:write('-- issue #389 second fixture\nreturn true\n')
fd:close()

-- Clear the DirChanged autocmd that a previous test run (e.g. fff_core_spec)
-- may have installed. Without this, the :cd below triggers a scheduled
-- change_indexing_directory(other_cwd) that races with our explicit
Expand Down Expand Up @@ -127,6 +132,7 @@ describe('picker find_files_in_dir path resolution (issue #389)', function()
picker_ui.state.location = nil
picker_ui.state.suggestion_source = nil
picker_ui.state.selected_files = {}
picker_ui.state.selected_file_order = {}
picker_ui.state.selected_items = {}

picker_ui.select('edit')
Expand All @@ -147,4 +153,45 @@ describe('picker find_files_in_dir path resolution (issue #389)', function()
local actual = norm(bufname)
assert.are.equal(expected, actual)
end)

it(':edit loads all selected files as listed buffers', function()
assert.is_true(require('fff.core').change_indexing_directory(target_dir))
wait_for_scan(target_dir, 10000)

local items = file_picker.search_files('', nil, nil, nil, nil)
local selected = {}
for _, item in ipairs(items) do
if item.name == target_filename or item.name == second_filename then table.insert(selected, item) end
end
assert.are.equal(2, #selected, 'expected both fixture files in picker results')

picker_ui.state.active = true
picker_ui.state.filtered_items = items
picker_ui.state.cursor = 1
picker_ui.state.query = ''
picker_ui.state.mode = nil
picker_ui.state.location = nil
picker_ui.state.suggestion_source = nil
picker_ui.state.selected_files = {}
picker_ui.state.selected_file_order = {}
picker_ui.state.selected_items = {}

for _, item in ipairs(selected) do
picker_ui.state.selected_files[item.relative_path] = true
table.insert(picker_ui.state.selected_file_order, item.relative_path)
end

picker_ui.select('edit')

local expected_first = norm(target_dir .. '/' .. selected[1].name)
vim.wait(2000, function() return norm(vim.api.nvim_buf_get_name(0)) == expected_first end)
assert.are.equal(expected_first, norm(vim.api.nvim_buf_get_name(0)))

for _, item in ipairs(selected) do
local path = norm(target_dir .. '/' .. item.name)
local bufnr = vim.fn.bufnr(path)
assert.is_true(bufnr > 0, 'expected selected file to have a buffer: ' .. path)
assert.are.equal(1, vim.fn.buflisted(bufnr), 'expected selected file to be listed: ' .. path)
end
end)
end)
Loading