3
3
* @license MIT
4
4
*/
5
5
6
- import { ILinkProvider , ILink , Terminal , IViewportRange } from 'xterm' ;
6
+ import { ILinkProvider , ILink , Terminal , IViewportRange , IBufferLine } from 'xterm' ;
7
7
8
8
export interface ILinkProviderOptions {
9
9
hover ?( event : MouseEvent , text : string , location : IViewportRange ) : void ;
@@ -45,61 +45,51 @@ export class LinkComputer {
45
45
public static computeLink ( y : number , regex : RegExp , terminal : Terminal , activate : ( event : MouseEvent , uri : string ) => void ) : ILink [ ] {
46
46
const rex = new RegExp ( regex . source , ( regex . flags || '' ) + 'g' ) ;
47
47
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 ( '' ) ;
55
50
56
51
let match ;
57
- let stringIndex = - 1 ;
58
52
const result : ILink [ ] = [ ] ;
59
53
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 ;
78
73
}
79
74
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 ) ;
82
79
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 ;
93
82
}
94
83
84
+ // range expects values 1-based right side including, thus +1 except for endX
95
85
const range = {
96
86
start : {
97
- x : startX ,
98
- y : startY
87
+ x : startX + 1 ,
88
+ y : startY + 1
99
89
} ,
100
90
end : {
101
91
x : endX ,
102
- y : endY
92
+ y : endY + 1
103
93
}
104
94
} ;
105
95
@@ -110,41 +100,99 @@ export class LinkComputer {
110
100
}
111
101
112
102
/**
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.
116
110
*/
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 ( ) ;
126
134
}
127
135
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
+ }
130
148
}
149
+ }
150
+ return [ lines , topIdx ] ;
151
+ }
131
152
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 ) ;
141
164
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
+ }
143
192
}
144
- lineString += line . translateToString ( ! lineWrapsToNext && trimRight ) . substring ( 0 , terminal . cols ) ;
145
193
lineIndex ++ ;
146
- } while ( lineWrapsToNext ) ;
147
-
148
- return [ lineString , startLineIndex ] ;
194
+ start = 0 ;
195
+ }
196
+ return [ lineIndex , start ] ;
149
197
}
150
198
}
0 commit comments