@@ -11,7 +11,7 @@ import { debounce } from 'vs/base/common/decorators';
11
11
12
12
// Importing types is safe in any layer
13
13
// eslint-disable-next-line local/code-import-patterns
14
- import type { Terminal , IMarker , IBufferLine , IBuffer } from '@xterm/headless' ;
14
+ import type { Terminal , IMarker , IBufferCell , IBufferLine , IBuffer } from '@xterm/headless' ;
15
15
16
16
const enum PromptInputState {
17
17
Unknown ,
@@ -26,6 +26,13 @@ export interface IPromptInputModel {
26
26
27
27
readonly value : string ;
28
28
readonly cursorIndex : number ;
29
+ readonly ghostTextIndex : number ;
30
+
31
+ /**
32
+ * Gets the prompt input as a user-friendly string where `|` is the cursor position and `[` and
33
+ * `]` wrap any ghost text.
34
+ */
35
+ getCombinedString ( ) : string ;
29
36
}
30
37
31
38
export class PromptInputModel extends Disposable implements IPromptInputModel {
@@ -41,6 +48,9 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
41
48
private _cursorIndex : number = 0 ;
42
49
get cursorIndex ( ) { return this . _cursorIndex ; }
43
50
51
+ private _ghostTextIndex : number = - 1 ;
52
+ get ghostTextIndex ( ) { return this . _ghostTextIndex ; }
53
+
44
54
private readonly _onDidStartInput = this . _register ( new Emitter < void > ( ) ) ;
45
55
readonly onDidStartInput = this . _onDidStartInput . event ;
46
56
private readonly _onDidChangeInput = this . _register ( new Emitter < void > ( ) ) ;
@@ -67,6 +77,18 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
67
77
this . _continuationPrompt = value ;
68
78
}
69
79
80
+ getCombinedString ( ) : string {
81
+ const value = this . _value . replaceAll ( '\n' , '\u23CE' ) ;
82
+ let result = `${ value . substring ( 0 , this . cursorIndex ) } |` ;
83
+ if ( this . ghostTextIndex !== - 1 ) {
84
+ result += `${ value . substring ( this . cursorIndex , this . ghostTextIndex ) } [` ;
85
+ result += `${ value . substring ( this . ghostTextIndex ) } ]` ;
86
+ } else {
87
+ result += value . substring ( this . cursorIndex ) ;
88
+ }
89
+ return result ;
90
+ }
91
+
70
92
private _handleCommandStart ( command : { marker : IMarker } ) {
71
93
if ( this . _state === PromptInputState . Input ) {
72
94
return ;
@@ -111,7 +133,7 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
111
133
const buffer = this . _xterm . buffer . active ;
112
134
let line = buffer . getLine ( commandStartY ) ;
113
135
const commandLine = line ?. translateToString ( true , this . _commandStartX ) ;
114
- if ( ! commandLine || ! line ) {
136
+ if ( ! line || commandLine === undefined ) {
115
137
this . _logService . trace ( `PromptInputModel#_sync: no line` ) ;
116
138
return ;
117
139
}
@@ -122,9 +144,14 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
122
144
// Get cursor index
123
145
const absoluteCursorY = buffer . baseY + buffer . cursorY ;
124
146
this . _cursorIndex = absoluteCursorY === commandStartY ? this . _getRelativeCursorIndex ( this . _commandStartX , buffer , line ) : commandLine . length + 1 ;
147
+ this . _ghostTextIndex = - 1 ;
148
+
149
+ // Detect ghost text by looking for italic or dim text in or after the cursor and
150
+ // non-italic/dim text in the cell closest non-whitespace cell before the cursor
151
+ if ( absoluteCursorY === commandStartY && buffer . cursorX > 1 ) {
152
+ this . _ghostTextIndex = this . _scanForGhostText ( buffer , line ) ;
153
+ }
125
154
126
- // IDEA: Detect ghost text based on SGR and cursor. This might work by checking for italic
127
- // or dim only to avoid false positives from shells that do immediate coloring.
128
155
// IDEA: Detect line continuation if it's not set
129
156
130
157
// From command start line to cursor line
@@ -160,12 +187,51 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
160
187
}
161
188
162
189
if ( this . _logService . getLevel ( ) === LogLevel . Trace ) {
163
- this . _logService . trace ( `PromptInputModel#_sync: Input=" ${ this . _value . substring ( 0 , this . _cursorIndex ) } | ${ this . value . substring ( this . _cursorIndex ) } " ` ) ;
190
+ this . _logService . trace ( `PromptInputModel#_sync: ${ this . getCombinedString ( ) } ` ) ;
164
191
}
165
192
166
193
this . _onDidChangeInput . fire ( ) ;
167
194
}
168
195
196
+ /**
197
+ * Detect ghost text by looking for italic or dim text in or after the cursor and
198
+ * non-italic/dim text in the cell closest non-whitespace cell before the cursor.
199
+ */
200
+ private _scanForGhostText ( buffer : IBuffer , line : IBufferLine ) : number {
201
+ // Check last non-whitespace character has non-ghost text styles
202
+ let ghostTextIndex = - 1 ;
203
+ let proceedWithGhostTextCheck = false ;
204
+ let x = buffer . cursorX ;
205
+ while ( x > 1 ) {
206
+ const cell = line . getCell ( -- x ) ;
207
+ if ( ! cell ) {
208
+ break ;
209
+ }
210
+ if ( cell . getChars ( ) . trim ( ) . length > 0 ) {
211
+ proceedWithGhostTextCheck = ! this . _isCellStyledLikeGhostText ( cell ) ;
212
+ break ;
213
+ }
214
+ }
215
+
216
+ // Check to the end of the line for possible ghost text. For example pwsh's ghost text
217
+ // can look like this `Get-|Ch[ildItem]`
218
+ if ( proceedWithGhostTextCheck ) {
219
+ let x = buffer . cursorX ;
220
+ while ( x < line . length ) {
221
+ const cell = line . getCell ( x ++ ) ;
222
+ if ( ! cell || cell . getCode ( ) === 0 ) {
223
+ break ;
224
+ }
225
+ if ( this . _isCellStyledLikeGhostText ( cell ) ) {
226
+ ghostTextIndex = this . _cursorIndex ;
227
+ break ;
228
+ }
229
+ }
230
+ }
231
+
232
+ return ghostTextIndex ;
233
+ }
234
+
169
235
private _trimContinuationPrompt ( lineText : string ) : string {
170
236
if ( this . _lineContainsContinuationPrompt ( lineText ) ) {
171
237
lineText = lineText . substring ( this . _continuationPrompt ! . length ) ;
@@ -192,4 +258,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
192
258
private _getRelativeCursorIndex ( startCellX : number , buffer : IBuffer , line : IBufferLine ) : number {
193
259
return line ?. translateToString ( true , startCellX , buffer . cursorX ) . length ?? 0 ;
194
260
}
261
+
262
+ private _isCellStyledLikeGhostText ( cell : IBufferCell ) : boolean {
263
+ return ! ! ( cell . isItalic ( ) || cell . isDim ( ) ) ;
264
+ }
195
265
}
0 commit comments