Skip to content

Commit d4fd48f

Browse files
nullvariantclaude
andauthored
refactor(displayLimits): reuse Intl.Segmenter instance and extract common truncation logic (#97)
- Add shared graphemeSegmenter instance to avoid repeated instantiation - Extract truncateByGraphemes() as common implementation - Fix suffix length calculation to use grapheme count instead of byte length - Add defensive Math.max(0, ...) to prevent negative slice indices - Update JSDoc with environment requirements and implementation details 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Model-Raw: claude-opus-4-5-20251101 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bfd5d40 commit d4fd48f

1 file changed

Lines changed: 38 additions & 41 deletions

File tree

extensions/git-id-switcher/src/displayLimits.ts

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
* Each context has different constraints based on available space and UX requirements.
66
*/
77

8+
/**
9+
* Shared Intl.Segmenter instance for grapheme counting.
10+
* Reused across all functions to avoid repeated instantiation.
11+
*
12+
* @remarks
13+
* Intl.Segmenter is available in Node.js 16+ and modern browsers.
14+
* VS Code extensions run on Node.js 18+, so this is always available.
15+
*/
16+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
17+
818
/**
919
* Display limits for each UI context
1020
*/
@@ -57,9 +67,31 @@ export const DISPLAY_LIMITS = {
5767
* @returns The number of visible characters
5868
*/
5969
export function countGraphemes(str: string): number {
60-
// Intl.Segmenter is available in Node.js 16+ and modern browsers
61-
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
62-
return [...segmenter.segment(str)].length;
70+
return [...graphemeSegmenter.segment(str)].length;
71+
}
72+
73+
/**
74+
* Truncate a string by grapheme clusters with a suffix.
75+
* Common implementation for all truncation functions.
76+
*
77+
* Both text length and suffix length are calculated in graphemes (visible characters),
78+
* ensuring accurate truncation even with emoji or combined characters.
79+
*
80+
* @param text - The text to truncate
81+
* @param maxLength - Maximum length in graphemes (must be positive)
82+
* @param suffix - Suffix to append when truncated (default: '...')
83+
* @returns Truncated text if necessary, original text otherwise
84+
*/
85+
function truncateByGraphemes(text: string, maxLength: number, suffix: string = '...'): string {
86+
const segments = [...graphemeSegmenter.segment(text)];
87+
if (segments.length <= maxLength) {
88+
return text;
89+
}
90+
// Calculate suffix length in graphemes (not bytes) for accurate truncation
91+
const suffixGraphemeLength = [...graphemeSegmenter.segment(suffix)].length;
92+
const targetLength = Math.max(0, maxLength - suffixGraphemeLength);
93+
const truncatedSegments = segments.slice(0, targetLength);
94+
return truncatedSegments.map(s => s.segment).join('') + suffix;
6395
}
6496

6597
/**
@@ -71,20 +103,7 @@ export function countGraphemes(str: string): number {
71103
*/
72104
export function truncateForStatusBar(text: string): string {
73105
const { maxLength, truncateSuffix } = DISPLAY_LIMITS.statusBar;
74-
75-
// Count graphemes for accurate length measurement
76-
const graphemeCount = countGraphemes(text);
77-
78-
if (graphemeCount <= maxLength) {
79-
return text;
80-
}
81-
82-
// Truncate by grapheme clusters, not bytes
83-
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
84-
const segments = [...segmenter.segment(text)];
85-
const truncatedSegments = segments.slice(0, maxLength - truncateSuffix.length);
86-
87-
return truncatedSegments.map(s => s.segment).join('') + truncateSuffix;
106+
return truncateByGraphemes(text, maxLength, truncateSuffix);
88107
}
89108

90109
/**
@@ -94,18 +113,7 @@ export function truncateForStatusBar(text: string): string {
94113
* @returns Truncated text if necessary, original text otherwise
95114
*/
96115
export function truncateForQuickPickLabel(text: string): string {
97-
const { maxLength } = DISPLAY_LIMITS.quickPickLabel;
98-
const graphemeCount = countGraphemes(text);
99-
100-
if (graphemeCount <= maxLength) {
101-
return text;
102-
}
103-
104-
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
105-
const segments = [...segmenter.segment(text)];
106-
const truncatedSegments = segments.slice(0, maxLength - 3);
107-
108-
return truncatedSegments.map(s => s.segment).join('') + '...';
116+
return truncateByGraphemes(text, DISPLAY_LIMITS.quickPickLabel.maxLength);
109117
}
110118

111119
/**
@@ -115,18 +123,7 @@ export function truncateForQuickPickLabel(text: string): string {
115123
* @returns Truncated text if necessary, original text otherwise
116124
*/
117125
export function truncateForQuickPickDetail(text: string): string {
118-
const { maxLength } = DISPLAY_LIMITS.quickPickDetail;
119-
const graphemeCount = countGraphemes(text);
120-
121-
if (graphemeCount <= maxLength) {
122-
return text;
123-
}
124-
125-
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
126-
const segments = [...segmenter.segment(text)];
127-
const truncatedSegments = segments.slice(0, maxLength - 3);
128-
129-
return truncatedSegments.map(s => s.segment).join('') + '...';
126+
return truncateByGraphemes(text, DISPLAY_LIMITS.quickPickDetail.maxLength);
130127
}
131128

132129
/**

0 commit comments

Comments
 (0)