Skip to content

Commit 42e7e54

Browse files
authored
fix: improve Chinese character width calculation in suggestion truncation (#780)
1 parent ca4126d commit 42e7e54

1 file changed

Lines changed: 52 additions & 10 deletions

File tree

src/ui/Suggestion.tsx

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,40 +60,82 @@ const ELLIPSIS_WIDTH = 3;
6060
const MIN_DESC_WIDTH = 20;
6161
const MIN_MAIN_DESC_WIDTH = 10;
6262

63+
function getCharDisplayWidth(code: number): number {
64+
if (
65+
(code >= 0x1100 && code <= 0x115f) ||
66+
(code >= 0x2e80 && code <= 0x303e) ||
67+
(code >= 0x3040 && code <= 0xa4cf) ||
68+
(code >= 0xac00 && code <= 0xd7af) ||
69+
(code >= 0xf900 && code <= 0xfaff) ||
70+
(code >= 0xfe10 && code <= 0xfe1f) ||
71+
(code >= 0xfe30 && code <= 0xfe6f) ||
72+
(code >= 0xff00 && code <= 0xff60) ||
73+
(code >= 0xffe0 && code <= 0xffe6) ||
74+
(code >= 0x1b000 && code <= 0x1b77f) ||
75+
(code >= 0x1f300 && code <= 0x1f64f) ||
76+
(code >= 0x1f900 && code <= 0x1f9ff) ||
77+
(code >= 0x20000 && code <= 0x2a6df) ||
78+
(code >= 0x2a700 && code <= 0x2ceaf) ||
79+
(code >= 0x2ceb0 && code <= 0x2ebef) ||
80+
(code >= 0x30000 && code <= 0x3134f)
81+
) {
82+
return 2;
83+
}
84+
return 1;
85+
}
86+
87+
function getStringDisplayWidth(str: string): number {
88+
let width = 0;
89+
for (const char of str) {
90+
const code = char.codePointAt(0) ?? 0;
91+
width += getCharDisplayWidth(code);
92+
}
93+
return width;
94+
}
95+
96+
function truncateToDisplayWidth(str: string, maxWidth: number): string {
97+
let width = 0;
98+
let i = 0;
99+
for (const char of str) {
100+
const code = char.codePointAt(0) ?? 0;
101+
const charWidth = getCharDisplayWidth(code);
102+
if (width + charWidth > maxWidth) break;
103+
width += charWidth;
104+
i += char.length;
105+
}
106+
return str.slice(0, i);
107+
}
108+
63109
export function SuggestionItem({
64110
name,
65111
description,
66112
isSelected,
67113
firstColumnWidth,
68114
maxWidth,
69115
}: SuggestionItemProps) {
70-
// Calculate available width for description
71-
// Account for: margin + firstColumnWidth + spacing + ellipsis reserve
72116
const reservedWidth =
73117
MARGIN_LEFT + firstColumnWidth + SPACING + ELLIPSIS_WIDTH;
74118
const maxDescriptionWidth = Math.max(
75119
MIN_DESC_WIDTH,
76120
maxWidth - reservedWidth,
77121
);
78122

79-
// Extract source suffix (content in the last parentheses, e.g., "(global)")
80123
const sourceMatch = description.match(/\(([^)]+)\)$/);
81124
const sourceSuffix = sourceMatch ? ` ${sourceMatch[0]}` : '';
82125
const mainDescription = sourceMatch
83126
? description.slice(0, sourceMatch.index).trim()
84127
: description;
85128

86-
// Truncate description if it exceeds max width, but preserve source suffix
87129
let truncatedDescription: string;
88-
if (description.length > maxDescriptionWidth) {
130+
const descDisplayWidth = getStringDisplayWidth(description);
131+
if (descDisplayWidth > maxDescriptionWidth) {
132+
const sourceSuffixWidth = getStringDisplayWidth(sourceSuffix);
89133
const availableForMain =
90-
maxDescriptionWidth - sourceSuffix.length - ELLIPSIS_WIDTH; // Reserve space for "..." and source
134+
maxDescriptionWidth - sourceSuffixWidth - ELLIPSIS_WIDTH;
91135
if (availableForMain > MIN_MAIN_DESC_WIDTH) {
92-
// If we have enough space, truncate main description and append source
93-
truncatedDescription = `${mainDescription.slice(0, availableForMain)}...${sourceSuffix}`;
136+
truncatedDescription = `${truncateToDisplayWidth(mainDescription, availableForMain)}...${sourceSuffix}`;
94137
} else {
95-
// If space is too tight, just truncate everything
96-
truncatedDescription = `${description.slice(0, maxDescriptionWidth - ELLIPSIS_WIDTH)}...`;
138+
truncatedDescription = `${truncateToDisplayWidth(description, maxDescriptionWidth - ELLIPSIS_WIDTH)}...`;
97139
}
98140
} else {
99141
truncatedDescription = description;

0 commit comments

Comments
 (0)