Skip to content

Commit 7d65f21

Browse files
committed
fix: handle Unicode properly when computing string lengths
When drawing icons (which are Unicode codepoints with multi-byte representations), we were running afoul of the fact that all of these Lua string operations operate on bytes instead of codepoints: - `#` to determine length - `string.len` - `string.format` with the `%-99s` format marker - `string.sub` This would cause us to not make full use of the available space: we would truncate strings too aggressively when inserting a middle ellipsis, and we wouldn't draw the selected line highlight all the way to the end of the line. The fix is to use `vim.str_utfindex` to determine width by counting codepoints, and `vim.str_byteindex` to figure out the byte index of a particular codepoint (so that we can truncate the string with `string.sub`, passing it that byte index).
1 parent 45b9b01 commit 7d65f21

File tree

1 file changed

+12
-11
lines changed

1 file changed

+12
-11
lines changed

lua/wincent/commandt/private/match_listing.lua

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,17 @@ function MatchListing:icon_getter()
5959
end
6060
end
6161

62+
local function len(str)
63+
return vim.str_utfindex(str, 'utf-32')
64+
end
65+
6266
local format_line = function(line, width, selected, truncate, get_icon)
6367
local prefix = selected and '> ' or ' '
6468

6569
local icon = get_icon and get_icon(line)
6670
local icon_length = 0
6771
if icon then
68-
icon_length = #(icon .. ' ')
72+
icon_length = len(icon .. ' ')
6973
prefix = prefix .. icon .. ' '
7074
end
7175

@@ -79,38 +83,35 @@ local format_line = function(line, width, selected, truncate, get_icon)
7983
:gsub('\t', '\\t')
8084
:gsub('\v', '\\v')
8185

82-
if #line + #prefix < width then
86+
if len(line) + len(prefix) < width then
8387
-- Line fits without trimming.
84-
elseif #line < (5 + icon_length) then
88+
elseif len(line) < (5 + icon_length) then
8589
-- Line is so short that adding an ellipsis is not practical.
8690
elseif truncate == true or truncate == 'true' or truncate == 'middle' then
8791
local half = math.floor((width - 2) / 2)
8892
local left = line:sub(1, half - 2 + width % 2)
8993
local right = line:sub(2 - half)
9094
line = left .. '...' .. right
9195
elseif truncate == 'beginning' then
92-
line = '...' .. line:sub(-width + #prefix + 3)
96+
line = '...' .. line:sub(-width + len(prefix) + 3)
9397
elseif truncate == false or truncate == 'false' or truncate == 'end' then
9498
-- Fall through; truncation will happen before the final `return`.
9599
end
96100

97101
-- Right pad so that selection highlighting is shown across full width.
98-
if width < 102 and #line > 99 then
102+
if width < 102 and len(line) > 99 then
99103
-- No padding needed.
100104
line = prefix .. line
101-
elseif width < 102 then
102-
line = prefix .. string.format('%-' .. (width - #prefix) .. 's', line)
103105
else
104-
-- Avoid: "invalid option" caused by format argument > 99.
105-
line = prefix .. string.format('%-99s', line)
106-
local diff = width - line:len()
106+
line = prefix .. line
107+
local diff = width - len(line)
107108
if diff > 0 then
108109
line = line .. string.rep(' ', diff)
109110
end
110111
end
111112

112113
-- Trim to make sure we never wrap.
113-
return line:sub(1, width)
114+
return line:sub(1, vim.str_byteindex(line, 'utf-32', width, false))
114115
end
115116

116117
function MatchListing:select(selected)

0 commit comments

Comments
 (0)