Skip to content

Commit b233e67

Browse files
committed
Add PromptInputModel.onDidInterrupt
Part of microsoft#210757
1 parent ba27ce3 commit b233e67

File tree

2 files changed

+46
-9
lines changed

2 files changed

+46
-9
lines changed

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@ const enum PromptInputState {
1919
Execute,
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,11 +84,12 @@ 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()));
@@ -121,11 +134,11 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
121134

122135
this._state = PromptInputState.Execute;
123136
this._cursorIndex = -1;
124-
this._onDidFinishInput.fire(this._createStateObject());
125-
}
126-
127-
private _handleInput(data: string) {
128-
this._sync();
137+
const event = this._createStateObject();
138+
if (this._lastUserInput === '\u0003') { // ETX end of text (ctrl+c)
139+
this._onDidInterrupt.fire(event);
140+
}
141+
this._onDidFinishInput.fire(event);
129142
}
130143

131144
@throttle(0)
@@ -207,6 +220,10 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
207220
}
208221
}
209222

223+
private _handleUserInput(e: string) {
224+
this._lastUserInput = e;
225+
}
226+
210227
/**
211228
* Detect ghost text by looking for italic or dim text in or after the cursor and
212229
* non-italic/dim text in the cell closest non-whitespace cell before the cursor.

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { Terminal } from '@xterm/headless';
1414
import { notDeepStrictEqual, strictEqual } from 'assert';
1515
import { timeout } from 'vs/base/common/async';
1616

17-
suite('PromptInputModel', () => {
17+
suite.only('PromptInputModel', () => {
1818
const store = ensureNoDisposablesAreLeakedInTestSuite();
1919

2020
let promptInputModel: PromptInputModel;
@@ -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();

0 commit comments

Comments
 (0)