@@ -8,8 +8,8 @@ import { SimpleCompletionItem } from 'vs/workbench/services/suggest/browser/simp
8
8
import { LineContext , SimpleCompletionModel } from 'vs/workbench/services/suggest/browser/simpleCompletionModel' ;
9
9
import { ISimpleSelectedSuggestion , SimpleSuggestWidget } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget' ;
10
10
import { Codicon } from 'vs/base/common/codicons' ;
11
- import { Emitter } from 'vs/base/common/event' ;
12
- import { Disposable } from 'vs/base/common/lifecycle' ;
11
+ import { Emitter , Event } from 'vs/base/common/event' ;
12
+ import { combinedDisposable , Disposable , MutableDisposable } from 'vs/base/common/lifecycle' ;
13
13
import { ThemeIcon } from 'vs/base/common/themables' ;
14
14
import { editorSuggestWidgetSelectedBackground } from 'vs/editor/contrib/suggest/browser/suggestWidget' ;
15
15
import { IContextKey } from 'vs/platform/contextkey/common/contextkey' ;
@@ -20,11 +20,9 @@ import { ISuggestController } from 'vs/workbench/contrib/terminal/browser/termin
20
20
import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys' ;
21
21
import type { ITerminalAddon , Terminal } from '@xterm/xterm' ;
22
22
import { getListStyles } from 'vs/platform/theme/browser/defaultStyles' ;
23
-
24
- const enum ShellIntegrationOscPs {
25
- // TODO: Pull from elsewhere
26
- VSCode = 633
27
- }
23
+ import { TerminalCapability , type ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities' ;
24
+ import type { IPromptInputModel , IPromptInputModelState } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel' ;
25
+ import { ShellIntegrationOscPs } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon' ;
28
26
29
27
const enum VSCodeOscPt {
30
28
Completions = 'Completions' ,
@@ -73,35 +71,59 @@ const pwshTypeToIconMap: { [type: string]: ThemeIcon | undefined } = {
73
71
74
72
export class SuggestAddon extends Disposable implements ITerminalAddon , ISuggestController {
75
73
private _terminal ?: Terminal ;
74
+
75
+ private _promptInputModel ?: IPromptInputModel ;
76
+ private readonly _promptInputModelSubscriptions = this . _register ( new MutableDisposable ( ) ) ;
77
+
78
+ private _mostRecentPromptInputState ?: IPromptInputModelState ;
79
+ private _initialPromptInputState ?: IPromptInputModelState ;
80
+ private _currentPromptInputState ?: IPromptInputModelState ;
81
+
76
82
private _panel ?: HTMLElement ;
77
83
private _screen ?: HTMLElement ;
78
84
private _suggestWidget ?: SimpleSuggestWidget ;
79
85
private _enableWidget : boolean = true ;
86
+
87
+ // TODO: Remove these in favor of prompt input state
80
88
private _leadingLineContent ?: string ;
81
- private _additionalInput ?: string ;
82
89
private _cursorIndexDelta : number = 0 ;
83
- private _inputQueue ?: string [ ] ;
84
90
85
91
private readonly _onBell = this . _register ( new Emitter < void > ( ) ) ;
86
92
readonly onBell = this . _onBell . event ;
87
93
private readonly _onAcceptedCompletion = this . _register ( new Emitter < string > ( ) ) ;
88
94
readonly onAcceptedCompletion = this . _onAcceptedCompletion . event ;
89
95
90
96
constructor (
97
+ private readonly _capabilities : ITerminalCapabilityStore ,
91
98
private readonly _terminalSuggestWidgetVisibleContextKey : IContextKey < boolean > ,
92
99
@IInstantiationService private readonly _instantiationService : IInstantiationService
93
100
) {
94
101
super ( ) ;
102
+
103
+ this . _register ( Event . runAndSubscribe ( Event . any (
104
+ this . _capabilities . onDidAddCapabilityType ,
105
+ this . _capabilities . onDidRemoveCapabilityType
106
+ ) , ( ) => {
107
+ const commandDetection = this . _capabilities . get ( TerminalCapability . CommandDetection ) ;
108
+ if ( commandDetection ) {
109
+ if ( this . _promptInputModel !== commandDetection . promptInputModel ) {
110
+ this . _promptInputModel = commandDetection . promptInputModel ;
111
+ this . _promptInputModelSubscriptions . value = combinedDisposable (
112
+ this . _promptInputModel . onDidChangeInput ( e => this . _sync ( e ) ) ,
113
+ this . _promptInputModel . onDidFinishInput ( ( ) => this . hideSuggestWidget ( ) ) ,
114
+ ) ;
115
+ }
116
+ } else {
117
+ this . _promptInputModel = undefined ;
118
+ }
119
+ } ) ) ;
95
120
}
96
121
97
122
activate ( xterm : Terminal ) : void {
98
123
this . _terminal = xterm ;
99
124
this . _register ( xterm . parser . registerOscHandler ( ShellIntegrationOscPs . VSCode , data => {
100
125
return this . _handleVSCodeSequence ( data ) ;
101
126
} ) ) ;
102
- this . _register ( xterm . onData ( e => {
103
- this . _handleTerminalInput ( e ) ;
104
- } ) ) ;
105
127
}
106
128
107
129
setPanel ( panel : HTMLElement ) : void {
@@ -112,6 +134,45 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
112
134
this . _screen = screen ;
113
135
}
114
136
137
+ private _sync ( promptInputState : IPromptInputModelState ) : void {
138
+ this . _mostRecentPromptInputState = promptInputState ;
139
+ if ( ! this . _promptInputModel || ! this . _terminal || ! this . _suggestWidget || ! this . _initialPromptInputState ) {
140
+ return ;
141
+ }
142
+
143
+ this . _currentPromptInputState = promptInputState ;
144
+
145
+ if ( this . _terminalSuggestWidgetVisibleContextKey . get ( ) ) {
146
+ const inputBeforeCursor = this . _currentPromptInputState . value . substring ( 0 , this . _currentPromptInputState . cursorIndex ) ;
147
+ this . _cursorIndexDelta = this . _currentPromptInputState . cursorIndex - this . _initialPromptInputState . cursorIndex ;
148
+
149
+ this . _suggestWidget . setLineContext ( new LineContext ( inputBeforeCursor , this . _cursorIndexDelta ) ) ;
150
+ }
151
+
152
+ // Hide and clear model if there are no more items
153
+ if ( ! this . _suggestWidget . hasCompletions ( ) ) {
154
+ this . hideSuggestWidget ( ) ;
155
+ // TODO: Don't request every time; refine completions
156
+ // this._onAcceptedCompletion.fire('\x1b[24~e');
157
+ return ;
158
+ }
159
+
160
+ // TODO: Expose on xterm.js
161
+ const dimensions = this . _getTerminalDimensions ( ) ;
162
+ if ( ! dimensions . width || ! dimensions . height ) {
163
+ return ;
164
+ }
165
+ // TODO: What do frozen and auto do?
166
+ const xtermBox = this . _screen ! . getBoundingClientRect ( ) ;
167
+ const panelBox = this . _panel ! . offsetParent ! . getBoundingClientRect ( ) ;
168
+
169
+ this . _suggestWidget . showSuggestions ( 0 , false , false , {
170
+ left : ( xtermBox . left - panelBox . left ) + this . _terminal . buffer . active . cursorX * dimensions . width ,
171
+ top : ( xtermBox . top - panelBox . top ) + this . _terminal . buffer . active . cursorY * dimensions . height ,
172
+ height : dimensions . height
173
+ } ) ;
174
+ }
175
+
115
176
private _handleVSCodeSequence ( data : string ) : boolean {
116
177
if ( ! this . _terminal ) {
117
178
return false ;
@@ -277,7 +338,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
277
338
}
278
339
279
340
private _handleCompletionModel ( model : SimpleCompletionModel ) : void {
280
- if ( model . items . length === 0 || ! this . _terminal ?. element ) {
341
+ if ( model . items . length === 0 || ! this . _terminal ?. element || ! this . _promptInputModel ) {
281
342
return ;
282
343
}
283
344
if ( model . items . length === 1 ) {
@@ -288,29 +349,24 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
288
349
return ;
289
350
}
290
351
const suggestWidget = this . _ensureSuggestWidget ( this . _terminal ) ;
291
- this . _additionalInput = undefined ;
292
352
const dimensions = this . _getTerminalDimensions ( ) ;
293
353
if ( ! dimensions . width || ! dimensions . height ) {
294
354
return ;
295
355
}
296
356
// TODO: What do frozen and auto do?
297
357
const xtermBox = this . _screen ! . getBoundingClientRect ( ) ;
298
358
const panelBox = this . _panel ! . offsetParent ! . getBoundingClientRect ( ) ;
359
+ this . _initialPromptInputState = {
360
+ value : this . _promptInputModel . value ,
361
+ cursorIndex : this . _promptInputModel . cursorIndex ,
362
+ ghostTextIndex : this . _promptInputModel . ghostTextIndex
363
+ } ;
299
364
suggestWidget . setCompletionModel ( model ) ;
300
365
suggestWidget . showSuggestions ( 0 , false , false , {
301
366
left : ( xtermBox . left - panelBox . left ) + this . _terminal . buffer . active . cursorX * dimensions . width ,
302
367
top : ( xtermBox . top - panelBox . top ) + this . _terminal . buffer . active . cursorY * dimensions . height ,
303
368
height : dimensions . height
304
369
} ) ;
305
-
306
- // Flush the input queue if any characters were typed after a trigger character
307
- if ( this . _inputQueue ) {
308
- const inputQueue = this . _inputQueue ;
309
- this . _inputQueue = undefined ;
310
- for ( const data of inputQueue ) {
311
- this . _handleTerminalInput ( data ) ;
312
- }
313
- }
314
370
}
315
371
316
372
private _ensureSuggestWidget ( terminal : Terminal ) : SimpleSuggestWidget {
@@ -328,7 +384,14 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
328
384
} ) ) ;
329
385
this . _suggestWidget . onDidSelect ( async e => this . acceptSelectedSuggestion ( e ) ) ;
330
386
this . _suggestWidget . onDidHide ( ( ) => this . _terminalSuggestWidgetVisibleContextKey . set ( false ) ) ;
331
- this . _suggestWidget . onDidShow ( ( ) => this . _terminalSuggestWidgetVisibleContextKey . set ( true ) ) ;
387
+ this . _suggestWidget . onDidShow ( ( ) => {
388
+ this . _initialPromptInputState = {
389
+ value : this . _promptInputModel ! . value ,
390
+ cursorIndex : this . _promptInputModel ! . cursorIndex ,
391
+ ghostTextIndex : this . _promptInputModel ! . ghostTextIndex
392
+ } ;
393
+ this . _terminalSuggestWidgetVisibleContextKey . set ( true ) ;
394
+ } ) ;
332
395
}
333
396
return this . _suggestWidget ;
334
397
}
@@ -353,137 +416,39 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
353
416
if ( ! suggestion ) {
354
417
suggestion = this . _suggestWidget ?. getFocusedItem ( ) ;
355
418
}
356
- if ( suggestion && this . _leadingLineContent ) {
357
- this . _suggestWidget ?. hide ( ) ;
358
-
359
- // Send the completion
360
- this . _onAcceptedCompletion . fire ( [
361
- // Disable suggestions
362
- '\x1b[24~y' ,
363
- // Right arrow to the end of the additional input
364
- '\x1b[C' . repeat ( Math . max ( ( this . _additionalInput ?. length ?? 0 ) - this . _cursorIndexDelta , 0 ) ) ,
365
- // Backspace to remove additional input
366
- '\x7F' . repeat ( this . _additionalInput ?. length ?? 0 ) ,
367
- // Backspace to remove the replacement
368
- '\x7F' . repeat ( suggestion . model . replacementLength ) ,
369
- // Write the completion
370
- suggestion . item . completion . label ,
371
- // Enable suggestions
372
- '\x1b[24~z' ,
373
- ] . join ( '' ) ) ;
419
+ const initialPromptInputState = this . _initialPromptInputState ?? this . _mostRecentPromptInputState ;
420
+ if ( ! suggestion || ! initialPromptInputState ) {
421
+ return ;
374
422
}
375
- }
376
-
377
- hideSuggestWidget ( ) : void {
378
423
this . _suggestWidget ?. hide ( ) ;
379
- }
380
424
381
- handleNonXtermData ( data : string ) : void {
382
- this . _handleTerminalInput ( data , true ) ;
425
+ const currentPromptInputState = this . _currentPromptInputState ?? initialPromptInputState ;
426
+ const additionalInput = currentPromptInputState . value . substring ( initialPromptInputState . cursorIndex , currentPromptInputState . cursorIndex ) ;
427
+
428
+ // We could start from a common prefix to reduce the number of characters we need to send
429
+ const initialInput = initialPromptInputState . value . substring ( 0 , initialPromptInputState . cursorIndex ) ;
430
+ const lastSpaceIndex = initialInput . lastIndexOf ( ' ' ) ;
431
+ const finalCompletion = suggestion . item . completion . label . substring ( initialPromptInputState . cursorIndex - ( lastSpaceIndex === - 1 ? 0 : lastSpaceIndex + 1 ) ) ;
432
+
433
+ // Send the completion
434
+ this . _onAcceptedCompletion . fire ( [
435
+ // Disable suggestions
436
+ '\x1b[24~y' ,
437
+ // Backspace to remove all additional input
438
+ '\x7F' . repeat ( additionalInput . length ) ,
439
+ // Write the completion
440
+ finalCompletion ,
441
+ // Enable suggestions
442
+ '\x1b[24~z' ,
443
+ ] . join ( '' ) ) ;
444
+
445
+ this . hideSuggestWidget ( ) ;
383
446
}
384
447
385
- private _handleTerminalInput ( data : string , nonUserInput ?: boolean ) : void {
386
- if ( ! this . _terminal || ! this . _enableWidget || ! this . _terminalSuggestWidgetVisibleContextKey . get ( ) ) {
387
- // HACK: Buffer any input to be evaluated when the completions come in, this is needed
388
- // because conpty may "render" the completion request after input characters that
389
- // actually come after it. This can happen when typing quickly after a trigger
390
- // character, especially on a freshly launched session.
391
- if ( data === '-' ) {
392
- this . _inputQueue = [ ] ;
393
- } else {
394
- this . _inputQueue ?. push ( data ) ;
395
- }
396
-
397
- return ;
398
- }
399
- let handled = false ;
400
- let handledCursorDelta = 0 ;
401
-
402
- // Backspace
403
- if ( data === '\x7f' ) {
404
- if ( this . _additionalInput && this . _additionalInput . length > 0 && this . _cursorIndexDelta > 0 ) {
405
- handled = true ;
406
- this . _additionalInput = this . _additionalInput . substring ( 0 , this . _cursorIndexDelta - 1 ) + this . _additionalInput . substring ( this . _cursorIndexDelta ) ;
407
- this . _cursorIndexDelta -- ;
408
- handledCursorDelta -- ;
409
- }
410
- }
411
- // Delete
412
- if ( data === '\x1b[3~' ) {
413
- if ( this . _additionalInput && this . _additionalInput . length > 0 && this . _cursorIndexDelta < this . _additionalInput . length - 1 ) {
414
- handled = true ;
415
- this . _additionalInput = this . _additionalInput . substring ( 0 , this . _cursorIndexDelta ) + this . _additionalInput . substring ( this . _cursorIndexDelta + 1 ) ;
416
- }
417
- }
418
- // Left
419
- else if ( data === '\x1b[D' ) {
420
- // If left goes beyond where the completion was requested, hide
421
- if ( this . _cursorIndexDelta > 0 ) {
422
- handled = true ;
423
- this . _cursorIndexDelta -- ;
424
- handledCursorDelta -- ;
425
- }
426
- }
427
- // Right
428
- else if ( data === '\x1b[C' ) {
429
- // If right requests beyond where the completion was requested (potentially accepting a shell completion), hide
430
- if ( this . _additionalInput ?. length !== this . _cursorIndexDelta ) {
431
- handled = true ;
432
- this . _cursorIndexDelta ++ ;
433
- handledCursorDelta ++ ;
434
- }
435
- }
436
- // Other CSI sequence (ignore)
437
- else if ( data . match ( / ^ \x1b \[ .+ [ a - z @ \^ ` { \| } ~ ] $ / i) ) {
438
- handled = true ;
439
- }
440
- if ( data . match ( / ^ [ a - z 0 - 9 ] $ / i) ) {
441
-
442
- // TODO: There is a race here where the completions may come through after new character presses because of conpty's rendering!
443
-
444
- handled = true ;
445
- if ( this . _additionalInput === undefined ) {
446
- this . _additionalInput = '' ;
447
- }
448
- this . _additionalInput += data ;
449
- this . _cursorIndexDelta ++ ;
450
- handledCursorDelta ++ ;
451
- }
452
- if ( handled ) {
453
- // typed -> moved cursor RIGHT -> update UI
454
- if ( this . _terminalSuggestWidgetVisibleContextKey . get ( ) ) {
455
- this . _suggestWidget ?. setLineContext ( new LineContext ( this . _leadingLineContent ! + ( this . _additionalInput ?? '' ) , this . _additionalInput ?. length ?? 0 ) ) ;
456
- }
457
-
458
- // Hide and clear model if there are no more items
459
- if ( ! this . _suggestWidget ?. hasCompletions ( ) || nonUserInput ) {
460
- this . _additionalInput = undefined ;
461
- this . hideSuggestWidget ( ) ;
462
- // TODO: Don't request every time; refine completions
463
- // this._onAcceptedCompletion.fire('\x1b[24~e');
464
- return ;
465
- }
466
-
467
- // TODO: Expose on xterm.js
468
- const dimensions = this . _getTerminalDimensions ( ) ;
469
- if ( ! dimensions . width || ! dimensions . height ) {
470
- return ;
471
- }
472
- // TODO: What do frozen and auto do?
473
- const xtermBox = this . _screen ! . getBoundingClientRect ( ) ;
474
- const panelBox = this . _panel ! . offsetParent ! . getBoundingClientRect ( ) ;
475
-
476
- this . _suggestWidget ?. showSuggestions ( 0 , false , false , {
477
- left : ( xtermBox . left - panelBox . left ) + ( this . _terminal . buffer . active . cursorX + handledCursorDelta ) * dimensions . width ,
478
- top : ( xtermBox . top - panelBox . top ) + this . _terminal . buffer . active . cursorY * dimensions . height ,
479
- height : dimensions . height
480
- } ) ;
481
- } else {
482
- this . _additionalInput = undefined ;
483
- this . hideSuggestWidget ( ) ;
484
- // TODO: Don't request every time; refine completions
485
- // this._onAcceptedCompletion.fire('\x1b[24~e');
486
- }
448
+ hideSuggestWidget ( ) : void {
449
+ this . _initialPromptInputState = undefined ;
450
+ this . _currentPromptInputState = undefined ;
451
+ this . _suggestWidget ?. hide ( ) ;
487
452
}
488
453
}
489
454
0 commit comments