@@ -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,15 @@ 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
+ // Ghost text in pwsh only appears to happen on the cursor line
153
+ this . _ghostTextIndex = this . _scanForGhostText ( buffer , line ) ;
154
+ }
125
155
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
156
// IDEA: Detect line continuation if it's not set
129
157
130
158
// From command start line to cursor line
@@ -160,12 +188,53 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
160
188
}
161
189
162
190
if ( this . _logService . getLevel ( ) === LogLevel . Trace ) {
163
- this . _logService . trace ( `PromptInputModel#_sync: Input=" ${ this . _value . substring ( 0 , this . _cursorIndex ) } | ${ this . value . substring ( this . _cursorIndex ) } " ` ) ;
191
+ this . _logService . trace ( `PromptInputModel#_sync: ${ this . getCombinedString ( ) } ` ) ;
164
192
}
165
193
166
194
this . _onDidChangeInput . fire ( ) ;
167
195
}
168
196
197
+ /**
198
+ * Detect ghost text by looking for italic or dim text in or after the cursor and
199
+ * non-italic/dim text in the cell closest non-whitespace cell before the cursor.
200
+ */
201
+ private _scanForGhostText ( buffer : IBuffer , line : IBufferLine ) : number {
202
+ // Check last non-whitespace character has non-ghost text styles
203
+ let ghostTextIndex = - 1 ;
204
+ let proceedWithGhostTextCheck = false ;
205
+ let x = buffer . cursorX ;
206
+ while ( x > 0 ) {
207
+ const cell = line . getCell ( -- x ) ;
208
+ if ( ! cell ) {
209
+ break ;
210
+ }
211
+ if ( cell . getChars ( ) . trim ( ) . length > 0 ) {
212
+ proceedWithGhostTextCheck = ! this . _isCellStyledLikeGhostText ( cell ) ;
213
+ break ;
214
+ }
215
+ }
216
+
217
+ // Check to the end of the line for possible ghost text. For example pwsh's ghost text
218
+ // can look like this `Get-|Ch[ildItem]`
219
+ if ( proceedWithGhostTextCheck ) {
220
+ let potentialGhostIndexOffset = 0 ;
221
+ let x = buffer . cursorX ;
222
+ while ( x < line . length ) {
223
+ const cell = line . getCell ( x ++ ) ;
224
+ if ( ! cell || cell . getCode ( ) === 0 ) {
225
+ break ;
226
+ }
227
+ if ( this . _isCellStyledLikeGhostText ( cell ) ) {
228
+ ghostTextIndex = this . _cursorIndex + potentialGhostIndexOffset ;
229
+ break ;
230
+ }
231
+ potentialGhostIndexOffset += cell . getChars ( ) . length ;
232
+ }
233
+ }
234
+
235
+ return ghostTextIndex ;
236
+ }
237
+
169
238
private _trimContinuationPrompt ( lineText : string ) : string {
170
239
if ( this . _lineContainsContinuationPrompt ( lineText ) ) {
171
240
lineText = lineText . substring ( this . _continuationPrompt ! . length ) ;
@@ -192,4 +261,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
192
261
private _getRelativeCursorIndex ( startCellX : number , buffer : IBuffer , line : IBufferLine ) : number {
193
262
return line ?. translateToString ( true , startCellX , buffer . cursorX ) . length ?? 0 ;
194
263
}
264
+
265
+ private _isCellStyledLikeGhostText ( cell : IBufferCell ) : boolean {
266
+ return ! ! ( cell . isItalic ( ) || cell . isDim ( ) ) ;
267
+ }
195
268
}
0 commit comments