Skip to content

Commit ed1446c

Browse files
committed
feat: add wide/multiple char string and nerd font support for chunk
1 parent 60900e2 commit ed1446c

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
@@ -104,8 +104,13 @@ function chunkHelper.get_chunk_range(opts)
104104
end
105105
end
106106

107-
function chunkHelper.calc(str, col, leftcol)
108-
local len = vim.api.nvim_strwidth(str)
107+
---@param str string
108+
---@param col integer
109+
---@param leftcol integer
110+
---@param shiftwidth integer
111+
---@return string, integer
112+
function chunkHelper.calc(str, col, leftcol, shiftwidth)
113+
local len = chunkHelper.virtTextStrWidth(str, shiftwidth)
109114
if col < leftcol then
110115
local byte_idx = math.min(leftcol - col, len)
111116
local utf_beg = vim.str_byteindex(str, byte_idx)
@@ -117,6 +122,8 @@ function chunkHelper.calc(str, col, leftcol)
117122
return str, col
118123
end
119124

125+
---@param inputstr string
126+
---@return string[]
120127
function chunkHelper.utf8Split(inputstr)
121128
local list = {}
122129
for uchar in string.gmatch(inputstr, "[^\128-\191][\128-\191]*") do
@@ -137,6 +144,76 @@ function chunkHelper.rangeFromTo(i, j, step)
137144
return t
138145
end
139146

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

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

0 commit comments

Comments
 (0)