33 * @license MIT
44 */
55
6- import { ILinkProvider , ILink , Terminal , IViewportRange } from 'xterm' ;
6+ import { ILinkProvider , ILink , Terminal , IViewportRange , IBufferLine } from 'xterm' ;
77
88export interface ILinkProviderOptions {
99 hover ?( event : MouseEvent , text : string , location : IViewportRange ) : void ;
@@ -45,61 +45,51 @@ export class LinkComputer {
4545 public static computeLink ( y : number , regex : RegExp , terminal : Terminal , activate : ( event : MouseEvent , uri : string ) => void ) : ILink [ ] {
4646 const rex = new RegExp ( regex . source , ( regex . flags || '' ) + 'g' ) ;
4747
48- const [ line , startLineIndex ] = LinkComputer . _translateBufferLineToStringWithWrap ( y - 1 , false , terminal ) ;
49-
50- // Don't try if the wrapped line if excessively large as the regex matching will block the main
51- // thread.
52- if ( line . length > 1024 ) {
53- return [ ] ;
54- }
48+ const [ lines , startLineIndex ] = LinkComputer . _getWindowedLineStrings ( y - 1 , terminal ) ;
49+ const line = lines . join ( '' ) ;
5550
5651 let match ;
57- let stringIndex = - 1 ;
5852 const result : ILink [ ] = [ ] ;
5953
60- while ( ( match = rex . exec ( line ) ) !== null ) {
61- const text = match [ 1 ] ;
62- if ( ! text ) {
63- // something matched but does not comply with the given matchIndex
64- // since this is most likely a bug the regex itself we simply do nothing here
65- console . log ( 'match found without corresponding matchIndex' ) ;
66- break ;
67- }
68-
69- // Get index, match.index is for the outer match which includes negated chars
70- // therefore we cannot use match.index directly, instead we search the position
71- // of the match group in text again
72- // also correct regex and string search offsets for the next loop run
73- stringIndex = line . indexOf ( text , stringIndex + 1 ) ;
74- rex . lastIndex = stringIndex + text . length ;
75- if ( stringIndex < 0 ) {
76- // invalid stringIndex (should not have happened)
77- break ;
54+ while ( match = rex . exec ( line ) ) {
55+ const text = match [ 0 ] ;
56+
57+ // check via URL if the matched text would form a proper url
58+ // NOTE: This outsources the ugly url parsing to the browser.
59+ // To avoid surprising auto expansion from URL we additionally
60+ // check afterwards if the provided string resembles the parsed
61+ // one close enough:
62+ // - decodeURI decode path segement back to byte repr
63+ // to detect unicode auto conversion correctly
64+ // - append / also match domain urls w'o any path notion
65+ try {
66+ const url = new URL ( text ) ;
67+ const urlText = decodeURI ( url . toString ( ) ) ;
68+ if ( text !== urlText && text + '/' !== urlText ) {
69+ continue ;
70+ }
71+ } catch ( e ) {
72+ continue ;
7873 }
7974
80- let endX = stringIndex + text . length ;
81- let endY = startLineIndex + 1 ;
75+ // map string positions back to buffer positions
76+ // values are 0-based right side excluding
77+ const [ startY , startX ] = LinkComputer . _mapStrIdx ( terminal , startLineIndex , 0 , match . index ) ;
78+ const [ endY , endX ] = LinkComputer . _mapStrIdx ( terminal , startY , startX , text . length ) ;
8279
83- while ( endX > terminal . cols ) {
84- endX -= terminal . cols ;
85- endY ++ ;
86- }
87-
88- let startX = stringIndex + 1 ;
89- let startY = startLineIndex + 1 ;
90- while ( startX > terminal . cols ) {
91- startX -= terminal . cols ;
92- startY ++ ;
80+ if ( startY === - 1 || startX === - 1 || endY === - 1 || endX === - 1 ) {
81+ continue ;
9382 }
9483
84+ // range expects values 1-based right side including, thus +1 except for endX
9585 const range = {
9686 start : {
97- x : startX ,
98- y : startY
87+ x : startX + 1 ,
88+ y : startY + 1
9989 } ,
10090 end : {
10191 x : endX ,
102- y : endY
92+ y : endY + 1
10393 }
10494 } ;
10595
@@ -110,41 +100,99 @@ export class LinkComputer {
110100 }
111101
112102 /**
113- * Gets the entire line for the buffer line
114- * @param lineIndex The index of the line being translated.
115- * @param trimRight Whether to trim whitespace to the right.
103+ * Get wrapped content lines for the current line index.
104+ * The top/bottom line expansion stops at whitespaces or length > 2048.
105+ * Returns an array with line strings and the top line index.
106+ *
107+ * NOTE: We pull line strings with trimRight=true on purpose to make sure
108+ * to correctly match urls with early wrapped wide chars. This corrupts the string index
109+ * for 1:1 backmapping to buffer positions, thus needs an additional correction in _mapStrIdx.
116110 */
117- private static _translateBufferLineToStringWithWrap ( lineIndex : number , trimRight : boolean , terminal : Terminal ) : [ string , number ] {
118- let lineString = '' ;
119- let lineWrapsToNext : boolean ;
120- let prevLinesToWrap : boolean ;
121-
122- do {
123- const line = terminal . buffer . active . getLine ( lineIndex ) ;
124- if ( ! line ) {
125- break ;
111+ private static _getWindowedLineStrings ( lineIndex : number , terminal : Terminal ) : [ string [ ] , number ] {
112+ let line : IBufferLine | undefined ;
113+ let topIdx = lineIndex ;
114+ let bottomIdx = lineIndex ;
115+ let length = 0 ;
116+ let content = '' ;
117+ const lines : string [ ] = [ ] ;
118+
119+ if ( ( line = terminal . buffer . active . getLine ( lineIndex ) ) ) {
120+ const currentContent = line . translateToString ( true ) ;
121+
122+ // expand top, stop on whitespaces or length > 2048
123+ if ( line . isWrapped && currentContent [ 0 ] !== ' ' ) {
124+ length = 0 ;
125+ while ( ( line = terminal . buffer . active . getLine ( -- topIdx ) ) && length < 2048 ) {
126+ content = line . translateToString ( true ) ;
127+ length += content . length ;
128+ lines . push ( content ) ;
129+ if ( ! line . isWrapped || content . indexOf ( ' ' ) !== - 1 ) {
130+ break ;
131+ }
132+ }
133+ lines . reverse ( ) ;
126134 }
127135
128- if ( line . isWrapped ) {
129- lineIndex -- ;
136+ // append current line
137+ lines . push ( currentContent ) ;
138+
139+ // expand bottom, stop on whitespaces or length > 2048
140+ length = 0 ;
141+ while ( ( line = terminal . buffer . active . getLine ( ++ bottomIdx ) ) && line . isWrapped && length < 2048 ) {
142+ content = line . translateToString ( true ) ;
143+ length += content . length ;
144+ lines . push ( content ) ;
145+ if ( content . indexOf ( ' ' ) !== - 1 ) {
146+ break ;
147+ }
130148 }
149+ }
150+ return [ lines , topIdx ] ;
151+ }
131152
132- prevLinesToWrap = line . isWrapped ;
133- } while ( prevLinesToWrap ) ;
134-
135- const startLineIndex = lineIndex ;
136-
137- do {
138- const nextLine = terminal . buffer . active . getLine ( lineIndex + 1 ) ;
139- lineWrapsToNext = nextLine ? nextLine . isWrapped : false ;
140- const line = terminal . buffer . active . getLine ( lineIndex ) ;
153+ /**
154+ * Map a string index back to buffer positions.
155+ * Returns buffer position as [lineIndex, columnIndex] 0-based,
156+ * or [-1, -1] in case the lookup ran into a non-existing line.
157+ */
158+ private static _mapStrIdx ( terminal : Terminal , lineIndex : number , rowIndex : number , stringIndex : number ) : [ number , number ] {
159+ const buf = terminal . buffer . active ;
160+ const cell = buf . getNullCell ( ) ;
161+ let start = rowIndex ;
162+ while ( stringIndex ) {
163+ const line = buf . getLine ( lineIndex ) ;
141164 if ( ! line ) {
142- break ;
165+ return [ - 1 , - 1 ] ;
166+ }
167+ for ( let i = start ; i < line . length ; ++ i ) {
168+ line . getCell ( i , cell ) ;
169+ const chars = cell . getChars ( ) ;
170+ const width = cell . getWidth ( ) ;
171+ if ( width ) {
172+ stringIndex -= chars . length || 1 ;
173+
174+ // correct stringIndex for early wrapped wide chars:
175+ // - currently only happens at last cell
176+ // - cells to the right are reset with chars='' and width=1 in InputHandler.print
177+ // - follow-up line must be wrapped and contain wide char at first cell
178+ // --> if all these conditions are met, correct stringIndex by +1
179+ if ( i === line . length - 1 && chars === '' ) {
180+ const line = buf . getLine ( lineIndex + 1 ) ;
181+ if ( line && line . isWrapped ) {
182+ line . getCell ( 0 , cell ) ;
183+ if ( cell . getWidth ( ) === 2 ) {
184+ stringIndex += 1 ;
185+ }
186+ }
187+ }
188+ }
189+ if ( stringIndex < 0 ) {
190+ return [ lineIndex , i ] ;
191+ }
143192 }
144- lineString += line . translateToString ( ! lineWrapsToNext && trimRight ) . substring ( 0 , terminal . cols ) ;
145193 lineIndex ++ ;
146- } while ( lineWrapsToNext ) ;
147-
148- return [ lineString , startLineIndex ] ;
194+ start = 0 ;
195+ }
196+ return [ lineIndex , start ] ;
149197 }
150198}
0 commit comments