Merge pull request #225336 from microsoft/tyriar/225330

Pause terminal input while completions are requested
This commit is contained in:
Daniel Imms
2024-08-10 18:14:10 -07:00
committed by GitHub
4 changed files with 40 additions and 7 deletions
@@ -30,6 +30,7 @@ import { ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP_TYPE, SIDE_GROUP_TYPE } from 'vs/wo
import type { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand';
import type { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';
import type { IMenu } from 'vs/platform/actions/common/actions';
import type { Barrier } from 'vs/base/common/async';
export const ITerminalService = createDecorator<ITerminalService>('terminalService');
export const ITerminalConfigurationService = createDecorator<ITerminalConfigurationService>('terminalConfigurationService');
@@ -1050,6 +1051,12 @@ export interface ITerminalInstance extends IBaseTerminalInstance {
* @returns Whether the context menu should be suppressed.
*/
handleMouseEvent(event: MouseEvent, contextMenu: IMenu): Promise<{ cancelContextMenu: boolean } | void>;
/**
* Pause input events until the provided barrier is resolved.
* @param barrier The barrier to wait for until input events can continue.
*/
pauseInputEvents(barrier: Barrier): void;
}
export const enum XtermTerminalConstants {
@@ -10,7 +10,7 @@ import * as dom from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Orientation } from 'vs/base/browser/ui/sash/sash';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { AutoOpenBarrier, Promises, disposableTimeout, timeout } from 'vs/base/common/async';
import { AutoOpenBarrier, Barrier, Promises, disposableTimeout, timeout } from 'vs/base/common/async';
import { Codicon } from 'vs/base/common/codicons';
import { debounce } from 'vs/base/common/decorators';
import { ErrorNoTelemetry, onUnexpectedError } from 'vs/base/common/errors';
@@ -198,6 +198,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
private _lineDataEventAddon: LineDataEventAddon | undefined;
private readonly _scopedContextKeyService: IContextKeyService;
private _resizeDebouncer?: TerminalResizeDebouncer;
private _pauseInputEventBarrier: Barrier | undefined;
pauseInputEvents(barrier: Barrier): void {
this._pauseInputEventBarrier = barrier;
}
readonly capabilities = this._register(new TerminalCapabilityStoreMultiplexer());
readonly statusList: ITerminalStatusList;
@@ -806,6 +810,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this._register(this._processManager.onProcessData(e => this._onProcessData(e)));
this._register(xterm.raw.onData(async data => {
await this._pauseInputEventBarrier?.wait();
await this._processManager.write(data);
this._onDidInputData.fire(data);
}));
@@ -5,6 +5,7 @@
import type { Terminal as RawXtermTerminal } from '@xterm/xterm';
import * as dom from 'vs/base/browser/dom';
import { AutoOpenBarrier } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -157,15 +158,28 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo
return;
}
if (this._terminalSuggestWidgetVisibleContextKey) {
this._addon.value = this._instantiationService.createInstance(SuggestAddon, TerminalSuggestContribution._cachedPwshCommands, this._instance.capabilities, this._terminalSuggestWidgetVisibleContextKey);
xterm.loadAddon(this._addon.value);
this._addon.value.setPanel(dom.findParentWithClass(xterm.element!, 'panel')!);
this._addon.value.setScreen(xterm.element!.querySelector('.xterm-screen')!);
this.add(this._instance.onDidBlur(() => this._addon.value?.hideSuggestWidget()));
this.add(this._addon.value.onAcceptedCompletion(async text => {
const addon = this._addon.value = this._instantiationService.createInstance(SuggestAddon, TerminalSuggestContribution._cachedPwshCommands, this._instance.capabilities, this._terminalSuggestWidgetVisibleContextKey);
xterm.loadAddon(addon);
addon.setPanel(dom.findParentWithClass(xterm.element!, 'panel')!);
addon.setScreen(xterm.element!.querySelector('.xterm-screen')!);
this.add(this._instance.onDidBlur(() => addon.hideSuggestWidget()));
this.add(addon.onAcceptedCompletion(async text => {
this._instance.focus();
this._instance.sendText(text, false);
}));
// If completions are requested, pause and queue input events until completions are
// received. This fixing some problems in PowerShell, particularly enter not executing
// when typing quickly and some characters being printed twice.
let barrier: AutoOpenBarrier | undefined;
this.add(addon.onDidRequestCompletions(() => {
barrier = new AutoOpenBarrier(2000);
this._instance.pauseInputEvents(barrier);
}));
this.add(addon.onDidReceiveCompletions(() => {
barrier?.open();
barrier = undefined;
}));
}
}
}
@@ -138,6 +138,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
readonly onBell = this._onBell.event;
private readonly _onAcceptedCompletion = this._register(new Emitter<string>());
readonly onAcceptedCompletion = this._onAcceptedCompletion.event;
private readonly _onDidRequestCompletions = this._register(new Emitter<void>());
readonly onDidRequestCompletions = this._onDidRequestCompletions.event;
private readonly _onDidReceiveCompletions = this._register(new Emitter<void>());
readonly onDidReceiveCompletions = this._onDidReceiveCompletions.event;
constructor(
private readonly _cachedPwshCommands: Set<SimpleCompletionItem>,
@@ -211,6 +215,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
// completions being requested again right after accepting a completion
if (this._lastUserDataTimestamp > this._lastAcceptedCompletionTimestamp) {
this._onAcceptedCompletion.fire(SuggestAddon.requestCompletionsSequence);
this._onDidRequestCompletions.fire();
}
}
@@ -329,6 +334,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
private _replacementLength: number = 0;
private _handleCompletionsSequence(terminal: Terminal, data: string, command: string, args: string[]): void {
this._onDidReceiveCompletions.fire();
// Nothing to handle if the terminal is not attached
if (!terminal.element || !this._enableWidget || !this._promptInputModel) {
return;