Skip to content

Commit 2232a55

Browse files
committed
feat: add wide/multiple char string and nerd font support for chunk
1 parent ee2621a commit 2232a55

File tree

3 files changed

+453
-25
lines changed

3 files changed

+453
-25
lines changed

lua/hlchunk/mods/chunk/init.lua

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,46 +78,117 @@ function ChunkMod:get_chunk_data(range, virt_text_list, row_list, virt_text_win_
7878
local start_col = math.max(math.min(beg_blank_len, end_blank_len) - self.meta.shiftwidth, 0)
7979

8080
if beg_blank_len > 0 then
81-
local virt_text_len = beg_blank_len - start_col
82-
local beg_virt_text = self.conf.chars.left_top
83-
.. self.conf.chars.horizontal_line:rep(virt_text_len - 2)
84-
.. (virt_text_len > 1 and self.conf.chars.left_arrow or "")
85-
local virt_text, virt_text_win_col = chunkHelper.calc(beg_virt_text, start_col, self.meta.leftcol)
86-
local char_list = fn.reverse(utf8Split(virt_text))
87-
vim.list_extend(virt_text_list, char_list)
81+
local virt_text_width = beg_blank_len - start_col
82+
local left_top_width = chunkHelper.virtTextStrWidth(self.conf.chars.left_top, self.meta.shiftwidth)
83+
local left_arrow_width = chunkHelper.virtTextStrWidth(self.conf.chars.left_arrow, self.meta.shiftwidth)
84+
---@type string
85+
local beg_virt_text
86+
if left_top_width + left_arrow_width <= virt_text_width then
87+
-- ╭─<if () {
88+
--
89+
-- ╭<if () {
90+
--
91+
beg_virt_text = self.conf.chars.left_top
92+
.. chunkHelper.repeatToWidth(
93+
self.conf.chars.horizontal_line,
94+
virt_text_width - left_top_width - left_arrow_width,
95+
self.meta.shiftwidth
96+
)
97+
.. self.conf.chars.left_arrow
98+
elseif left_top_width <= virt_text_width then
99+
-- ╭─if () {
100+
--
101+
-- ╭if () {
102+
--
103+
beg_virt_text = self.conf.chars.left_top
104+
.. chunkHelper.repeatToWidth(
105+
self.conf.chars.horizontal_line,
106+
virt_text_width - left_top_width,
107+
self.meta.shiftwidth
108+
)
109+
else
110+
-- if () {
111+
--
112+
beg_virt_text = string.rep(" ", virt_text_width)
113+
end
114+
local virt_text, virt_text_win_col =
115+
chunkHelper.calc(beg_virt_text, start_col, self.meta.leftcol, self.meta.shiftwidth)
116+
local char_list = utf8Split(virt_text)
117+
vim.list_extend(virt_text_list, vim.fn.reverse(char_list))
88118
vim.list_extend(row_list, vim.fn["repeat"]({ range.start }, #char_list))
89-
vim.list_extend(virt_text_win_col_list, rangeFromTo(virt_text_win_col + #char_list - 1, virt_text_win_col, -1))
119+
vim.list_extend(
120+
virt_text_win_col_list,
121+
vim.fn.reverse(chunkHelper.getColList(char_list, virt_text_win_col, self.meta.shiftwidth))
122+
)
90123
end
91124

92-
local mid_char_nums = range.finish - range.start - 1
125+
local mid_row_nums = range.finish - range.start - 1
93126
vim.list_extend(row_list, rangeFromTo((range.start + 1), (range.finish - 1)))
94-
vim.list_extend(virt_text_win_col_list, vim.fn["repeat"]({ start_col - self.meta.leftcol }, mid_char_nums))
95-
local mid = self.conf.chars.vertical_line:rep(mid_char_nums)
127+
vim.list_extend(virt_text_win_col_list, vim.fn["repeat"]({ start_col - self.meta.leftcol }, mid_row_nums))
128+
---@type string[]
96129
local chars
97130
if start_col - self.meta.leftcol < 0 then
98-
chars = vim.fn["repeat"]({ "" }, mid_char_nums)
131+
chars = vim.fn["repeat"]({ "" }, mid_row_nums)
99132
else
100-
chars = utf8Split(mid)
133+
chars = vim.fn["repeat"]({ self.conf.chars.vertical_line }, mid_row_nums)
101134
-- when use click `<<` or `>>` to indent, we should make sure the line would not encounter the indent char
102-
for i = 1, mid_char_nums do
103-
local char = Pos.get_char_at_pos(Pos(range.bufnr, range.start + i, start_col), self.meta.shiftwidth)
104-
if not char:match("%s") and #char ~= 0 then
135+
for i = 1, mid_row_nums do
136+
local line = cFunc.get_line(range.bufnr, range.start + i)
137+
local vertical_line_width =
138+
-- here we need to stop virtTextStrWidth at NULL;
139+
-- "mid" virtual texts are not separated and they will be terminated on NULL
140+
chunkHelper.virtTextStrWidth(self.conf.chars.vertical_line, self.meta.shiftwidth, true)
141+
local end_col = start_col + vertical_line_width
142+
if not chunkHelper.checkCellsBlank(line, start_col + 1, end_col, self.meta.shiftwidth) then
105143
chars[i] = ""
106144
end
107145
end
108146
end
109147
vim.list_extend(virt_text_list, chars)
110148

111149
if end_blank_len > 0 then
112-
local virt_text_len = end_blank_len - start_col
113-
local end_virt_text = self.conf.chars.left_bottom
114-
.. self.conf.chars.horizontal_line:rep(virt_text_len - 2)
115-
.. (virt_text_len > 1 and self.conf.chars.right_arrow or "")
116-
local virt_text, virt_text_win_col = chunkHelper.calc(end_virt_text, start_col, self.meta.leftcol)
150+
local virt_text_width = end_blank_len - start_col
151+
local left_bottom_width = chunkHelper.virtTextStrWidth(self.conf.chars.left_bottom, self.meta.shiftwidth)
152+
local right_arrow_width = chunkHelper.virtTextStrWidth(self.conf.chars.right_arrow, self.meta.shiftwidth)
153+
---@type string
154+
local end_virt_text
155+
if left_bottom_width + right_arrow_width <= virt_text_width then
156+
--
157+
-- ╰─>}
158+
--
159+
-- ╰>}
160+
end_virt_text = self.conf.chars.left_bottom
161+
.. chunkHelper.repeatToWidth(
162+
self.conf.chars.horizontal_line,
163+
virt_text_width - left_bottom_width - right_arrow_width,
164+
self.meta.shiftwidth
165+
)
166+
.. self.conf.chars.right_arrow
167+
elseif left_bottom_width <= virt_text_width then
168+
--
169+
-- ╰─}
170+
--
171+
-- ╰}
172+
end_virt_text = self.conf.chars.left_bottom
173+
.. chunkHelper.repeatToWidth(
174+
self.conf.chars.horizontal_line,
175+
virt_text_width - left_bottom_width,
176+
self.meta.shiftwidth
177+
)
178+
else
179+
--
180+
-- }
181+
end_virt_text = string.rep(" ", virt_text_width)
182+
end
183+
local virt_text, virt_text_win_col =
184+
chunkHelper.calc(end_virt_text, start_col, self.meta.leftcol, self.meta.shiftwidth)
117185
local char_list = utf8Split(virt_text)
118186
vim.list_extend(virt_text_list, char_list)
119187
vim.list_extend(row_list, vim.fn["repeat"]({ range.finish }, #char_list))
120-
vim.list_extend(virt_text_win_col_list, rangeFromTo(virt_text_win_col, virt_text_win_col + #char_list - 1))
188+
vim.list_extend(
189+
virt_text_win_col_list,
190+
chunkHelper.getColList(char_list, virt_text_win_col, self.meta.shiftwidth)
191+
)
121192
end
122193
end
123194

lua/hlchunk/utils/chunkHelper.lua

Lines changed: 225 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,13 @@ function chunkHelper.get_chunk_range(opts)
100100
end
101101
end
102102

103-
function chunkHelper.calc(str, col, leftcol)
104-
local len = vim.api.nvim_strwidth(str)
103+
---@param str string
104+
---@param col integer
105+
---@param leftcol integer
106+
---@param shiftwidth integer
107+
---@return string, integer
108+
function chunkHelper.calc(str, col, leftcol, shiftwidth)
109+
local len = chunkHelper.virtTextStrWidth(str, shiftwidth)
105110
if col < leftcol then
106111
local byte_idx = math.min(leftcol - col, len)
107112
local utf_beg = vim.str_byteindex(str, byte_idx)
@@ -113,6 +118,8 @@ function chunkHelper.calc(str, col, leftcol)
113118
return str, col
114119
end
115120

121+
---@param inputstr string
122+
---@return string[]
116123
function chunkHelper.utf8Split(inputstr)
117124
local list = {}
118125
for uchar in string.gmatch(inputstr, "[^\128-\191][\128-\191]*") do
@@ -133,6 +140,76 @@ function chunkHelper.rangeFromTo(i, j, step)
133140
return t
134141
end
135142

143+
---@param char_list table<integer, string>
144+
---@param leftcol integer
145+
---@param shiftwidth integer
146+
---@return integer[]
147+
function chunkHelper.getColList(char_list, leftcol, shiftwidth)
148+
local t = {}
149+
local next_col = leftcol
150+
for i = 1, #char_list do
151+
table.insert(t, next_col)
152+
next_col = next_col + chunkHelper.virtTextStrWidth(char_list[i], shiftwidth)
153+
end
154+
return t
155+
end
156+
157+
---@param str string
158+
---@param width integer
159+
---@param shiftwidth integer
160+
function chunkHelper.repeatToWidth(str, width, shiftwidth)
161+
local str_width = chunkHelper.virtTextStrWidth(str, shiftwidth)
162+
163+
-- "1" -> "1111"
164+
if str_width == 1 then
165+
return str:rep(width)
166+
end
167+
168+
-- "12" -> "1212"
169+
if width % str_width == 0 then
170+
return str:rep(width / str_width)
171+
end
172+
173+
-- "12" -> "12121"
174+
-- "1" -> "11 "
175+
-- "⏻ " -> "⏻ ⏻ "
176+
local repeatable_len = math.floor(width / str_width)
177+
local s = str:rep(repeatable_len)
178+
local chars = chunkHelper.utf8Split(str)
179+
local current_width = str_width * repeatable_len
180+
local i = 1
181+
while i <= #chars do
182+
local char_width = chunkHelper.virtTextStrWidth(chars[i], shiftwidth)
183+
---assumed to be an out-of-bounds char (like in nerd fonts) followed by a whitespace if true
184+
local likely_oob_char =
185+
-- single-cell
186+
char_width == 1
187+
-- followed by a whitespace
188+
and chars[i + 1] == " "
189+
-- non-ASCII
190+
and chars[i]:byte(1) > 0x7F
191+
local char = likely_oob_char and chars[i] .. " " or chars[i]
192+
local next_width = current_width + (likely_oob_char and 2 or char_width)
193+
if next_width < width then
194+
s = s .. char
195+
current_width = next_width
196+
elseif next_width == width then
197+
s = s .. char
198+
break
199+
else
200+
s = s .. string.rep(" ", width - current_width)
201+
break
202+
end
203+
if likely_oob_char then
204+
-- skip the whitespace part of out-of-bounds char + " "
205+
i = i + 2
206+
else
207+
i = i + 1
208+
end
209+
end
210+
return s
211+
end
212+
136213
function chunkHelper.shallowCmp(t1, t2)
137214
if #t1 ~= #t2 then
138215
return false
@@ -147,4 +224,150 @@ function chunkHelper.shallowCmp(t1, t2)
147224
return flag
148225
end
149226

227+
---@param line string
228+
---@param start_col integer
229+
---@param end_col integer
230+
---@param shiftwidth integer
231+
---@return boolean
232+
function chunkHelper.checkCellsBlank(line, start_col, end_col, shiftwidth)
233+
local current_col = 1
234+
local current_byte = 1
235+
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)
239+
local b1, b2, b3 = char:byte(1, 3)
240+
if char == "" then
241+
break
242+
end
243+
---@type integer
244+
local next_col
245+
local next_byte = final_byte + 1
246+
local next_char = current_char + 1
247+
if char == " " then
248+
next_col = current_col + 1
249+
elseif char == "\t" then
250+
next_col = current_col + shiftwidth
251+
elseif b1 <= 0x1F or char == "\127" then
252+
-- despite nvim_strwidth returning 0 or 1, control chars are 2 cells wide
253+
next_col = current_col + 2
254+
elseif b1 <= 0x7F then
255+
-- other ASCII chars are single cell wide
256+
next_col = current_col + 1
257+
else
258+
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
261+
-- the char is assumed to be an out-of-bounds char (like in nerd fonts)
262+
-- followed by a whitespace
263+
next_col = current_col + 2
264+
-- skip the whitespace part of out-of-bounds char + " "
265+
next_byte = next_byte + 1
266+
next_char = next_char + 1
267+
else
268+
next_col = current_col + char_width
269+
end
270+
end
271+
-- we're going to match these characters manually
272+
-- as we can't use "%s" to check blank cells
273+
-- (e.g. "%s" matches to "\v" but it will be printed as ^K)
274+
if
275+
(current_col >= start_col or next_col - 1 >= start_col)
276+
-- Singles
277+
--
278+
-- Indent characters
279+
-- Unicode Scripts Z*
280+
-- 0020 - SPACE
281+
and char ~= " "
282+
--
283+
-- Unicode Scripts C*
284+
-- 0009 - TAB
285+
-- control characters except TAB should be rendered like "^[" or "<200b>"
286+
and char ~= " "
287+
--
288+
-- Non indent characters
289+
-- Unicode Scripts Z*
290+
-- 00A0 - NO-BREAK SPACE
291+
and char ~= " "
292+
--[[
293+
-- 1680 - OGHAM SPACE MARK
294+
-- usually rendered as "-"
295+
-- see https://www.unicode.org/charts/PDF/U1680.pdf
296+
and char ~= " "
297+
]]
298+
-- 2000..200A - EN QUAD..HAIR SPACE
299+
-- " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "
300+
and not (b1 == 0xe2 and b2 == 0x80 and b3 >= 0x80 and b3 <= 0x8a)
301+
-- 202F - NARROW NO-BREAK SPACE
302+
and char ~= ""
303+
-- 205F - MEDIUM MATHEMATICAL SPACE
304+
and char ~= ""
305+
-- 3000 - IDEOGRAPHIC SPACE
306+
and char ~= " "
307+
--[[
308+
-- 2028 - LINE SEPARATOR
309+
-- some fonts lacks this and may render it as "?" or "█"
310+
-- as this character is usually treated as a line-break
311+
and char ~= "
"
312+
]]
313+
--[[
314+
-- 2029 - PARAGRAPH SEPARATOR
315+
-- some fonts lacks this and may render it as "?" or "█"
316+
-- as this character is usually treated as a line-break
317+
and char ~= "
"
318+
]]
319+
--
320+
-- Others
321+
-- 2800 - BRAILLE PATTERN BLANK
322+
and char ~= ""
323+
--[[
324+
-- 3164 - HANGUL FILLER
325+
-- technically "blank" but can easily break the rendering
326+
and "\227\133\164" -- do not replace this with a literal notation
327+
]]
328+
--[[
329+
-- FFA0 - HALFWIDTH HANGUL FILLER
330+
-- technically "blank" but can easily break the rendering
331+
and "\239\190\160" -- do not replace this with a literal notation
332+
]]
333+
then
334+
return false
335+
end
336+
current_col = next_col
337+
current_byte = next_byte
338+
current_char = next_char
339+
end
340+
return true
341+
end
342+
343+
---@param str string
344+
---@param shiftwidth integer
345+
---@param stop_on_null? boolean
346+
---@return integer
347+
function chunkHelper.virtTextStrWidth(str, shiftwidth, stop_on_null)
348+
local current_width = 0
349+
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
368+
end
369+
end
370+
return current_width
371+
end
372+
150373
return chunkHelper

0 commit comments

Comments
 (0)