Skip to content

Commit 0e75b1c

Browse files
committed
feat: Reuse existing open buffers for preview
1 parent 4a1297d commit 0e75b1c

5 files changed

Lines changed: 123 additions & 97 deletions

File tree

lua/fff/file_picker/image.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,11 @@ end
100100
--- @param bufnr number Buffer number to display in
101101
--- @param max_width number Maximum width in characters
102102
--- @param max_height number Maximum height in characters
103+
--- @return boolean
103104
function M.display_image(file_path, bufnr, max_width, max_height)
104105
max_width = max_width or 80
105106
max_height = max_height or 24
107+
vim.api.nvim_buf_set_option(bufnr, 'number', false)
106108

107109
local ok, snacks = pcall(require, 'snacks')
108110
if ok and snacks.image and snacks.image.buf then
@@ -125,10 +127,11 @@ function M.display_image(file_path, bufnr, max_width, max_height)
125127
M.display_image_info(file_path, bufnr, 'Snacks.nvim failed: ' .. tostring(placement or 'unknown error'))
126128
end
127129
end)
128-
return
130+
return true
129131
end
130132

131133
M.display_image_info(file_path, bufnr, 'Snacks.nvim not available')
134+
return false
132135
end
133136

134137
--- Display image information when image display fails

lua/fff/file_picker/preview.lua

Lines changed: 116 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,65 @@ local function get_image()
99
return image
1010
end
1111

12-
-- Helper function to safely set buffer lines
13-
local function safe_set_buffer_lines(bufnr, start, end_line, strict_indexing, lines)
14-
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return false end
12+
-- Set lines in preview buffer (simpler approach)
13+
local function set_buffer_lines(bufnr, lines)
14+
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end
1515

16-
-- Make buffer modifiable temporarily
17-
local was_modifiable = vim.api.nvim_buf_get_option(bufnr, 'modifiable')
1816
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
17+
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
18+
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
19+
end
20+
21+
-- Check if file is already loaded in a buffer
22+
local function find_existing_buffer(file_path)
23+
local abs_path = vim.fn.resolve(vim.fn.fnamemodify(file_path, ':p'))
24+
25+
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
26+
if vim.api.nvim_buf_is_loaded(bufnr) then
27+
local buf_name = vim.api.nvim_buf_get_name(bufnr)
28+
if buf_name ~= '' then
29+
local buf_path = vim.fn.resolve(vim.fn.fnamemodify(buf_name, ':p'))
30+
if buf_path == abs_path then return bufnr end
31+
end
32+
end
33+
end
34+
return nil
35+
end
1936

20-
-- Set lines
21-
local ok, err = pcall(vim.api.nvim_buf_set_lines, bufnr, start, end_line, strict_indexing, lines)
37+
local function read_file_fast(file_path, max_lines)
38+
local fd = vim.uv.fs_open(file_path, 'r', 0x666)
39+
if not fd then return nil end
2240

23-
-- Restore modifiable state
24-
vim.api.nvim_buf_set_option(bufnr, 'modifiable', was_modifiable)
41+
local stat = vim.uv.fs_fstat(fd)
42+
if not stat then
43+
vim.uv.fs_close(fd)
44+
return nil
45+
end
2546

26-
if not ok then
27-
vim.notify('Error setting buffer lines: ' .. err, vim.log.levels.WARN)
28-
return false
47+
local data = vim.uv.fs_read(fd, stat.size, 0)
48+
vim.uv.fs_close(fd)
49+
50+
if not data then return nil end
51+
52+
local lines = vim.split(data, '\n', { plain = true })
53+
if max_lines and #lines > max_lines then
54+
local limited_lines = {}
55+
for i = 1, max_lines do
56+
table.insert(limited_lines, lines[i])
57+
end
58+
lines = limited_lines
2959
end
3060

61+
return lines
62+
end
63+
64+
local function copy_buffer_content(source_bufnr, target_bufnr)
65+
local lines = vim.api.nvim_buf_get_lines(source_bufnr, 0, -1, false)
66+
set_buffer_lines(target_bufnr, lines)
67+
68+
local source_ft = vim.api.nvim_buf_get_option(source_bufnr, 'filetype')
69+
if source_ft ~= '' then vim.api.nvim_buf_set_option(target_bufnr, 'filetype', source_ft) end
70+
3171
return true
3272
end
3373

@@ -289,33 +329,55 @@ function M.preview_file(file_path, bufnr)
289329
'',
290330
'Use a text editor to view this file.',
291331
}
292-
safe_set_buffer_lines(bufnr, 0, -1, false, lines)
332+
set_buffer_lines(bufnr, lines)
293333
return true
294334
end
295335

336+
-- if the buffer is already opened for this file we reuse the buffer directly
337+
local existing_bufnr = find_existing_buffer(file_path)
338+
339+
if existing_bufnr then
340+
local success = copy_buffer_content(existing_bufnr, bufnr)
341+
if success then
342+
local file_config = M.get_file_config(file_path)
343+
344+
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
345+
vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
346+
vim.api.nvim_buf_set_option(bufnr, 'buftype', 'nofile')
347+
vim.api.nvim_buf_set_option(bufnr, 'wrap', file_config.wrap_lines or M.config.wrap_lines)
348+
vim.api.nvim_buf_set_option(bufnr, 'number', M.config.line_numbers)
349+
350+
local content = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
351+
M.state.content_height = #content
352+
M.state.scroll_offset = 0
353+
354+
return true
355+
end
356+
end
357+
358+
-- Fallback: Manual file reading with fast UV operations
296359
local file_config = M.get_file_config(file_path)
297360

298361
local content
299362
if file_config.tail_lines then
300363
content = M.read_file_tail(file_path, file_config.tail_lines)
301-
if content then
302-
-- Add virtual text showing tail lines are showed
303-
end
304364
else
305-
content = M.read_file_content(file_path, M.config.max_lines)
365+
content = read_file_fast(file_path, M.config.max_lines)
366+
if not content then content = M.read_file_content(file_path, M.config.max_lines) end
306367
end
307368

308369
if not content then return false end
309370

310-
safe_set_buffer_lines(bufnr, 0, -1, false, content)
371+
set_buffer_lines(bufnr, content)
311372

312373
vim.api.nvim_buf_set_option(bufnr, 'filetype', info.filetype)
313374
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
314375
vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
315376
vim.api.nvim_buf_set_option(bufnr, 'buftype', 'nofile')
316377
vim.api.nvim_buf_set_option(bufnr, 'wrap', file_config.wrap_lines or M.config.wrap_lines)
378+
vim.api.nvim_buf_set_option(bufnr, 'number', M.config.line_numbers)
317379

318-
M.state.content_height = content
380+
M.state.content_height = #content
319381
M.state.scroll_offset = 0
320382

321383
return true
@@ -324,12 +386,10 @@ end
324386
--- Preview a binary file
325387
--- @param file_path string Path to the file
326388
--- @param bufnr number Buffer number for preview
327-
--- @param info table File information
328-
--- @param file table | nil Optional file information from search results for debug info
329389
--- @return boolean Success status
330-
function M.preview_binary_file(file_path, bufnr, info, file)
390+
function M.preview_binary_file(file_path, bufnr)
331391
local lines = {}
332-
392+
local info = M.get_file_info(file_path)
333393
table.insert(lines, '⚠ Binary File Detected')
334394
table.insert(lines, '')
335395
table.insert(lines, 'This file contains binary data and cannot be displayed as text.')
@@ -363,7 +423,7 @@ function M.preview_binary_file(file_path, bufnr, info, file)
363423
table.insert(lines, 'Use a hex editor or appropriate application to view this file.')
364424
end
365425

366-
safe_set_buffer_lines(bufnr, 0, -1, false, lines)
426+
set_buffer_lines(bufnr, lines)
367427
vim.api.nvim_buf_set_option(bufnr, 'filetype', 'text')
368428
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
369429
vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
@@ -377,95 +437,67 @@ end
377437
function M.get_file_config(file_path)
378438
if not M.config or not M.config.filetypes then return {} end
379439

380-
-- Get filetype using Neovim's built-in filetype detection
381440
local filetype = vim.filetype.match({ filename = file_path }) or 'text'
382-
383-
-- Return filetype-specific configuration
384441
return M.config.filetypes[filetype] or {}
385442
end
386443

387-
--- Main preview function
388444
--- @param file_path string Path to the file or directory
389445
--- @param bufnr number Buffer number for preview
390-
--- @param file table Optional file information from search results for debug info
391-
--- @return boolean Success status
392-
function M.preview(file_path, bufnr, file)
446+
--- @return boolean if the preview was successful
447+
function M.preview(file_path, bufnr)
393448
if not file_path or file_path == '' then
394-
M.clear_buffer_completely(bufnr)
395-
safe_set_buffer_lines(bufnr, 0, -1, false, { 'No file selected' })
449+
M.clear_buffer(bufnr)
450+
set_buffer_lines(bufnr, { 'No file selected' })
396451
return false
397452
end
398453

399454
M.state.current_file = file_path
400455
M.state.bufnr = bufnr
401456

402-
local stat = vim.uv.fs_stat(file_path)
403-
if not stat then
404-
M.clear_buffer_completely(bufnr)
405-
safe_set_buffer_lines(bufnr, 0, -1, false, {
406-
'File not found or inaccessible:',
407-
file_path,
408-
})
409-
return false
410-
end
411-
412-
-- Clear buffer completely before switching content types
413-
M.clear_buffer_completely(bufnr)
457+
M.clear_buffer(bufnr)
414458

415-
-- Handle different file types
416-
if stat.type == 'directory' then
417-
-- This is a file search tool, directories shouldn't be previewed
418-
safe_set_buffer_lines(bufnr, 0, -1, false, {
419-
'Directory Preview Not Available',
420-
'',
421-
'This is a file search tool.',
422-
'Directories are not meant to be previewed.',
423-
'',
424-
'Path: ' .. file_path,
425-
})
426-
return false
427-
elseif get_image().is_image(file_path) then
428-
-- Delegate to image preview
459+
if get_image().is_image(file_path) then
429460
local win_width = 80
430461
local win_height = 24
431462

432-
-- Try to get actual window dimensions if available
433463
if M.state.winid and vim.api.nvim_win_is_valid(M.state.winid) then
434464
win_width = vim.api.nvim_win_get_width(M.state.winid) - 2
435465
win_height = vim.api.nvim_win_get_height(M.state.winid) - 2
436466
end
437467

438-
get_image().display_image(file_path, bufnr, win_width, win_height)
439-
return true
468+
return get_image().display_image(file_path, bufnr, win_width, win_height)
440469
elseif M.is_binary_file(file_path) then
441-
-- Handle binary files before attempting to read as text
442-
local info = M.get_file_info(file_path)
443-
return M.preview_binary_file(file_path, bufnr, info, file)
470+
return M.preview_binary_file(file_path, bufnr)
444471
else
445472
return M.preview_file(file_path, bufnr)
446473
end
447474
end
448475

449-
--- Scroll preview content
450-
--- @param lines number Number of lines to scroll (positive = down, negative = up)
451476
function M.scroll(lines)
452477
if not M.state.bufnr or not vim.api.nvim_buf_is_valid(M.state.bufnr) then return end
453-
454478
if not M.state.winid or not vim.api.nvim_win_is_valid(M.state.winid) then return end
455479

456-
-- Get current cursor position
457-
local cursor = vim.api.nvim_win_get_cursor(M.state.winid)
458-
local current_line = cursor[1]
459480
local win_height = vim.api.nvim_win_get_height(M.state.winid)
481+
vim.notify(win_height)
482+
local content_height = M.state.content_height or 0
483+
484+
-- allows scrolling for a full content + half window
485+
local half_screen = math.floor(win_height / 2)
486+
local max_scroll = math.max(0, content_height + half_screen - win_height)
487+
488+
local current_offset = M.state.scroll_offset or 0
489+
local new_offset = math.max(0, math.min(max_scroll, current_offset + lines))
460490

461-
-- Calculate new position
462-
local new_line = math.max(1, math.min(M.state.content_height, current_line + lines))
491+
if new_offset ~= current_offset then
492+
M.state.scroll_offset = new_offset
463493

464-
-- Set new cursor position
465-
vim.api.nvim_win_set_cursor(M.state.winid, { new_line, 0 })
494+
local target_line = math.min(content_height, math.max(1, new_offset + 1))
466495

467-
-- Update scroll offset
468-
M.state.scroll_offset = new_line
496+
vim.api.nvim_win_call(M.state.winid, function()
497+
vim.api.nvim_win_set_cursor(M.state.winid, { target_line, 0 })
498+
vim.cmd('normal! zt')
499+
end)
500+
end
469501
end
470502

471503
--- Set preview window
@@ -498,18 +530,18 @@ end
498530
--- @return boolean Success status
499531
function M.update_file_info_buffer(file, bufnr, file_index)
500532
if not file then
501-
safe_set_buffer_lines(bufnr, 0, -1, false, { 'No file selected' })
533+
set_buffer_lines(bufnr, { 'No file selected' })
502534
return false
503535
end
504536

505537
local info = M.get_file_info(file.path)
506538
if not info then
507-
safe_set_buffer_lines(bufnr, 0, -1, false, { 'File info unavailable' })
539+
set_buffer_lines(bufnr, { 'File info unavailable' })
508540
return false
509541
end
510542

511543
local file_info_lines = M.create_file_info_content(file, info, file_index)
512-
safe_set_buffer_lines(bufnr, 0, -1, false, file_info_lines)
544+
set_buffer_lines(bufnr, file_info_lines)
513545

514546
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
515547
vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
@@ -521,24 +553,19 @@ end
521553

522554
--- Clear buffer completely including any image attachments
523555
--- @param bufnr number Buffer number to clear
524-
function M.clear_buffer_completely(bufnr)
556+
function M.clear_buffer(bufnr)
525557
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end
526558

527-
-- Clear any image attachments first
528559
get_image().clear_buffer_images(bufnr)
529-
530-
-- Clear text content
531-
safe_set_buffer_lines(bufnr, 0, -1, false, {})
532-
533-
-- Reset filetype to prevent syntax highlighting issues
560+
set_buffer_lines(bufnr, {})
534561
vim.api.nvim_buf_set_option(bufnr, 'filetype', '')
535562
end
536563

537564
--- Clear preview
538565
function M.clear()
539566
if M.state.bufnr and vim.api.nvim_buf_is_valid(M.state.bufnr) then
540-
M.clear_buffer_completely(M.state.bufnr)
541-
safe_set_buffer_lines(M.state.bufnr, 0, -1, false, { 'No preview available' })
567+
M.clear_buffer(M.state.bufnr)
568+
set_buffer_lines(M.state.bufnr, { 'No preview available' })
542569
end
543570

544571
M.state.current_file = nil

lua/fff/main.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function M.setup(config)
1818
preview = {
1919
enabled = true,
2020
width = 0.5,
21-
max_lines = 1000,
21+
max_lines = 5000,
2222
max_size = 10 * 1024 * 1024, -- 10MB
2323
imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit',
2424
line_numbers = false,

lua/fff/picker_ui.lua

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ local git_utils = require('fff.git_utils')
77
local main = require('fff.main')
88

99
-- Initialize preview with main config
10-
if main.config and main.config.preview then
11-
preview.setup(main.config.preview)
12-
end
10+
if main.config and main.config.preview then preview.setup(main.config.preview) end
1311

1412
M.state = {
1513
active = false,
@@ -823,8 +821,7 @@ function M.update_preview()
823821
if M.state.file_info_buf then preview.update_file_info_buffer(item, M.state.file_info_buf, M.state.cursor) end
824822

825823
preview.set_preview_window(M.state.preview_win)
826-
827-
preview.preview(item.path, M.state.preview_buf, item)
824+
preview.preview(item.path, M.state.preview_buf)
828825
end
829826

830827
--- Clear preview

lua/fff/utils.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,3 @@ function M.format_file_size(size)
1818
end
1919

2020
return M
21-

0 commit comments

Comments
 (0)