Skip to content

Commit 3f0227b

Browse files
committed
feat: indent add mark cache
ref: add lazy opts fix: when scroll horizon not handle win_col properly
1 parent dcf9a4e commit 3f0227b

File tree

3 files changed

+209
-69
lines changed

3 files changed

+209
-69
lines changed

lua/hlchunk/mods/indent/init.lua

Lines changed: 86 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ local fn = vim.fn
1212
local ROWS_INDENT_RETCODE = indentHelper.ROWS_INDENT_RETCODE
1313

1414
---@class IndentMetaInfo : MetaInfo
15+
---@field pre_leftcol number
1516
---@field cache Cache
1617

1718
local constructor = function(self, conf, meta)
@@ -21,8 +22,9 @@ local constructor = function(self, conf, meta)
2122
hl_base_name = "HLIndent",
2223
ns_id = api.nvim_create_namespace("indent"),
2324
shiftwidth = fn.shiftwidth(),
25+
pre_leftcol = 0,
2426
leftcol = fn.winsaveview().leftcol,
25-
cache = Cache(),
27+
cache = Cache("bufnr", "line"),
2628
}
2729

2830
BaseMod.init(self, conf, meta)
@@ -33,83 +35,117 @@ end
3335
---@class IndentMod : BaseMod
3436
---@field conf IndentConf
3537
---@field meta IndentMetaInfo
36-
---@field render fun(self: IndentMod, range: Scope)
37-
---@field renderLine function
38+
---@field render fun(self: IndentMod, range: Scope, opts: {lazy: boolean})
39+
---@field narrowRange fun(self: IndentMod, range: Scope): Scope
40+
---@field calcRenderInfo fun(self: IndentMod, range: Scope): table<number, string>
41+
---@field setmark function
3842
---@overload fun(conf?: UserIndentConf, meta?: MetaInfo): IndentMod
3943
local IndentMod = class(BaseMod, constructor)
4044

41-
function IndentMod:render(range)
42-
self:clear(range)
45+
local pos2info = Cache("bufnr", "line", "col")
46+
local pos2id = Cache("bufnr", "line", "col")
4347

44-
-- narrow the range that should get indent
45-
local non_cached_start = range.start
46-
local non_cached_finish = range.finish
48+
function IndentMod:disable()
49+
pos2info:clear()
50+
pos2id:clear()
51+
BaseMod.disable(self)
52+
end
53+
54+
function IndentMod:narrowRange(range)
55+
local start = range.start
56+
local finish = range.finish
4757
for i = range.start, range.finish do
4858
if not self.meta.cache:has(range.bufnr, i) then
49-
non_cached_start = i
59+
start = i
5060
break
5161
end
5262
end
53-
for i = non_cached_start, range.finish do
54-
if self.meta.cache:has(range.bufnr, i) then
55-
non_cached_finish = i - 1
63+
for i = range.finish, range.start, -1 do
64+
if not self.meta.cache:has(range.bufnr, i) then
65+
finish = i
5666
break
5767
end
5868
end
69+
return Scope(range.bufnr, start, finish)
70+
end
5971

60-
-- calculate indent
61-
local retcode, rows_indent = indentHelper.get_rows_indent(Scope(range.bufnr, non_cached_start, non_cached_finish), {
62-
use_treesitter = self.conf.use_treesitter,
63-
virt_indent = true,
64-
})
65-
if retcode == ROWS_INDENT_RETCODE.NO_TS and self.conf.use_treesitter then
66-
if self.conf.notify then
67-
self:notify("[hlchunk.indent]: no parser for " .. vim.bo.filetype, nil, { once = true })
68-
end
69-
return
70-
end
71-
72-
-- update cache
73-
for lnum, indent in pairs(rows_indent) do
74-
self.meta.cache:set(range.bufnr, lnum, indent)
75-
end
76-
72+
function IndentMod:calcRenderInfo(range)
7773
-- calc render info
78-
local row_opts = {
79-
virt_text_pos = "overlay",
80-
hl_mode = "combine",
81-
priority = self.conf.priority,
82-
}
8374
local char_num = #self.conf.chars
8475
local style_num = #self.meta.hl_name_list
8576
local leftcol = self.meta.leftcol
8677
local sw = self.meta.shiftwidth
8778
local render_info = {}
8879
for lnum = range.start, range.finish do
89-
local blankLen = self.meta.cache:get(range.bufnr, lnum)
80+
local blankLen = self.meta.cache:get(range.bufnr, lnum) --[[@as string]]
9081
local render_char_num, offset, shadow_char_num = indentHelper.calc(blankLen, leftcol, sw)
9182
for i = 1, render_char_num do
83+
local win_col = offset + (i - 1) * sw
9284
local char = self.conf.chars[(i - 1 + shadow_char_num) % char_num + 1]
9385
local style = self.meta.hl_name_list[(i - 1 + shadow_char_num) % style_num + 1]
9486
table.insert(render_info, {
9587
lnum = lnum,
96-
virt_text_win_col = offset + leftcol + (i - 1) * sw,
88+
virt_text_win_col = win_col,
9789
virt_text = { { char, style } },
9890
})
91+
pos2info:set(range.bufnr, lnum, win_col, { char, style })
9992
end
10093
end
10194

95+
return render_info
96+
end
97+
98+
function IndentMod:setmark(bufnr, render_info)
10299
-- render
100+
local row_opts = {
101+
virt_text_pos = "overlay",
102+
hl_mode = "combine",
103+
priority = self.conf.priority,
104+
}
103105
for _, v in pairs(render_info) do
104106
row_opts.virt_text = v.virt_text
105107
row_opts.virt_text_win_col = v.virt_text_win_col
106-
api.nvim_buf_set_extmark(range.bufnr, self.meta.ns_id, v.lnum, 0, row_opts)
108+
if not pos2id:get(bufnr, v.lnum, v.virt_text_win_col) then
109+
local id = api.nvim_buf_set_extmark(bufnr, self.meta.ns_id, v.lnum, 0, row_opts)
110+
pos2id:set(bufnr, v.lnum, v.virt_text_win_col, id)
111+
end
107112
end
108113
end
109114

115+
function IndentMod:render(range, opts)
116+
opts = opts or { lazy = false }
117+
if not opts.lazy then
118+
self:clear(Scope(range.bufnr, 0, api.nvim_buf_line_count(range.bufnr)))
119+
self.meta.cache:clear(range.bufnr)
120+
pos2id:clear(range.bufnr)
121+
pos2info:clear(range.bufnr)
122+
end
123+
124+
local narrowed_range = self:narrowRange(range)
125+
-- calculate indent
126+
local retcode, rows_indent = indentHelper.get_rows_indent(narrowed_range, {
127+
use_treesitter = self.conf.use_treesitter,
128+
virt_indent = true,
129+
})
130+
if retcode == ROWS_INDENT_RETCODE.NO_TS and self.conf.use_treesitter then
131+
if self.conf.notify then
132+
self:notify("[hlchunk.indent]: no parser for " .. vim.bo.filetype, nil, { once = true })
133+
end
134+
return
135+
end
136+
137+
-- update cache
138+
for lnum, indent in pairs(rows_indent) do
139+
self.meta.cache:set(range.bufnr, lnum, indent)
140+
end
141+
local render_info = self:calcRenderInfo(narrowed_range)
142+
self:setmark(range.bufnr, render_info)
143+
end
144+
110145
function IndentMod:createAutocmd()
111146
BaseMod.createAutocmd(self)
112-
local render_cb = function(event)
147+
local render_cb = function(event, opts)
148+
opts = opts or { lazy = false }
113149
local bufnr = event.buf
114150
if not (api.nvim_buf_is_valid(bufnr) and self:shouldRender(bufnr)) then
115151
return
@@ -122,29 +158,35 @@ function IndentMod:createAutocmd()
122158
range.finish = math.min(api.nvim_buf_line_count(bufnr) - 1, range.finish + ahead_lines)
123159
api.nvim_win_call(winid, function()
124160
self.meta.shiftwidth = cFunc.get_sw(bufnr)
161+
self.meta.pre_leftcol = self.meta.leftcol
125162
self.meta.leftcol = fn.winsaveview().leftcol
126-
self:render(range)
163+
if self.meta.pre_leftcol ~= self.meta.leftcol then
164+
opts.lazy = false
165+
end
166+
self:render(range, opts)
127167
end)
128168
end
129169
end
130170
local throttle_render_cb = throttle(render_cb, self.conf.delay)
131-
local throttle_render_cb_with_pre_hook = function(event)
171+
local throttle_render_cb_with_pre_hook = function(event, opts)
172+
opts = opts or { lazy = false }
132173
local bufnr = event.buf
133174
if not (api.nvim_buf_is_valid(bufnr) and self:shouldRender(bufnr)) then
134175
return
135176
end
136-
throttle_render_cb(event)
177+
throttle_render_cb(event, opts)
137178
end
138179

139180
api.nvim_create_autocmd({ "WinScrolled" }, {
140181
group = self.meta.augroup_name,
141-
callback = throttle_render_cb_with_pre_hook,
182+
callback = function(e)
183+
throttle_render_cb_with_pre_hook(e, { lazy = true })
184+
end,
142185
})
143186
api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
144187
group = self.meta.augroup_name,
145188
callback = function(e)
146-
self.meta.cache:clear(e.buf)
147-
throttle_render_cb_with_pre_hook(e)
189+
throttle_render_cb_with_pre_hook(e, { lazy = false })
148190
end,
149191
})
150192
api.nvim_create_autocmd({ "BufWinEnter" }, {

lua/hlchunk/utils/cache.lua

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,69 @@
11
local class = require("hlchunk.utils.class")
22

33
---@class Cache
4-
---@field private cache table<number, table<string|number, any>>
5-
---@overload fun():Cache
6-
local Cache = class(function(self)
4+
---@field private cache table
5+
---@field private keys table
6+
---@overload fun(...):Cache
7+
local Cache = class(function(self, ...)
8+
self.keys = { ... } -- 在构造函数中保存键名
79
self.cache = {}
810
end)
911

10-
---@param bufnr number
11-
---@param key string|number
12-
---@return any
13-
function Cache:get(bufnr, key)
14-
if not self.cache[bufnr] then
15-
return nil
12+
local function navigateCache(cache, values, createIfMissing, setValue)
13+
local tableRef = cache
14+
local searchSteps = #values
15+
for i = 1, searchSteps - 1 do
16+
local key = values[i]
17+
if tableRef[key] == nil then
18+
if createIfMissing then
19+
tableRef[key] = {}
20+
else
21+
return nil
22+
end
23+
end
24+
tableRef = tableRef[key]
25+
end
26+
27+
if setValue then
28+
tableRef[values[searchSteps]] = setValue
29+
return setValue
30+
else
31+
return tableRef[values[searchSteps]]
1632
end
17-
return self.cache[bufnr][key]
1833
end
1934

20-
---@param bufnr number
21-
---@param key string|number
22-
---@param value any
23-
function Cache:set(bufnr, key, value)
24-
if not self.cache[bufnr] then
25-
self.cache[bufnr] = {}
35+
function Cache:get(...)
36+
local values = { ... }
37+
if #values ~= #self.keys then
38+
error("The number of keys passed to get() must match the number of keys passed to the constructor")
2639
end
27-
self.cache[bufnr][key] = value
40+
return navigateCache(self.cache, values, false)
2841
end
2942

30-
---@param bufnr number
31-
---@param key string|number
32-
---@return boolean
33-
function Cache:has(bufnr, key)
34-
return self.cache[bufnr] and self.cache[bufnr][key]
43+
function Cache:set(...)
44+
local values = { ... }
45+
if #values ~= #self.keys + 1 then
46+
error("The number of keys passed to set() must be one more than the number of keys passed to the constructor")
47+
end
48+
local value = table.remove(values) -- 将最后一个参数作为要设置的值
49+
navigateCache(self.cache, values, true, value)
3550
end
3651

37-
---@param bufnr number
38-
function Cache:clear(bufnr)
39-
self.cache[bufnr] = {}
52+
function Cache:has(...)
53+
local values = { ... }
54+
if #values ~= #self.keys then
55+
error("The number of keys passed to has() must match the number of keys passed to the constructor")
56+
end
57+
return navigateCache(self.cache, values, false) ~= nil
58+
end
59+
60+
function Cache:clear(...)
61+
local values = { ... }
62+
if #values == 0 then
63+
self.cache = {}
64+
else
65+
navigateCache(self.cache, values, false, {})
66+
end
4067
end
4168

4269
return Cache

test/features/cache_spec.lua

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
local assert = require("luassert")
2+
local Cache = require("hlchunk.utils.cache")
3+
4+
describe("cache", function()
5+
it("cache with one", function()
6+
local cache = Cache("bufnr")
7+
assert.equal(nil, cache:get(1))
8+
cache:set(0, "a")
9+
cache:set(1, "b")
10+
assert.equal("a", cache:get(0))
11+
assert.equal("b", cache:get(1))
12+
cache:set(0, "c")
13+
assert.equal("c", cache:get(0))
14+
assert.is_true(cache:has(0))
15+
assert.is_false(cache:has(2))
16+
cache:clear()
17+
assert.equal(nil, cache:get(0))
18+
end)
19+
it("cache with two field", function()
20+
local cache = Cache("bufnr", "line")
21+
assert.equal(nil, cache:get(1, 1))
22+
cache:set(0, 0, "a")
23+
cache:set(0, 1, "b")
24+
cache:set(1, 0, "c")
25+
cache:set(1, 1, "d")
26+
assert.equal("a", cache:get(0, 0))
27+
assert.equal("b", cache:get(0, 1))
28+
assert.equal("c", cache:get(1, 0))
29+
assert.equal("d", cache:get(1, 1))
30+
cache:set(0, 0, "e")
31+
assert.equal("e", cache:get(0, 0))
32+
assert.is_true(cache:has(0, 0))
33+
assert.is_false(cache:has(2, 2))
34+
cache:clear(0)
35+
assert.equal(nil, cache:get(0, 0))
36+
assert.equal("c", cache:get(1, 0))
37+
cache:clear()
38+
assert.equal(nil, cache:get(0, 0))
39+
assert.equal(nil, cache:get(1, 0))
40+
end)
41+
it("cache with three field", function()
42+
local cache = Cache("bufnr", "line", "col")
43+
assert.equal(nil, cache:get(1, 1, 1))
44+
cache:set(0, 0, 0, "a")
45+
cache:set(0, 0, 1, "b")
46+
cache:set(0, 1, 0, "c")
47+
cache:set(0, 1, 1, "d")
48+
cache:set(1, 0, 0, "e")
49+
cache:set(1, 0, 1, "f")
50+
cache:set(1, 1, 0, "g")
51+
cache:set(1, 1, 1, "h")
52+
assert.equal("a", cache:get(0, 0, 0))
53+
assert.equal("b", cache:get(0, 0, 1))
54+
assert.equal("c", cache:get(0, 1, 0))
55+
assert.equal("d", cache:get(0, 1, 1))
56+
assert.equal("e", cache:get(1, 0, 0))
57+
assert.equal("f", cache:get(1, 0, 1))
58+
assert.equal("g", cache:get(1, 1, 0))
59+
assert.equal("h", cache:get(1, 1, 1))
60+
cache:set(0, 0, 0, "i")
61+
assert.equal("i", cache:get(0, 0, 0))
62+
assert.is_true(cache:has(0, 0, 0))
63+
assert.is_false(cache:has(2, 2, 2))
64+
cache:clear(0, 0)
65+
assert.equal(nil, cache:get(0, 0, 0))
66+
assert.equal("c", cache:get(0, 1, 0))
67+
cache:clear()
68+
assert.equal(nil, cache:get(0, 0, 0))
69+
assert.equal(nil, cache:get(1, 0, 0))
70+
end)
71+
end)

0 commit comments

Comments
 (0)