diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 6e72fa32a00..221d81e7ba6 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -92,6 +92,7 @@ export interface ICommandDetectionCapability { readonly cwd: string | undefined; readonly onCommandStarted: Event; readonly onCommandFinished: Event; + readonly onCommandInvalidated: Event; setCwd(value: string): void; setIsWindowsPty(value: boolean): void; /** diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 5bc1e157bd2..01f37b2386e 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -42,6 +42,11 @@ interface ITerminalDimensions { rows: number; } +interface IBeforeCommandFinishedEvent { + command: ITerminalCommand; + veto?: boolean; +} + export class CommandDetectionCapability implements ICommandDetectionCapability { readonly type = TerminalCapability.CommandDetection; @@ -67,8 +72,12 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { private readonly _onCommandStarted = new Emitter(); readonly onCommandStarted = this._onCommandStarted.event; + private readonly _onBeforeCommandFinished = new Emitter(); + readonly onBeforeCommandFinished = this._onBeforeCommandFinished.event; private readonly _onCommandFinished = new Emitter(); readonly onCommandFinished = this._onCommandFinished.event; + private readonly _onCommandInvalidated = new Emitter(); + readonly onCommandInvalidated = this._onCommandInvalidated.event; constructor( private readonly _terminal: Terminal, @@ -79,6 +88,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { rows: this._terminal.rows }; this._terminal.onResize(e => this._handleResize(e)); + this._setupClearListeners(); } private _handleResize(e: { cols: number; rows: number }) { @@ -89,6 +99,36 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { this._dimensions.rows = e.rows; } + private _setupClearListeners() { + // Setup listeners for when clear is run in the shell. Since we don't know immediately if + // this is a Windows pty, listen to both routes and do the Windows check inside them + + // For a Windows backend we cannot listen to CSI J, instead we assume running clear or + // cls will clear all commands in the viewport. This is not perfect but it's right most + // of the time. + this.onBeforeCommandFinished(event => { + if (this._isWindowsPty) { + if (event.command.command.trim().toLowerCase() === 'clear' || event.command.command.trim().toLowerCase() === 'cls') { + this._clearCommandsInViewport(); + // Prevent current command to get to command finished listeners + event.veto = true; + } + } + }); + + // For non-Windows backends we can just listen to CSI J which is what the clear command + // typically emits. + this._terminal.parser.registerCsiHandler({ final: 'J' }, params => { + if (!this._isWindowsPty) { + if (params.length >= 1 && (params[0] === 2 || params[0] === 3)) { + this._clearCommandsInViewport(); + } + } + // We don't want to override xterm.js' default behavior, just augment it + return false; + }); + } + private _preHandleResizeWindows(e: { cols: number; rows: number }) { // Resize behavior is different under conpty; instead of bringing parts of the scrollback // back into the viewport, new lines are inserted at the bottom (ie. the same behavior as if @@ -138,6 +178,22 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { } } + private _clearCommandsInViewport(): void { + // Find the number of commands on the tail end of the array that are within the viewport + let count = 0; + for (let i = this._commands.length - 1; i >= 0; i--) { + const line = this._commands[i].marker?.line; + if (line && line < this._terminal.buffer.active.baseY) { + break; + } + count++; + } + // Remove them + if (count > 0) { + this._onCommandInvalidated.fire(this._commands.splice(this._commands.length - count, count)); + } + } + private _waitForCursorMove(): Promise { const cursorX = this._terminal.buffer.active.cursorX; const cursorY = this._terminal.buffer.active.cursorY; @@ -346,7 +402,13 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { }; this._commands.push(newCommand); this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); - this._onCommandFinished.fire(newCommand); + + // Fire the command finished event provided there is no veto + const beforeEvent: IBeforeCommandFinishedEvent = { command: newCommand }; + this._onBeforeCommandFinished.fire(beforeEvent); + if (!beforeEvent.veto) { + this._onCommandFinished.fire(newCommand); + } } this._currentCommand.previousCommandMarker = this._currentCommand.commandStartMarker; this._currentCommand = {}; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index c48979fa4d6..7a9ca118355 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -902,11 +902,6 @@ export interface IXtermTerminal { */ clearBuffer(): void; - /** - * Clears decorations - for example, when shell integration is disabled. - */ - clearDecorations(): void; - /** * Clears the search result decorations */ diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index efaa21a930c..e0a420f570a 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -46,6 +46,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { private _hoverDelayer: Delayer; private _commandStartedListener: IDisposable | undefined; private _commandFinishedListener: IDisposable | undefined; + private _commandClearedListener: IDisposable | undefined; private _contextMenuVisible: boolean = false; private _decorations: Map = new Map(); private _placeholderDecoration: IDecoration | undefined; @@ -63,7 +64,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { @IOpenerService private readonly _openerService: IOpenerService ) { super(); - this._register(toDisposable(() => this.clearDecorations(true))); + this._register(toDisposable(() => this._dispose())); this._register(this._contextMenuService.onDidShowContextMenu(() => this._contextMenuVisible = true)); this._register(this._contextMenuService.onDidHideContextMenu(() => this._contextMenuVisible = false)); this._hoverDelayer = this._register(new Delayer(this._configurationService.getValue('workbench.hover.delay'))); @@ -111,11 +112,10 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } } - public clearDecorations(disableDecorations?: boolean): void { - if (disableDecorations) { - this._commandStartedListener?.dispose(); - this._commandFinishedListener?.dispose(); - } + private _dispose(): void { + this._commandStartedListener?.dispose(); + this._commandFinishedListener?.dispose(); + this._commandClearedListener?.dispose(); this._placeholderDecoration?.dispose(); this._placeholderDecoration?.marker.dispose(); for (const value of this._decorations.values()) { @@ -129,11 +129,13 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { if (this._capabilities.has(TerminalCapability.CommandDetection)) { this._addCommandFinishedListener(); this._addCommandStartedListener(); + this._addCommandClearedListener(); } else { this._register(this._capabilities.onDidAddCapability(c => { if (c === TerminalCapability.CommandDetection) { this._addCommandFinishedListener(); this._addCommandStartedListener(); + this._addCommandClearedListener(); } })); } @@ -141,6 +143,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { if (c === TerminalCapability.CommandDetection) { this._commandStartedListener?.dispose(); this._commandFinishedListener?.dispose(); + this._commandClearedListener?.dispose(); } })); } @@ -171,12 +174,29 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { for (const command of capability.commands) { this.registerCommandDecoration(command); } - this._commandFinishedListener = capability.onCommandFinished(command => { - if (command.command.trim().toLowerCase() === 'clear' || command.command.trim().toLowerCase() === 'cls') { - this.clearDecorations(); - return; + this._commandFinishedListener = capability.onCommandFinished(command => this.registerCommandDecoration(command)); + } + + private _addCommandClearedListener(): void { + if (this._commandClearedListener) { + return; + } + const capability = this._capabilities.get(TerminalCapability.CommandDetection); + if (!capability) { + return; + } + + this._commandClearedListener = capability.onCommandInvalidated(commands => { + for (const command of commands) { + const id = command.marker?.id; + if (id) { + const match = this._decorations.get(id); + if (match) { + match.decoration.dispose(); + dispose(match.disposables); + } + } } - this.registerCommandDecoration(command); }); } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index a1efe91c333..27c0a94e424 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -250,10 +250,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { } clearDecorations(): void { - this._decorationAddon?.clearDecorations(); + this._decorationAddon?.dispose(); + this._decorationAddon = undefined; } - forceRefresh() { this._core.viewport?._innerRefresh(); }