Skip to content

Commit c5346f4

Browse files
committed
open all multi-selected files on cr
1 parent fce72fa commit c5346f4

4 files changed

Lines changed: 116 additions & 3 deletions

File tree

lua/fff/picker_ui/layout_manager.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ function M.close()
131131
S.current_file_cache = nil
132132
S.location = nil
133133
S.selected_files = {}
134+
S.selected_file_order = {}
134135
S.selected_items = {}
135136
S.mode = nil
136137
S.grep_config = nil

lua/fff/picker_ui/picker_ui.lua

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ local function restore_from_state(state, source_label)
133133
M.state.grep_config = state.grep_config
134134
M.state.grep_mode = state.grep_mode
135135
M.state.selected_files = vim.deepcopy(state.selected_files or {})
136+
M.state.selected_file_order = vim.deepcopy(state.selected_file_order or {})
136137
M.state.selected_items = vim.deepcopy(state.selected_items or {})
137138

138139
-- Restore the saved base_path for the indexer if it differs from the current CWD
@@ -266,6 +267,7 @@ function M.toggle_debug()
266267
local current_grep_config = M.state.grep_config
267268
local current_filtered_items = M.state.filtered_items
268269
local current_selected_files = M.state.selected_files
270+
local current_selected_file_order = M.state.selected_file_order
269271
local current_selected_items = M.state.selected_items
270272

271273
M.close()
@@ -281,6 +283,7 @@ function M.toggle_debug()
281283
M.state.grep_mode = current_grep_mode
282284
M.state.filtered_items = current_filtered_items
283285
M.state.selected_files = current_selected_files
286+
M.state.selected_file_order = current_selected_file_order
284287
M.state.selected_items = current_selected_items
285288
M.render_list()
286289
M.update_preview()
@@ -461,6 +464,12 @@ function M.select(action)
461464

462465
-- In grep mode (or when selecting a grep suggestion), derive location from the match item
463466
local is_grep_item = mode == 'grep' or suggestion_source == 'grep'
467+
468+
-- When opening with selections active, open every selected file. The first
469+
-- selected file is focused; the rest are added as listed buffers.
470+
local selected_file_entries = {}
471+
if action == 'edit' and not is_grep_item then selected_file_entries = picker_ui_state.get_selected_file_entries() end
472+
464473
if is_grep_item and item.line_number and item.line_number > 0 then
465474
location = { line = item.line_number }
466475
if item.col and item.col > 0 then
@@ -485,6 +494,10 @@ function M.select(action)
485494
end
486495
end
487496

497+
-- The focused file is the first selection, which may differ from the cursor
498+
-- item; a cursor-derived location no longer applies, so drop it.
499+
if #selected_file_entries > 0 and selected_file_entries[1].relative_path ~= item.relative_path then location = nil end
500+
488501
vim.cmd('stopinsert')
489502
M.close()
490503

@@ -514,15 +527,23 @@ function M.select(action)
514527
end
515528

516529
if action == 'edit' then
530+
-- Add every additional selection as a listed buffer before focusing one.
531+
for _, entry in ipairs(selected_file_entries) do
532+
local buf = vim.fn.bufadd(entry.edit_path)
533+
vim.bo[buf].buflisted = true
534+
end
535+
536+
local edit_path = #selected_file_entries > 0 and selected_file_entries[1].edit_path or relative_path
537+
517538
-- Hard guard against E1513 ("Cannot switch buffer. 'winfixbuf' is enabled"):
518539
-- if the (post-hook) current window is pinned, fall back to :split.
519540
local opened_via_split = false
520541
if window_has_winfixbuf(vim.api.nvim_get_current_win()) then
521-
vim.cmd('split ' .. vim.fn.fnameescape(relative_path))
542+
vim.cmd('split ' .. vim.fn.fnameescape(edit_path))
522543
opened_via_split = true
523544
end
524545

525-
if not opened_via_split then vim.cmd('edit ' .. vim.fn.fnameescape(relative_path)) end
546+
if not opened_via_split then vim.cmd('edit ' .. vim.fn.fnameescape(edit_path)) end
526547
elseif action == 'split' then
527548
vim.cmd('split ' .. vim.fn.fnameescape(relative_path))
528549
elseif action == 'vsplit' then
@@ -541,6 +562,10 @@ function M.select(action)
541562
-- Track in background thread (non-blocking, handled by Rust)
542563
if mode == 'grep' then
543564
pcall(fff.track_grep_query, query)
565+
elseif #selected_file_entries > 0 then
566+
for _, entry in ipairs(selected_file_entries) do
567+
pcall(fff.track_query_completion, query, entry.relative_path)
568+
end
544569
else
545570
pcall(fff.track_query_completion, query, item.relative_path)
546571
end
@@ -700,6 +725,7 @@ function M.open(opts)
700725
if M.state.active then return end
701726

702727
M.state.selected_files = {}
728+
M.state.selected_file_order = {}
703729
M.state.selected_items = {}
704730
M.state.renderer = opts and opts.renderer or nil
705731
M.state.mode = opts and opts.mode or nil

lua/fff/picker_ui/picker_ui_state.lua

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ M.state = {
6565

6666
-- Selection state
6767
selected_files = {},
68+
selected_file_order = {},
6869
selected_items = {},
6970

7071
-- Cross-mode suggestion state
@@ -142,6 +143,7 @@ function M.reset_state()
142143
M.state.grep_regex_fallback_error = nil
143144

144145
M.state.selected_files = {}
146+
M.state.selected_file_order = {}
145147
M.state.selected_items = {}
146148
M.state.suggestion_items = nil
147149
M.state.suggestion_source = nil
@@ -161,6 +163,7 @@ end
161163
-- Clear all selections
162164
function M.clear_selections()
163165
M.state.selected_files = {}
166+
M.state.selected_file_order = {}
164167
M.state.selected_items = {}
165168
end
166169

@@ -210,12 +213,48 @@ function M.toggle_selection()
210213
was_selected = M.state.selected_files[item.relative_path]
211214
if was_selected then
212215
M.state.selected_files[item.relative_path] = nil
216+
M.state.selected_file_order = vim.tbl_filter(
217+
function(path) return path ~= item.relative_path end,
218+
M.state.selected_file_order
219+
)
213220
else
214221
M.state.selected_files[item.relative_path] = true
222+
table.insert(M.state.selected_file_order, item.relative_path)
215223
end
216224
end
217225

218226
return was_selected
219227
end
220228

229+
-- Ordered, deduped selected file entries for opening (file mode only).
230+
-- Each entry has the raw fff relative_path and a cwd-relative edit_path.
231+
function M.get_selected_file_entries()
232+
if not next(M.state.selected_files) then return {} end
233+
234+
local entries = {}
235+
local seen = {}
236+
237+
local function add(relative_path)
238+
if not relative_path or seen[relative_path] or not M.state.selected_files[relative_path] then return end
239+
240+
local abs_path = canonicalize_fff_path(relative_path)
241+
if not abs_path then return end
242+
243+
seen[relative_path] = true
244+
table.insert(entries, {
245+
relative_path = relative_path,
246+
edit_path = vim.fn.fnamemodify(abs_path, ':.'),
247+
})
248+
end
249+
250+
for _, relative_path in ipairs(M.state.selected_file_order) do
251+
add(relative_path)
252+
end
253+
for relative_path, _ in pairs(M.state.selected_files) do
254+
add(relative_path)
255+
end
256+
257+
return entries
258+
end
259+
221260
return M

tests/picker_dir_resolution_spec.lua

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ local function wait_for_scan(expected_dir, timeout_ms)
5252
end
5353

5454
describe('picker find_files_in_dir path resolution (issue #389)', function()
55-
local sandbox_root, target_dir, other_cwd, target_filename
55+
local sandbox_root, target_dir, other_cwd, target_filename, second_filename
5656

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

69+
second_filename = 'issue389_second.lua'
70+
fd = assert(io.open(target_dir .. '/' .. second_filename, 'w'))
71+
fd:write('-- issue #389 second fixture\nreturn true\n')
72+
fd:close()
73+
6974
-- Clear the DirChanged autocmd that a previous test run (e.g. fff_core_spec)
7075
-- may have installed. Without this, the :cd below triggers a scheduled
7176
-- change_indexing_directory(other_cwd) that races with our explicit
@@ -127,6 +132,7 @@ describe('picker find_files_in_dir path resolution (issue #389)', function()
127132
picker_ui.state.location = nil
128133
picker_ui.state.suggestion_source = nil
129134
picker_ui.state.selected_files = {}
135+
picker_ui.state.selected_file_order = {}
130136
picker_ui.state.selected_items = {}
131137

132138
picker_ui.select('edit')
@@ -147,4 +153,45 @@ describe('picker find_files_in_dir path resolution (issue #389)', function()
147153
local actual = norm(bufname)
148154
assert.are.equal(expected, actual)
149155
end)
156+
157+
it(':edit loads all selected files as listed buffers', function()
158+
assert.is_true(require('fff.core').change_indexing_directory(target_dir))
159+
wait_for_scan(target_dir, 10000)
160+
161+
local items = file_picker.search_files('', nil, nil, nil, nil)
162+
local selected = {}
163+
for _, item in ipairs(items) do
164+
if item.name == target_filename or item.name == second_filename then table.insert(selected, item) end
165+
end
166+
assert.are.equal(2, #selected, 'expected both fixture files in picker results')
167+
168+
picker_ui.state.active = true
169+
picker_ui.state.filtered_items = items
170+
picker_ui.state.cursor = 1
171+
picker_ui.state.query = ''
172+
picker_ui.state.mode = nil
173+
picker_ui.state.location = nil
174+
picker_ui.state.suggestion_source = nil
175+
picker_ui.state.selected_files = {}
176+
picker_ui.state.selected_file_order = {}
177+
picker_ui.state.selected_items = {}
178+
179+
for _, item in ipairs(selected) do
180+
picker_ui.state.selected_files[item.relative_path] = true
181+
table.insert(picker_ui.state.selected_file_order, item.relative_path)
182+
end
183+
184+
picker_ui.select('edit')
185+
186+
local expected_first = norm(target_dir .. '/' .. selected[1].name)
187+
vim.wait(2000, function() return norm(vim.api.nvim_buf_get_name(0)) == expected_first end)
188+
assert.are.equal(expected_first, norm(vim.api.nvim_buf_get_name(0)))
189+
190+
for _, item in ipairs(selected) do
191+
local path = norm(target_dir .. '/' .. item.name)
192+
local bufnr = vim.fn.bufnr(path)
193+
assert.is_true(bufnr > 0, 'expected selected file to have a buffer: ' .. path)
194+
assert.are.equal(1, vim.fn.buflisted(bufnr), 'expected selected file to be listed: ' .. path)
195+
end
196+
end)
150197
end)

0 commit comments

Comments
 (0)