Skip to content

Commit dd4b103

Browse files
authored
Merge pull request microsoft#210766 from microsoft/tyriar/210757_interrupt
PromptInputModel: Detect interrupts and show executing in dev mode
2 parents b8eba5d + b7b8b9d commit dd4b103

File tree

4 files changed

+68
-13
lines changed

4 files changed

+68
-13
lines changed

src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,23 @@ import { throttle } from 'vs/base/common/decorators';
1414
import type { Terminal, IMarker, IBufferCell, IBufferLine, IBuffer } from '@xterm/headless';
1515

1616
const enum PromptInputState {
17-
Unknown,
18-
Input,
19-
Execute,
17+
Unknown = 0,
18+
Input = 1,
19+
Execute = 2,
2020
}
2121

22+
/**
23+
* A model of the prompt input state using shell integration and analyzing the terminal buffer. This
24+
* may not be 100% accurate but provides a best guess.
25+
*/
2226
export interface IPromptInputModel {
2327
readonly onDidStartInput: Event<IPromptInputModelState>;
2428
readonly onDidChangeInput: Event<IPromptInputModelState>;
2529
readonly onDidFinishInput: Event<IPromptInputModelState>;
30+
/**
31+
* Fires immediately before {@link onDidFinishInput} when a SIGINT/Ctrl+C/^C is detected.
32+
*/
33+
readonly onDidInterrupt: Event<IPromptInputModelState>;
2634

2735
readonly value: string;
2836
readonly cursorIndex: number;
@@ -48,6 +56,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
4856
private _commandStartX: number = 0;
4957
private _continuationPrompt: string | undefined;
5058

59+
private _lastUserInput: string = '';
60+
5161
private _value: string = '';
5262
get value() { return this._value; }
5363

@@ -63,6 +73,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
6373
readonly onDidChangeInput = this._onDidChangeInput.event;
6474
private readonly _onDidFinishInput = this._register(new Emitter<IPromptInputModelState>());
6575
readonly onDidFinishInput = this._onDidFinishInput.event;
76+
private readonly _onDidInterrupt = this._register(new Emitter<IPromptInputModelState>());
77+
readonly onDidInterrupt = this._onDidInterrupt.event;
6678

6779
constructor(
6880
private readonly _xterm: Terminal,
@@ -72,14 +84,27 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
7284
) {
7385
super();
7486

75-
this._register(this._xterm.onData(e => this._handleInput(e)));
7687
this._register(Event.any(
77-
this._xterm.onWriteParsed,
7888
this._xterm.onCursorMove,
89+
this._xterm.onData,
90+
this._xterm.onWriteParsed,
7991
)(() => this._sync()));
92+
this._register(this._xterm.onData(e => this._handleUserInput(e)));
8093

8194
this._register(onCommandStart(e => this._handleCommandStart(e as { marker: IMarker })));
8295
this._register(onCommandExecuted(() => this._handleCommandExecuted()));
96+
97+
this._register(this.onDidStartInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidStartInput')));
98+
this._register(this.onDidChangeInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidChangeInput')));
99+
this._register(this.onDidFinishInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidFinishInput')));
100+
this._register(this.onDidInterrupt(() => this._logCombinedStringIfTrace('PromptInputModel#onDidInterrupt')));
101+
}
102+
103+
private _logCombinedStringIfTrace(message: string) {
104+
// Only generate the combined string if trace
105+
if (this._logService.getLevel() === LogLevel.Trace) {
106+
this._logService.trace(message, this.getCombinedString());
107+
}
83108
}
84109

85110
setContinuationPrompt(value: string): void {
@@ -112,20 +137,24 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
112137
this._value = '';
113138
this._cursorIndex = 0;
114139
this._onDidStartInput.fire(this._createStateObject());
140+
this._onDidChangeInput.fire(this._createStateObject());
115141
}
116142

117143
private _handleCommandExecuted() {
118144
if (this._state === PromptInputState.Execute) {
119145
return;
120146
}
121147

122-
this._state = PromptInputState.Execute;
123148
this._cursorIndex = -1;
124-
this._onDidFinishInput.fire(this._createStateObject());
125-
}
149+
this._ghostTextIndex = -1;
150+
const event = this._createStateObject();
151+
if (this._lastUserInput === '\u0003') {
152+
this._onDidInterrupt.fire(event);
153+
}
126154

127-
private _handleInput(data: string) {
128-
this._sync();
155+
this._state = PromptInputState.Execute;
156+
this._onDidFinishInput.fire(event);
157+
this._onDidChangeInput.fire(event);
129158
}
130159

131160
@throttle(0)
@@ -207,6 +236,10 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
207236
}
208237
}
209238

239+
private _handleUserInput(e: string) {
240+
this._lastUserInput = e;
241+
}
242+
210243
/**
211244
* Detect ghost text by looking for italic or dim text in or after the cursor and
212245
* non-italic/dim text in the cell closest non-whitespace cell before the cursor.

src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
9191
) {
9292
super();
9393

94-
this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandFinished, this._logService));
94+
this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandExecuted, this._logService));
9595

9696
// Pull command line from the buffer if it was not set explicitly
9797
this._register(this.onCommandExecuted(command => {

src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ suite('PromptInputModel', () => {
7878
await assertPromptInput('|');
7979
});
8080

81-
test('should not fire events when nothing changes', async () => {
81+
test('should not fire onDidChangeInput events when nothing changes', async () => {
8282
const events: IPromptInputModelState[] = [];
8383
store.add(promptInputModel.onDidChangeInput(e => events.push(e)));
8484

@@ -108,6 +108,26 @@ suite('PromptInputModel', () => {
108108
}
109109
});
110110

111+
test('should fire onDidInterrupt followed by onDidFinish when ctrl+c is pressed', async () => {
112+
await writePromise('$ ');
113+
fireCommandStart();
114+
await assertPromptInput('|');
115+
116+
await writePromise('foo');
117+
await assertPromptInput('foo|');
118+
119+
await new Promise<void>(r => {
120+
store.add(promptInputModel.onDidInterrupt(() => {
121+
// Fire onDidFinishInput immediately after onDidInterrupt
122+
store.add(promptInputModel.onDidFinishInput(() => {
123+
r();
124+
}));
125+
}));
126+
xterm.input('\x03');
127+
writePromise('^C').then(() => fireCommandExecuted());
128+
});
129+
});
130+
111131
test('cursor navigation', async () => {
112132
await writePromise('$ ');
113133
fireCommandStart();

src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,12 @@ class DevModeContribution extends Disposable implements ITerminalContribution {
246246
const promptInputModel = commandDetection.promptInputModel;
247247
if (promptInputModel) {
248248
const name = localize('terminalDevMode', 'Terminal Dev Mode');
249+
const isExecuting = promptInputModel.cursorIndex === -1;
249250
this._statusbarEntry = {
250251
name,
251-
text: `$(terminal) ${promptInputModel.getCombinedString()}`,
252+
text: `$(${isExecuting ? 'loading~spin' : 'terminal'}) ${promptInputModel.getCombinedString()}`,
252253
ariaLabel: name,
254+
tooltip: 'The detected terminal prompt input',
253255
kind: 'prominent'
254256
};
255257
if (!this._statusbarEntryAccessor.value) {

0 commit comments

Comments
 (0)