@@ -18,22 +18,188 @@ export function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: stri
1818 return measureTextCache . get ( text ) !
1919}
2020
21- function buildTrimmedText ( text : string , length : number ) {
22- const prefixLength = Math . floor ( length / 2 )
21+ interface TrimmedTextResult {
22+ trimmedString : string
23+ trimmedLength : number
24+ prefixLength : number
25+ suffixLength : number
26+ originalLength : number
27+ originalString : string
28+ }
29+
30+ // Trim text, placing an ellipsis in the middle, with a slight bias towards
31+ // keeping text from the beginning rather than the end
32+ export function buildTrimmedText ( text : string , length : number ) : TrimmedTextResult {
33+ if ( text . length <= length ) {
34+ return {
35+ trimmedString : text ,
36+ trimmedLength : text . length ,
37+ prefixLength : text . length ,
38+ suffixLength : 0 ,
39+ originalString : text ,
40+ originalLength : text . length ,
41+ }
42+ }
43+
44+ let prefixLength = Math . floor ( length / 2 )
45+ const suffixLength = length - prefixLength - 1
2346 const prefix = text . substr ( 0 , prefixLength )
24- const suffix = text . substr ( text . length - prefixLength , prefixLength )
25- return prefix + ELLIPSIS + suffix
47+ const suffix = text . substr ( text . length - suffixLength , suffixLength )
48+ const trimmedString = prefix + ELLIPSIS + suffix
49+ return {
50+ trimmedString,
51+ trimmedLength : trimmedString . length ,
52+ prefixLength : prefix . length ,
53+ suffixLength : suffix . length ,
54+ originalString : text ,
55+ originalLength : text . length ,
56+ }
2657}
2758
28- export function trimTextMid ( ctx : CanvasRenderingContext2D , text : string , maxWidth : number ) {
29- if ( cachedMeasureTextWidth ( ctx , text ) <= maxWidth ) return text
59+ // Trim text to fit within the given number of pixels on the canvas
60+ export function trimTextMid (
61+ ctx : CanvasRenderingContext2D ,
62+ text : string ,
63+ maxWidth : number ,
64+ ) : TrimmedTextResult {
65+ if ( cachedMeasureTextWidth ( ctx , text ) <= maxWidth ) {
66+ return buildTrimmedText ( text , text . length )
67+ }
3068 const [ lo ] = binarySearch (
3169 0 ,
3270 text . length ,
3371 n => {
34- return cachedMeasureTextWidth ( ctx , buildTrimmedText ( text , n ) )
72+ return cachedMeasureTextWidth ( ctx , buildTrimmedText ( text , n ) . trimmedString )
3573 } ,
3674 maxWidth ,
3775 )
3876 return buildTrimmedText ( text , lo )
3977}
78+
79+ enum IndexTypeInTrimmed {
80+ IN_PREFIX ,
81+ IN_SUFFIX ,
82+ ELIDED ,
83+ }
84+
85+ function getIndexTypeInTrimmed ( result : TrimmedTextResult , index : number ) : IndexTypeInTrimmed {
86+ if ( index < result . prefixLength ) {
87+ return IndexTypeInTrimmed . IN_PREFIX
88+ } else if ( index < result . originalLength - result . suffixLength ) {
89+ return IndexTypeInTrimmed . ELIDED
90+ } else {
91+ return IndexTypeInTrimmed . IN_SUFFIX
92+ }
93+ }
94+
95+ export function remapRangesToTrimmedText (
96+ trimmedText : TrimmedTextResult ,
97+ ranges : [ number , number ] [ ] ,
98+ ) : [ number , number ] [ ] {
99+ // We intentionally don't just re-run fuzzy matching on the trimmed
100+ // text, beacuse if the search query is "helloWorld", the frame name
101+ // is "application::helloWorld", and that gets trimmed down to
102+ // "appl...oWorld", we still want "oWorld" to be highlighted, even
103+ // though the string "appl...oWorld" is not matched by the query
104+ // "helloWorld".
105+ //
106+ // There's a weird case to consider here: what if the trimmedText is
107+ // also matched by the query, but results in a different match than
108+ // the original query? Consider, e.g. the search string of "ab". The
109+ // string "hello ab shabby" will be matched at the first "ab", but
110+ // may be trimmed to "hello...shabby". In this case, should we
111+ // highlight the "ab" hidden by the ellipsis, or the "ab" in
112+ // "shabby"? The code below highlights the ellipsis so that the
113+ // matched characters don't change as you zoom in and out.
114+
115+ const rangesToHighlightInTrimmedText : [ number , number ] [ ] = [ ]
116+ const lengthLoss = trimmedText . originalLength - trimmedText . trimmedLength
117+ let highlightedEllipsis = false
118+
119+ for ( let [ origStart , origEnd ] of ranges ) {
120+ let startPosType = getIndexTypeInTrimmed ( trimmedText , origStart )
121+ let endPosType = getIndexTypeInTrimmed ( trimmedText , origEnd - 1 )
122+
123+ switch ( startPosType ) {
124+ case IndexTypeInTrimmed . IN_PREFIX : {
125+ switch ( endPosType ) {
126+ case IndexTypeInTrimmed . IN_PREFIX : {
127+ // The entire range fits in the prefix. Add it unmodified.
128+ rangesToHighlightInTrimmedText . push ( [ origStart , origEnd ] )
129+ break
130+ }
131+ case IndexTypeInTrimmed . ELIDED : {
132+ // The range starts in the prefix, but ends in the elided
133+ // section. Add just the prefix + one char for the ellipsis.
134+ rangesToHighlightInTrimmedText . push ( [
135+ origStart ,
136+ origStart + trimmedText . prefixLength + 1 ,
137+ ] )
138+ highlightedEllipsis = true
139+ break
140+ }
141+ case IndexTypeInTrimmed . IN_SUFFIX : {
142+ // The range crosses from the prefix to the suffix.
143+ // Highlight everything including the ellipsis.
144+ rangesToHighlightInTrimmedText . push ( [ origStart , origEnd - lengthLoss ] )
145+ break
146+ }
147+ }
148+ break
149+ }
150+ case IndexTypeInTrimmed . ELIDED : {
151+ switch ( endPosType ) {
152+ case IndexTypeInTrimmed . IN_PREFIX : {
153+ // This should be impossible
154+ throw new Error ( 'Unexpected highlight range starts in elided and ends in prefix' )
155+ }
156+ case IndexTypeInTrimmed . ELIDED : {
157+ // The match starts & ends within the elided section.
158+ if ( ! highlightedEllipsis ) {
159+ rangesToHighlightInTrimmedText . push ( [
160+ trimmedText . prefixLength ,
161+ trimmedText . prefixLength + 1 ,
162+ ] )
163+ highlightedEllipsis = true
164+ }
165+ break
166+ }
167+ case IndexTypeInTrimmed . IN_SUFFIX : {
168+ // The match starts in elided, but ends in suffix.
169+ if ( highlightedEllipsis ) {
170+ rangesToHighlightInTrimmedText . push ( [
171+ trimmedText . trimmedLength - trimmedText . suffixLength ,
172+ origEnd - lengthLoss ,
173+ ] )
174+ } else {
175+ rangesToHighlightInTrimmedText . push ( [ trimmedText . prefixLength , origEnd - lengthLoss ] )
176+ highlightedEllipsis = true
177+ }
178+ break
179+ }
180+ }
181+ break
182+ }
183+ case IndexTypeInTrimmed . IN_SUFFIX : {
184+ switch ( endPosType ) {
185+ case IndexTypeInTrimmed . IN_PREFIX : {
186+ // This should be impossible
187+ throw new Error ( 'Unexpected highlight range starts in suffix and ends in prefix' )
188+ }
189+ case IndexTypeInTrimmed . ELIDED : {
190+ // This should be impossible
191+ throw new Error ( 'Unexpected highlight range starts in suffix and ends in elided' )
192+ break
193+ }
194+ case IndexTypeInTrimmed . IN_SUFFIX : {
195+ // Match starts & ends in suffix
196+ rangesToHighlightInTrimmedText . push ( [ origStart - lengthLoss , origEnd - lengthLoss ] )
197+ break
198+ }
199+ }
200+ break
201+ }
202+ }
203+ }
204+ return rangesToHighlightInTrimmedText
205+ }
0 commit comments