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 */
5969export 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 */
72104export 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 */
96115export 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 */
117125export 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