diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index acbf92a5f0e..49d9f38b2cb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -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('terminalService'); export const ITerminalConfigurationService = createDecorator('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 { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 4435b45e013..f8736293859 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -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); })); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 9a7bbb232b8..9ef44f9bcea 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -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; + })); } } } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 0bb39d8b94c..75c427be3c6 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -138,6 +138,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest readonly onBell = this._onBell.event; private readonly _onAcceptedCompletion = this._register(new Emitter()); readonly onAcceptedCompletion = this._onAcceptedCompletion.event; + private readonly _onDidRequestCompletions = this._register(new Emitter()); + readonly onDidRequestCompletions = this._onDidRequestCompletions.event; + private readonly _onDidReceiveCompletions = this._register(new Emitter()); + readonly onDidReceiveCompletions = this._onDidReceiveCompletions.event; constructor( private readonly _cachedPwshCommands: Set, @@ -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;