move invalidation to the commandDetectionCapability (#146563)

This commit is contained in:
Megan Rogge 2022-05-06 16:49:05 -07:00 committed by GitHub
parent 2dff5deef7
commit 7e0ee0d395
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 97 additions and 19 deletions

View file

@ -92,6 +92,7 @@ export interface ICommandDetectionCapability {
readonly cwd: string | undefined;
readonly onCommandStarted: Event<ITerminalCommand>;
readonly onCommandFinished: Event<ITerminalCommand>;
readonly onCommandInvalidated: Event<ITerminalCommand[]>;
setCwd(value: string): void;
setIsWindowsPty(value: boolean): void;
/**

View file

@ -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<ITerminalCommand>();
readonly onCommandStarted = this._onCommandStarted.event;
private readonly _onBeforeCommandFinished = new Emitter<IBeforeCommandFinishedEvent>();
readonly onBeforeCommandFinished = this._onBeforeCommandFinished.event;
private readonly _onCommandFinished = new Emitter<ITerminalCommand>();
readonly onCommandFinished = this._onCommandFinished.event;
private readonly _onCommandInvalidated = new Emitter<ITerminalCommand[]>();
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<void> {
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 = {};

View file

@ -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
*/

View file

@ -46,6 +46,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon {
private _hoverDelayer: Delayer<void>;
private _commandStartedListener: IDisposable | undefined;
private _commandFinishedListener: IDisposable | undefined;
private _commandClearedListener: IDisposable | undefined;
private _contextMenuVisible: boolean = false;
private _decorations: Map<number, IDisposableDecoration> = 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);
});
}

View file

@ -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();
}