Skip to content

Commit cd32476

Browse files
committed
perf: fix performance regression
1 parent ed1446c commit cd32476

File tree

3 files changed

+126
-48
lines changed

3 files changed

+126
-48
lines changed

lua/hlchunk/mods/chunk/init.lua

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,23 +114,23 @@ function ChunkMod:get_chunk_data(range, virt_text_list, row_list, virt_text_win_
114114
local virt_text, virt_text_win_col =
115115
chunkHelper.calc(beg_virt_text, start_col, self.meta.leftcol, self.meta.shiftwidth)
116116
local char_list = utf8Split(virt_text)
117-
vim.list_extend(virt_text_list, vim.fn.reverse(char_list))
118-
vim.list_extend(row_list, vim.fn["repeat"]({ range.start }, #char_list))
119-
vim.list_extend(
117+
chunkHelper.list_extend(virt_text_list, chunkHelper.listReverse(char_list))
118+
chunkHelper.list_extend(row_list, chunkHelper.repeated(range.start, #char_list))
119+
chunkHelper.list_extend(
120120
virt_text_win_col_list,
121-
vim.fn.reverse(chunkHelper.getColList(char_list, virt_text_win_col, self.meta.shiftwidth))
121+
chunkHelper.listReverse(chunkHelper.getColList(char_list, virt_text_win_col, self.meta.shiftwidth))
122122
)
123123
end
124124

125125
local mid_row_nums = range.finish - range.start - 1
126-
vim.list_extend(row_list, rangeFromTo((range.start + 1), (range.finish - 1)))
127-
vim.list_extend(virt_text_win_col_list, vim.fn["repeat"]({ start_col - self.meta.leftcol }, mid_row_nums))
126+
chunkHelper.list_extend(row_list, rangeFromTo((range.start + 1), (range.finish - 1)))
127+
chunkHelper.list_extend(virt_text_win_col_list, chunkHelper.repeated(start_col - self.meta.leftcol, mid_row_nums))
128128
---@type string[]
129129
local chars
130130
if start_col - self.meta.leftcol < 0 then
131-
chars = vim.fn["repeat"]({ "" }, mid_row_nums)
131+
chars = chunkHelper.repeated("", mid_row_nums)
132132
else
133-
chars = vim.fn["repeat"]({ self.conf.chars.vertical_line }, mid_row_nums)
133+
chars = chunkHelper.repeated(self.conf.chars.vertical_line, mid_row_nums)
134134
-- when use click `<<` or `>>` to indent, we should make sure the line would not encounter the indent char
135135
for i = 1, mid_row_nums do
136136
local line = cFunc.get_line(range.bufnr, range.start + i)
@@ -144,7 +144,7 @@ function ChunkMod:get_chunk_data(range, virt_text_list, row_list, virt_text_win_
144144
end
145145
end
146146
end
147-
vim.list_extend(virt_text_list, chars)
147+
chunkHelper.list_extend(virt_text_list, chars)
148148

149149
if end_blank_len > 0 then
150150
local virt_text_width = end_blank_len - start_col
@@ -183,9 +183,9 @@ function ChunkMod:get_chunk_data(range, virt_text_list, row_list, virt_text_win_
183183
local virt_text, virt_text_win_col =
184184
chunkHelper.calc(end_virt_text, start_col, self.meta.leftcol, self.meta.shiftwidth)
185185
local char_list = utf8Split(virt_text)
186-
vim.list_extend(virt_text_list, char_list)
187-
vim.list_extend(row_list, vim.fn["repeat"]({ range.finish }, #char_list))
188-
vim.list_extend(
186+
chunkHelper.list_extend(virt_text_list, char_list)
187+
chunkHelper.list_extend(row_list, chunkHelper.repeated(range.finish, #char_list))
188+
chunkHelper.list_extend(
189189
virt_text_win_col_list,
190190
chunkHelper.getColList(char_list, virt_text_win_col, self.meta.shiftwidth)
191191
)

lua/hlchunk/utils/chunkHelper.lua

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,68 @@ local function get_chunk_range_by_treesitter(pos)
9191
return chunkHelper.CHUNK_RANGE_RET.NO_CHUNK, Scope(pos.bufnr, -1, -1)
9292
end
9393

94-
---@param opts? {pos: HlChunk.Pos, use_treesitter: boolean}
94+
---@param char string
95+
---@param shiftwidth integer
96+
---@return integer
97+
local function virt_text_char_width(char, shiftwidth)
98+
local b1 = char:byte(1)
99+
if b1 == 0x00 then
100+
-- NULL is a terminator when used in virtual texts
101+
return 0
102+
elseif b1 == 0x09 then
103+
return shiftwidth
104+
elseif b1 <= 0x1F or b1 == 0x7F then
105+
-- control chars other than NULL and TAB are two cells wide
106+
return 2
107+
elseif b1 <= 0x7F then
108+
-- other ASCII chars are single cell wide
109+
return 1
110+
else
111+
return vim.api.nvim_strwidth(char)
112+
end
113+
end
114+
115+
---faster alternative to `vim.fn.reverse()`
116+
---unlike the original, this only supports lists
117+
---@generic T
118+
---@param list T[]
119+
---@return T[]
120+
function chunkHelper.listReverse(list)
121+
local dst = {}
122+
for i, v in ipairs(list) do
123+
dst[#list + 1 - i] = v
124+
end
125+
return dst
126+
end
127+
128+
---faster alternative to `vim.fn.repeat()`
129+
---unlike the original, the input will be repeated as-is and the output will always be a list
130+
---@generic T
131+
---@param input T
132+
---@param count integer
133+
---@return T[]
134+
function chunkHelper.repeated(input, count)
135+
local dst = {}
136+
for i = 1, count do
137+
dst[i] = input
138+
end
139+
return dst
140+
end
141+
142+
---faster alternative to `vim.list_extend()` (mutates dst!)
143+
---unlike the original, this function lacks validation and range support
144+
---@generic T
145+
---@param dst T[]
146+
---@param src T[]
147+
---@return T[] dst
148+
function chunkHelper.list_extend(dst, src)
149+
for i = 1, #src do
150+
dst[#dst + 1] = src[i]
151+
end
152+
return dst
153+
end
154+
155+
---@param opts? {pos: Pos, use_treesitter: boolean}
95156
---@return CHUNK_RANGE_RETCODE enum
96157
---@return HlChunk.Scope
97158
function chunkHelper.get_chunk_range(opts)
@@ -127,7 +188,7 @@ end
127188
function chunkHelper.utf8Split(inputstr)
128189
local list = {}
129190
for uchar in string.gmatch(inputstr, "[^\128-\191][\128-\191]*") do
130-
table.insert(list, uchar)
191+
list[#list + 1] = uchar
131192
end
132193
return list
133194
end
@@ -139,7 +200,7 @@ function chunkHelper.rangeFromTo(i, j, step)
139200
local t = {}
140201
step = step or 1
141202
for x = i, j, step do
142-
table.insert(t, x)
203+
t[#t + 1] = x
143204
end
144205
return t
145206
end
@@ -152,8 +213,8 @@ function chunkHelper.getColList(char_list, leftcol, shiftwidth)
152213
local t = {}
153214
local next_col = leftcol
154215
for i = 1, #char_list do
155-
table.insert(t, next_col)
156-
next_col = next_col + chunkHelper.virtTextStrWidth(char_list[i], shiftwidth)
216+
t[#t + 1] = next_col
217+
next_col = next_col + virt_text_char_width(char_list[i], shiftwidth)
157218
end
158219
return t
159220
end
@@ -183,7 +244,7 @@ function chunkHelper.repeatToWidth(str, width, shiftwidth)
183244
local current_width = str_width * repeatable_len
184245
local i = 1
185246
while i <= #chars do
186-
local char_width = chunkHelper.virtTextStrWidth(chars[i], shiftwidth)
247+
local char_width = virt_text_char_width(chars[i], shiftwidth)
187248
---assumed to be an out-of-bounds char (like in nerd fonts) followed by a whitespace if true
188249
local likely_oob_char =
189250
-- single-cell
@@ -235,18 +296,13 @@ end
235296
---@return boolean
236297
function chunkHelper.checkCellsBlank(line, start_col, end_col, shiftwidth)
237298
local current_col = 1
238-
local current_byte = 1
239299
local current_char = 1
240-
while current_byte <= #line and current_col <= end_col do
241-
local final_byte = vim.str_byteindex(line, current_char)
242-
local char = line:sub(current_byte, final_byte)
300+
local chars = chunkHelper.utf8Split(line)
301+
while current_char <= #chars and current_col <= end_col do
302+
local char = chars[current_char]
243303
local b1, b2, b3 = char:byte(1, 3)
244-
if char == "" then
245-
break
246-
end
247304
---@type integer
248305
local next_col
249-
local next_byte = final_byte + 1
250306
local next_char = current_char + 1
251307
if char == " " then
252308
next_col = current_col + 1
@@ -260,13 +316,11 @@ function chunkHelper.checkCellsBlank(line, start_col, end_col, shiftwidth)
260316
next_col = current_col + 1
261317
else
262318
local char_width = vim.api.nvim_strwidth(char)
263-
local next_byte_peek = line:byte(final_byte + 1)
264-
if char_width == 1 and next_byte_peek == 0x20 then
319+
if char_width == 1 and chars[current_char + 1] == " " then
265320
-- the char is assumed to be an out-of-bounds char (like in nerd fonts)
266321
-- followed by a whitespace
267322
next_col = current_col + 2
268323
-- skip the whitespace part of out-of-bounds char + " "
269-
next_byte = next_byte + 1
270324
next_char = next_char + 1
271325
else
272326
next_col = current_col + char_width
@@ -338,7 +392,6 @@ function chunkHelper.checkCellsBlank(line, start_col, end_col, shiftwidth)
338392
return false
339393
end
340394
current_col = next_col
341-
current_byte = next_byte
342395
current_char = next_char
343396
end
344397
return true
@@ -351,25 +404,11 @@ end
351404
function chunkHelper.virtTextStrWidth(str, shiftwidth, stop_on_null)
352405
local current_width = 0
353406
for _, char in ipairs(chunkHelper.utf8Split(str)) do
354-
if char == "\0" then
355-
if stop_on_null then
356-
return current_width
357-
end
358-
-- just ignore otherwise
359-
elseif char == "\t" then
360-
current_width = current_width + shiftwidth
361-
else
362-
local b1 = char:byte(1)
363-
if b1 <= 0x1F or b1 == 0x7F then
364-
-- control chars other than NULL and TAB are two cells wide
365-
current_width = current_width + 2
366-
elseif b1 <= 0x7F then
367-
-- other ASCII chars are single cell wide
368-
current_width = current_width + 1
369-
else
370-
current_width = current_width + vim.api.nvim_strwidth(char)
371-
end
407+
if stop_on_null and char == "\0" then
408+
-- NULL is a terminator when used in virtual texts
409+
return current_width
372410
end
411+
current_width = current_width + virt_text_char_width(char, shiftwidth)
373412
end
374413
return current_width
375414
end

test/features/chunkHelper_spec.lua

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,32 @@ describe("indentHelper", function()
134134
end
135135
end)
136136

137+
it("listReverse happy path", function()
138+
local inputList = {
139+
{ t = {}, res = {} },
140+
{ t = { 1 }, res = { 1 } },
141+
{ t = { 1, 2, 3 }, res = { 3, 2, 1 } },
142+
{ t = { 1, 2, 3, 4 }, res = { 4, 3, 2, 1 } },
143+
}
144+
145+
for _, testCase in ipairs(inputList) do
146+
local res = chunkHelper.listReverse(testCase.t)
147+
assert.same(res, testCase.res)
148+
end
149+
end)
150+
151+
it("repeated happy path", function()
152+
local inputList = {
153+
{ input = 1, repeat_to = 1, res = { 1 } },
154+
{ input = 1, repeat_to = 3, res = { 1, 1, 1 } },
155+
}
156+
157+
for _, testCase in ipairs(inputList) do
158+
local res = chunkHelper.repeated(testCase.input, testCase.repeat_to)
159+
assert.same(res, testCase.res)
160+
end
161+
end)
162+
137163
it("checkCellsBlank happy path", function()
138164
-- bunch of edge cases
139165
local inputList = {
@@ -218,4 +244,17 @@ describe("indentHelper", function()
218244
assert.same(res, testCase.res)
219245
end
220246
end)
247+
248+
it("list_extend happy path", function()
249+
local inputList = {
250+
{ dst = { 1, 2, 3 }, src = {}, res = { 1, 2, 3 } },
251+
{ dst = {}, src = { 4, 5, 6 }, res = { 4, 5, 6 } },
252+
{ dst = { 1, 2, 3 }, src = { 4, 5, 6 }, res = { 1, 2, 3, 4, 5, 6 } },
253+
}
254+
255+
for _, testCase in ipairs(inputList) do
256+
chunkHelper.list_extend(testCase.dst, testCase.src)
257+
assert.same(testCase.dst, testCase.res)
258+
end
259+
end)
221260
end)

0 commit comments

Comments
 (0)