mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-24 09:21:35 +01:00
Merge pull request #210766 from microsoft/tyriar/210757_interrupt
PromptInputModel: Detect interrupts and show executing in dev mode
This commit is contained in:
@@ -14,15 +14,23 @@ import { throttle } from 'vs/base/common/decorators';
|
||||
import type { Terminal, IMarker, IBufferCell, IBufferLine, IBuffer } from '@xterm/headless';
|
||||
|
||||
const enum PromptInputState {
|
||||
Unknown,
|
||||
Input,
|
||||
Execute,
|
||||
Unknown = 0,
|
||||
Input = 1,
|
||||
Execute = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* A model of the prompt input state using shell integration and analyzing the terminal buffer. This
|
||||
* may not be 100% accurate but provides a best guess.
|
||||
*/
|
||||
export interface IPromptInputModel {
|
||||
readonly onDidStartInput: Event<IPromptInputModelState>;
|
||||
readonly onDidChangeInput: Event<IPromptInputModelState>;
|
||||
readonly onDidFinishInput: Event<IPromptInputModelState>;
|
||||
/**
|
||||
* Fires immediately before {@link onDidFinishInput} when a SIGINT/Ctrl+C/^C is detected.
|
||||
*/
|
||||
readonly onDidInterrupt: Event<IPromptInputModelState>;
|
||||
|
||||
readonly value: string;
|
||||
readonly cursorIndex: number;
|
||||
@@ -48,6 +56,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
|
||||
private _commandStartX: number = 0;
|
||||
private _continuationPrompt: string | undefined;
|
||||
|
||||
private _lastUserInput: string = '';
|
||||
|
||||
private _value: string = '';
|
||||
get value() { return this._value; }
|
||||
|
||||
@@ -63,6 +73,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
|
||||
readonly onDidChangeInput = this._onDidChangeInput.event;
|
||||
private readonly _onDidFinishInput = this._register(new Emitter<IPromptInputModelState>());
|
||||
readonly onDidFinishInput = this._onDidFinishInput.event;
|
||||
private readonly _onDidInterrupt = this._register(new Emitter<IPromptInputModelState>());
|
||||
readonly onDidInterrupt = this._onDidInterrupt.event;
|
||||
|
||||
constructor(
|
||||
private readonly _xterm: Terminal,
|
||||
@@ -72,14 +84,27 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(this._xterm.onData(e => this._handleInput(e)));
|
||||
this._register(Event.any(
|
||||
this._xterm.onWriteParsed,
|
||||
this._xterm.onCursorMove,
|
||||
this._xterm.onData,
|
||||
this._xterm.onWriteParsed,
|
||||
)(() => this._sync()));
|
||||
this._register(this._xterm.onData(e => this._handleUserInput(e)));
|
||||
|
||||
this._register(onCommandStart(e => this._handleCommandStart(e as { marker: IMarker })));
|
||||
this._register(onCommandExecuted(() => this._handleCommandExecuted()));
|
||||
|
||||
this._register(this.onDidStartInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidStartInput')));
|
||||
this._register(this.onDidChangeInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidChangeInput')));
|
||||
this._register(this.onDidFinishInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidFinishInput')));
|
||||
this._register(this.onDidInterrupt(() => this._logCombinedStringIfTrace('PromptInputModel#onDidInterrupt')));
|
||||
}
|
||||
|
||||
private _logCombinedStringIfTrace(message: string) {
|
||||
// Only generate the combined string if trace
|
||||
if (this._logService.getLevel() === LogLevel.Trace) {
|
||||
this._logService.trace(message, this.getCombinedString());
|
||||
}
|
||||
}
|
||||
|
||||
setContinuationPrompt(value: string): void {
|
||||
@@ -112,6 +137,7 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
|
||||
this._value = '';
|
||||
this._cursorIndex = 0;
|
||||
this._onDidStartInput.fire(this._createStateObject());
|
||||
this._onDidChangeInput.fire(this._createStateObject());
|
||||
}
|
||||
|
||||
private _handleCommandExecuted() {
|
||||
@@ -119,13 +145,16 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
|
||||
return;
|
||||
}
|
||||
|
||||
this._state = PromptInputState.Execute;
|
||||
this._cursorIndex = -1;
|
||||
this._onDidFinishInput.fire(this._createStateObject());
|
||||
}
|
||||
this._ghostTextIndex = -1;
|
||||
const event = this._createStateObject();
|
||||
if (this._lastUserInput === '\u0003') {
|
||||
this._onDidInterrupt.fire(event);
|
||||
}
|
||||
|
||||
private _handleInput(data: string) {
|
||||
this._sync();
|
||||
this._state = PromptInputState.Execute;
|
||||
this._onDidFinishInput.fire(event);
|
||||
this._onDidChangeInput.fire(event);
|
||||
}
|
||||
|
||||
@throttle(0)
|
||||
@@ -207,6 +236,10 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleUserInput(e: string) {
|
||||
this._lastUserInput = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect ghost text by looking for italic or dim text in or after the cursor and
|
||||
* non-italic/dim text in the cell closest non-whitespace cell before the cursor.
|
||||
|
||||
@@ -91,7 +91,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
|
||||
) {
|
||||
super();
|
||||
|
||||
this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandFinished, this._logService));
|
||||
this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandExecuted, this._logService));
|
||||
|
||||
// Pull command line from the buffer if it was not set explicitly
|
||||
this._register(this.onCommandExecuted(command => {
|
||||
|
||||
+21
-1
@@ -78,7 +78,7 @@ suite('PromptInputModel', () => {
|
||||
await assertPromptInput('|');
|
||||
});
|
||||
|
||||
test('should not fire events when nothing changes', async () => {
|
||||
test('should not fire onDidChangeInput events when nothing changes', async () => {
|
||||
const events: IPromptInputModelState[] = [];
|
||||
store.add(promptInputModel.onDidChangeInput(e => events.push(e)));
|
||||
|
||||
@@ -108,6 +108,26 @@ suite('PromptInputModel', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should fire onDidInterrupt followed by onDidFinish when ctrl+c is pressed', async () => {
|
||||
await writePromise('$ ');
|
||||
fireCommandStart();
|
||||
await assertPromptInput('|');
|
||||
|
||||
await writePromise('foo');
|
||||
await assertPromptInput('foo|');
|
||||
|
||||
await new Promise<void>(r => {
|
||||
store.add(promptInputModel.onDidInterrupt(() => {
|
||||
// Fire onDidFinishInput immediately after onDidInterrupt
|
||||
store.add(promptInputModel.onDidFinishInput(() => {
|
||||
r();
|
||||
}));
|
||||
}));
|
||||
xterm.input('\x03');
|
||||
writePromise('^C').then(() => fireCommandExecuted());
|
||||
});
|
||||
});
|
||||
|
||||
test('cursor navigation', async () => {
|
||||
await writePromise('$ ');
|
||||
fireCommandStart();
|
||||
|
||||
+3
-1
@@ -246,10 +246,12 @@ class DevModeContribution extends Disposable implements ITerminalContribution {
|
||||
const promptInputModel = commandDetection.promptInputModel;
|
||||
if (promptInputModel) {
|
||||
const name = localize('terminalDevMode', 'Terminal Dev Mode');
|
||||
const isExecuting = promptInputModel.cursorIndex === -1;
|
||||
this._statusbarEntry = {
|
||||
name,
|
||||
text: `$(terminal) ${promptInputModel.getCombinedString()}`,
|
||||
text: `$(${isExecuting ? 'loading~spin' : 'terminal'}) ${promptInputModel.getCombinedString()}`,
|
||||
ariaLabel: name,
|
||||
tooltip: 'The detected terminal prompt input',
|
||||
kind: 'prominent'
|
||||
};
|
||||
if (!this._statusbarEntryAccessor.value) {
|
||||
|
||||
Reference in New Issue
Block a user