Skip to content

Commit f92d1ea

Browse files
committed
refactor(previewer): handle preview buffer cache
1 parent 23c0124 commit f92d1ea

4 files changed

Lines changed: 133 additions & 101 deletions

File tree

lua/fzf-lua/previewer/bcache.lua

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
-- cache entry for preview buf
2+
3+
---rename bufnr to buf?
4+
---@class fzf-lua.BcacheEntry
5+
---@field bufnr integer
6+
---@field tick? integer
7+
---@field min_winopts? boolean
8+
---@field pos? [integer, integer]|true cached cursor positions
9+
10+
---@class fzf-lua.Bcache
11+
---@field private entries table<any, fzf-lua.BcacheEntry> cached preview entries
12+
---@field private buffers table<integer, fzf-lua.BcacheEntry> cached bufnrs
13+
---@field private bufdel fun(buf: integer) callback to delete a buffer when evicted from cache
14+
local M = {}
15+
M.__index = M
16+
17+
---@param bufdel fun(buf: integer)
18+
---@return fzf-lua.Bcache
19+
function M.new(bufdel)
20+
return setmetatable({ entries = {}, buffers = {}, bufdel = bufdel }, M)
21+
end
22+
23+
---Prioritize entry.cache_key since override self:key_from_entry is not allowed now
24+
---@param entry fzf-lua.buffer_or_file.Entry
25+
---@return string?
26+
local key_from_entry = function(entry)
27+
-- entry.do_not_cache -> entry.cache_key=false?
28+
if entry.do_not_cache then return nil end
29+
return entry.cache_key
30+
or entry.bufnr and ("bufnr:%d"):format(entry.bufnr)
31+
or entry.uri
32+
or entry.path
33+
end
34+
35+
---@param entry fzf-lua.buffer_or_file.Entry
36+
---@return fzf-lua.BcacheEntry?
37+
function M:get(entry)
38+
return self.entries[key_from_entry(entry)]
39+
end
40+
41+
---cache only buffer have key_from_entry
42+
---@param entry fzf-lua.buffer_or_file.Entry
43+
---@param buf integer buf should be valid
44+
---@param min_winopts boolean?
45+
function M:set(entry, buf, min_winopts)
46+
local key = key_from_entry(entry)
47+
if not key then return end
48+
local cached = self.entries[key]
49+
if cached and cached.bufnr == buf then
50+
cached.tick = entry.tick
51+
return
52+
elseif cached then
53+
self.bufdel(cached.bufnr)
54+
end
55+
---@type fzf-lua.BcacheEntry
56+
local newcache = {
57+
bufnr = buf,
58+
tick = entry.tick,
59+
min_winopts = min_winopts,
60+
pos = true, -- reset scroll position
61+
}
62+
self.entries[key] = newcache
63+
self.buffers[buf] = newcache
64+
-- remove buffer auto-delete since it's now cached
65+
vim.bo[buf].bufhidden = "hide"
66+
end
67+
68+
---@param entry fzf-lua.buffer_or_file.Entry
69+
---@return fzf-lua.BcacheEntry?, boolean? (true: stale, false: valid, nil: not cached)
70+
function M:check(entry)
71+
local cached = self.entries[key_from_entry(entry)]
72+
return cached, cached and entry.tick ~= cached.tick or nil
73+
end
74+
75+
---nop if update on a un-managed buf
76+
---@param buf integer
77+
---@param pos [integer, integer]|true set/reset cursor position
78+
function M:update_pos(buf, pos)
79+
local cached = self.buffers[buf]
80+
if cached then cached.pos = pos end
81+
end
82+
83+
---nop if update on a un-managed buf
84+
---@param buf integer
85+
function M:reset_pos(buf)
86+
self:update_pos(buf, true)
87+
end
88+
89+
---@param buf integer
90+
---@return [integer, integer]|true?
91+
function M:get_pos(buf)
92+
local cached = self.buffers[buf]
93+
return cached and cached.pos or nil
94+
end
95+
96+
function M:clear()
97+
for _, entry in pairs(self.entries) do
98+
self.bufdel(entry.bufnr)
99+
end
100+
self.entries = {}
101+
self.buffers = {}
102+
end
103+
104+
return M

lua/fzf-lua/previewer/builtin.lua

Lines changed: 23 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ local Previewer = {}
3737
---@field winopts_orig table
3838
---@field extensions { [string]: string[]? }
3939
---@field ueberzug_scaler "crop"|"distort"|"contain"|"fit_contain"|"cover"|"forced_cover"
40-
---@field cached_bufnrs table<integer, [integer, integer]|true?> items are cached_pos
41-
---@field cached_buffers table<string, fzf-lua.buffer_or_file.Bcache?>
40+
---@field bcache fzf-lua.Bcache
4241
---@field listed_buffers table<integer, boolean?>
4342
---@field clear_on_redraw boolean?
4443
---@field timers? table<string, uv.uv_timer_t?>
@@ -88,9 +87,9 @@ function Previewer.base:new(o, opts)
8887
utils.warn(("Invalid ueberzug image scaler '%s', option will be omitted.")
8988
:format(o.ueberzug_scaler))
9089
end
91-
-- cached buffers
92-
self.cached_bufnrs = {}
93-
self.cached_buffers = {}
90+
self.bcache = require("fzf-lua.previewer.bcache").new(function(buf)
91+
self:safe_buf_delete(buf, true)
92+
end)
9493
-- store currently listed buffers, this helps us determine which buffers
9594
-- navigated with 'vim.lsp.util.show_document' we can safely unload
9695
-- since show_document reuses buffers and I couldn't find a better way
@@ -119,9 +118,7 @@ function Previewer.base:close(do_not_clear_cache)
119118
TSContext.deregister()
120119
self:restore_winopts()
121120
self:clear_preview_buf()
122-
if not do_not_clear_cache then
123-
self:clear_cached_buffers()
124-
end
121+
if not do_not_clear_cache and self.bcache then self.bcache:clear() end
125122
self.winopts_orig = {}
126123
end
127124

@@ -180,7 +177,7 @@ function Previewer.base:safe_buf_delete(bufnr, del_cached)
180177
elseif not api.nvim_buf_is_valid(bufnr) then
181178
-- print("safe_buf_delete INVALID", bufnr)
182179
return
183-
elseif not del_cached and self.cached_bufnrs[bufnr] then
180+
elseif not del_cached and self.bcache:get_pos(bufnr) then
184181
-- print("safe_buf_delete CACHED", bufnr)
185182
return
186183
end
@@ -225,15 +222,6 @@ function Previewer.base:set_preview_buf(newbuf, min_winopts, no_wipe)
225222
end
226223
end
227224

228-
function Previewer.base:clear_cached_buffers()
229-
-- clear the buffer cache
230-
for _, c in pairs(self.cached_buffers) do
231-
self:safe_buf_delete(c.bufnr, true)
232-
end
233-
self.cached_bufnrs = {}
234-
self.cached_buffers = {}
235-
end
236-
237225
---@param newbuf boolean?
238226
---@return integer?
239227
function Previewer.base:clear_preview_buf(newbuf)
@@ -454,13 +442,8 @@ function Previewer.base:scroll(direction)
454442
-- Conditionally toggle 'cursorline' based on cursor position
455443
self:maybe_set_cursorline(preview_winid, self.orig_pos)
456444
-- HACK: Hijack cached bufnr value as last scroll position
457-
if self.cached_bufnrs[self.preview_bufnr] then
458-
if direction == "reset" then
459-
self.cached_bufnrs[self.preview_bufnr] = true
460-
else
461-
self.cached_bufnrs[self.preview_bufnr] = api.nvim_win_get_cursor(preview_winid)
462-
end
463-
end
445+
self.bcache:update_pos(self.preview_bufnr,
446+
direction == "reset" or api.nvim_win_get_cursor(preview_winid))
464447
self.win:update_preview_scrollbar()
465448
self:update_render_markdown()
466449
self:update_ts_context()
@@ -726,56 +709,6 @@ function Previewer.buffer_or_file:populate_terminal_cmd(tmpbuf, cmd, entry)
726709
return true
727710
end
728711

729-
---@diagnostic disable-next-line: unused
730-
---@param entry fzf-lua.buffer_or_file.Entry
731-
---@return string?
732-
local key_from_entry = function(entry)
733-
if entry.do_not_cache then return nil end
734-
return (entry.bufnr and string.format("bufnr:%d", entry.bufnr) or entry.uri or entry.path) or nil
735-
end
736-
737-
---get and check if cached is update-to-date to be reuse
738-
---@param entry fzf-lua.buffer_or_file.Entry
739-
---@return fzf-lua.buffer_or_file.Bcache?
740-
function Previewer.buffer_or_file:check_bcache(entry)
741-
local key = key_from_entry(entry)
742-
if not key then return end
743-
local cached = self.cached_buffers[key]
744-
if not cached then return end
745-
entry.cached = cached
746-
assert(self.cached_bufnrs[cached.bufnr])
747-
assert(api.nvim_buf_is_valid(cached.bufnr))
748-
if entry.tick ~= cached.tick then
749-
cached.invalid = true
750-
cached.tick = entry.tick
751-
end
752-
return cached
753-
end
754-
755-
---cache the bufnr for the entry, evict previous cache if exists
756-
---@param bufnr integer
757-
---@param entry fzf-lua.buffer_or_file.Entry
758-
---@param min_winopts boolean?
759-
function Previewer.buffer_or_file:cache_buffer(bufnr, entry, min_winopts)
760-
local key = key_from_entry(entry)
761-
if not key then return end
762-
local cached = self.cached_buffers[key]
763-
if cached then
764-
if cached.bufnr == bufnr then
765-
-- already cached, nothing to do
766-
return cached
767-
else
768-
-- new cached buffer for key, wipe current cached buf
769-
self.cached_bufnrs[cached.bufnr] = nil
770-
self:safe_buf_delete(cached.bufnr)
771-
end
772-
end
773-
self.cached_bufnrs[bufnr] = true -- reset scroll position
774-
self.cached_buffers[key] = { bufnr = bufnr, min_winopts = min_winopts, tick = entry.tick }
775-
-- remove buffer auto-delete since it's now cached
776-
vim.bo[bufnr].bufhidden = "hide"
777-
end
778-
779712
---@alias fzf-lua.line (string|[string,string])[]
780713
---@param content (string|fzf-lua.line)[]
781714
---@return string[], table
@@ -864,26 +797,28 @@ function Previewer.buffer_or_file:populate_preview_buf(entry_str)
864797
entry = entry or coroutine.yield()
865798
if entry_str ~= self._last_entry or not self.win:validate_preview() then return false end
866799
if utils.tbl_isempty(entry) then return end
867-
local cached = self:check_bcache(entry)
800+
801+
-- check if cached is update-to-date to be reuse
802+
local cached, stale = self.bcache:check(entry)
803+
entry.cached = cached
868804

869805
-- same file/buffer as previous entry no need to change preview buf
870-
if cached and not cached.invalid and cached.bufnr == self.preview_bufnr then
871-
if type(self.cached_bufnrs[self.preview_bufnr]) == "table"
872-
and ((entry.line and entry.line > 0 and entry.line ~= self.orig_pos[1])
873-
or (entry.col and entry.col > 0 and entry.col - 1 ~= self.orig_pos[2])) then
874-
-- entry is within the same buffer but line|col has changed
875-
-- clear cached buffer position so we scroll to entry's line|col
876-
self.cached_bufnrs[self.preview_bufnr] = true
806+
-- (e.g. only lnum changed, or unhide/resume)
807+
if cached and not stale and cached.bufnr == self.preview_bufnr then
808+
local line = entry.line and entry.line > 0 and entry.line or nil
809+
local col = entry.col and entry.col > 0 and entry.col - 1 or nil
810+
if not vim.deep_equal({ line, col }, self.orig_pos) then
811+
self.bcache:reset_pos(self.preview_bufnr)
877812
end
878-
self:preview_buf_post(entry)
813+
self:preview_buf_post(entry, cached.min_winopts)
879814
return
880815
end
881-
if cached and not cached.invalid then
816+
if cached and not stale then
882817
self:set_preview_buf(cached.bufnr, cached.min_winopts)
883818
self:preview_buf_post(entry, cached.min_winopts)
884819
return
885820
end
886-
local reuse_buf = (cached or {}).bufnr -- cached must be invalid now if exists
821+
local reuse_buf = (cached or {}).bufnr -- cached must be stale now if exists
887822
self.clear_on_redraw = false
888823
-- kill previously running terminal jobs
889824
-- when using external commands extension map
@@ -1281,7 +1216,7 @@ function Previewer.buffer_or_file:set_cursor_hl(entry)
12811216
---@diagnostic disable-next-line: unnecessary-assert
12821217
local buf, win, hls = assert(self.preview_bufnr, self.win.preview_winid, self.win.hls)
12831218
pcall(api.nvim_win_call, win, function()
1284-
local cached_pos = self.cached_bufnrs[buf]
1219+
local cached_pos = self.bcache:get_pos(buf)
12851220
if type(cached_pos) ~= "table" then cached_pos = nil end
12861221
local lnum, col = entry.line, math.max(1, entry.col or 1)
12871222
if not lnum or lnum < 1 then
@@ -1397,7 +1332,7 @@ function Previewer.buffer_or_file:preview_buf_post(entry, min_winopts)
13971332

13981333
-- Should we cache the current preview buffer?
13991334
-- we cache only named buffers with valid path/uri
1400-
self:cache_buffer(self.preview_bufnr, entry, min_winopts)
1335+
self.bcache:set(entry, self.preview_bufnr, min_winopts)
14011336
end
14021337

14031338
---@class fzf-lua.previewer.HelpTags : fzf-lua.previewer.BufferOrFile,{}

lua/fzf-lua/types.lua

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,19 @@ local FzfLua = require("fzf-lua")
3030
---@field no_scrollbar? boolean
3131
---@field tick? integer
3232
---@field no_syntax? boolean
33-
---@field cached? fzf-lua.buffer_or_file.Bcache
33+
---@field cached? fzf-lua.BcacheEntry
3434
---@field filetype? string
3535
---@field content? (string|fzf-lua.line)[]
3636
---@field end_line? integer 1-based
3737
---@field end_col? integer 1-based
3838
---@field open_term? boolean open_term for content (cmd always open_term)
39+
---@field cache_key? any unique cache key for entry (to reuse preview buffer)
3940

4041
---@class fzf-lua.keymap.Entry: fzf-lua.buffer_or_file.Entry
4142
---@field vmap string?
4243
---@field mode string?
4344
---@field key string?
4445

45-
---@class fzf-lua.buffer_or_file.Bcache
46-
---@field bufnr integer
47-
---@field min_winopts? boolean
48-
---@field invalid? boolean buffer content changed
49-
---@field tick? integer
50-
5146
---@alias fzf-lua.config.Action fzf-lua.ActionSpec|fzf-lua.shell.data2|fzf-lua.shell.data2[]|false
5247
---@alias fzf-lua.config.Actions { [1]?: boolean, [string]: fzf-lua.config.Action }
5348

lua/fzf-lua/win.lua

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -866,12 +866,10 @@ function FzfWin:redraw_main()
866866
}, self.layout.fzf)
867867

868868
if self:validate() then
869-
if self._previewer
870-
and self._previewer.clear_on_redraw
871-
and self._previewer.clear_preview_buf
872-
and self._previewer.clear_cached_buffers then
873-
self._previewer:clear_preview_buf(true)
874-
self._previewer:clear_cached_buffers()
869+
local prev = self._previewer
870+
if prev and prev.clear_on_redraw then
871+
if prev.clear_preview_buf then prev:clear_preview_buf(true) end
872+
if prev.bcache then prev.bcache:clear() end
875873
end
876874
utils.win_set_config(self.fzf_winid, winopts)
877875
else

0 commit comments

Comments
 (0)