Skip to content

Commit 5dbb1be

Browse files
committed
perf: fix performance regression
1 parent 2232a55 commit 5dbb1be

File tree

3 files changed

+125
-47
lines changed

3 files changed

+125
-47
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: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,67 @@ local function get_chunk_range_by_treesitter(pos)
8787
return chunkHelper.CHUNK_RANGE_RET.NO_CHUNK, Scope(pos.bufnr, -1, -1)
8888
end
8989

90+
---@param char string
91+
---@param shiftwidth integer
92+
---@return integer
93+
local function virt_text_char_width(char, shiftwidth)
94+
local b1 = char:byte(1)
95+
if b1 == 0x00 then
96+
-- NULL is a terminator when used in virtual texts
97+
return 0
98+
elseif b1 == 0x09 then
99+
return shiftwidth
100+
elseif b1 <= 0x1F or b1 == 0x7F then
101+
-- control chars other than NULL and TAB are two cells wide
102+
return 2
103+
elseif b1 <= 0x7F then
104+
-- other ASCII chars are single cell wide
105+
return 1
106+
else
107+
return vim.api.nvim_strwidth(char)
108+
end
109+
end
110+
111+
---faster alternative to `vim.fn.reverse()`
112+
---unlike the original, this only supports lists
113+
---@generic T
114+
---@param list T[]
115+
---@return T[]
116+
function chunkHelper.listReverse(list)
117+
local dst = {}
118+
for i, v in ipairs(list) do
119+
dst[#list + 1 - i] = v
120+
end
121+
return dst
122+
end
123+
124+
---faster alternative to `vim.fn.repeat()`
125+
---unlike the original, the input will be repeated as-is and the output will always be a list
126+
---@generic T
127+
---@param input T
128+
---@param count integer
129+
---@return T[]
130+
function chunkHelper.repeated(input, count)
131+
local dst = {}
132+
for i = 1, count do
133+
dst[i] = input
134+
end
135+
return dst
136+
end
137+
138+
---faster alternative to `vim.list_extend()` (mutates dst!)
139+
---unlike the original, this function lacks validation and range support
140+
---@generic T
141+
---@param dst T[]
142+
---@param src T[]
143+
---@return T[] dst
144+
function chunkHelper.list_extend(dst, src)
145+
for i = 1, #src do
146+
dst[#dst + 1] = src[i]
147+
end
148+
return dst
149+
end
150+
90151
---@param opts? {pos: Pos, use_treesitter: boolean}
91152
---@return CHUNK_RANGE_RETCODE enum
92153
---@return Scope
@@ -123,7 +184,7 @@ end
123184
function chunkHelper.utf8Split(inputstr)
124185
local list = {}
125186
for uchar in string.gmatch(inputstr, "[^\128-\191][\128-\191]*") do
126-
table.insert(list, uchar)
187+
list[#list + 1] = uchar
127188
end
128189
return list
129190
end
@@ -135,7 +196,7 @@ function chunkHelper.rangeFromTo(i, j, step)
135196
local t = {}
136197
step = step or 1
137198
for x = i, j, step do
138-
table.insert(t, x)
199+
t[#t + 1] = x
139200
end
140201
return t
141202
end
@@ -148,8 +209,8 @@ function chunkHelper.getColList(char_list, leftcol, shiftwidth)
148209
local t = {}
149210
local next_col = leftcol
150211
for i = 1, #char_list do
151-
table.insert(t, next_col)
152-
next_col = next_col + chunkHelper.virtTextStrWidth(char_list[i], shiftwidth)
212+
t[#t + 1] = next_col
213+
next_col = next_col + virt_text_char_width(char_list[i], shiftwidth)
153214
end
154215
return t
155216
end
@@ -179,7 +240,7 @@ function chunkHelper.repeatToWidth(str, width, shiftwidth)
179240
local current_width = str_width * repeatable_len
180241
local i = 1
181242
while i <= #chars do
182-
local char_width = chunkHelper.virtTextStrWidth(chars[i], shiftwidth)
243+
local char_width = virt_text_char_width(chars[i], shiftwidth)
183244
---assumed to be an out-of-bounds char (like in nerd fonts) followed by a whitespace if true
184245
local likely_oob_char =
185246
-- single-cell
@@ -231,18 +292,13 @@ end
231292
---@return boolean
232293
function chunkHelper.checkCellsBlank(line, start_col, end_col, shiftwidth)
233294
local current_col = 1
234-
local current_byte = 1
235295
local current_char = 1
236-
while current_byte <= #line and current_col <= end_col do
237-
local final_byte = vim.str_byteindex(line, current_char)
238-
local char = line:sub(current_byte, final_byte)
296+
local chars = chunkHelper.utf8Split(line)
297+
while current_char <= #chars and current_col <= end_col do
298+
local char = chars[current_char]
239299
local b1, b2, b3 = char:byte(1, 3)
240-
if char == "" then
241-
break
242-
end
243300
---@type integer
244301
local next_col
245-
local next_byte = final_byte + 1
246302
local next_char = current_char + 1
247303
if char == " " then
248304
next_col = current_col + 1
@@ -256,13 +312,11 @@ function chunkHelper.checkCellsBlank(line, start_col, end_col, shiftwidth)
256312
next_col = current_col + 1
257313
else
258314
local char_width = vim.api.nvim_strwidth(char)
259-
local next_byte_peek = line:byte(final_byte + 1)
260-
if char_width == 1 and next_byte_peek == 0x20 then
315+
if char_width == 1 and chars[current_char + 1] == " " then
261316
-- the char is assumed to be an out-of-bounds char (like in nerd fonts)
262317
-- followed by a whitespace
263318
next_col = current_col + 2
264319
-- skip the whitespace part of out-of-bounds char + " "
265-
next_byte = next_byte + 1
266320
next_char = next_char + 1
267321
else
268322
next_col = current_col + char_width
@@ -334,7 +388,6 @@ function chunkHelper.checkCellsBlank(line, start_col, end_col, shiftwidth)
334388
return false
335389
end
336390
current_col = next_col
337-
current_byte = next_byte
338391
current_char = next_char
339392
end
340393
return true
@@ -347,25 +400,11 @@ end
347400
function chunkHelper.virtTextStrWidth(str, shiftwidth, stop_on_null)
348401
local current_width = 0
349402
for _, char in ipairs(chunkHelper.utf8Split(str)) do
350-
if char == "\0" then
351-
if stop_on_null then
352-
return current_width
353-
end
354-
-- just ignore otherwise
355-
elseif char == "\t" then
356-
current_width = current_width + shiftwidth
357-
else
358-
local b1 = char:byte(1)
359-
if b1 <= 0x1F or b1 == 0x7F then
360-
-- control chars other than NULL and TAB are two cells wide
361-
current_width = current_width + 2
362-
elseif b1 <= 0x7F then
363-
-- other ASCII chars are single cell wide
364-
current_width = current_width + 1
365-
else
366-
current_width = current_width + vim.api.nvim_strwidth(char)
367-
end
403+
if stop_on_null and char == "\0" then
404+
-- NULL is a terminator when used in virtual texts
405+
return current_width
368406
end
407+
current_width = current_width + virt_text_char_width(char, shiftwidth)
369408
end
370409
return current_width
371410
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)