From e13cf45abc08e2af12326107ba2adeb7dea121e0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 18 Mar 2022 05:32:33 -0700 Subject: [PATCH 1/2] Workaround for conpty's resize behavior Fixes #143067 --- .../common/capabilities/capabilities.ts | 1 + .../commandDetectionCapability.ts | 105 +++++++++++++++++- .../terminal/common/terminalProcess.ts | 1 + 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index f87d3c7ffdf..cc4c3218908 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -128,6 +128,7 @@ export interface ITerminalCommand { marker?: IXtermMarker; endMarker?: IXtermMarker; executedMarker?: IXtermMarker; + commandStartLineContent?: string; getOutput(): string | undefined; hasOutput: boolean; } diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index bea3d7ba028..ebec6b83900 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -19,6 +19,7 @@ export interface ICurrentPartialCommand { commandStartMarker?: IMarker; commandStartX?: number; + commandStartLineContent?: string; commandLines?: IMarker; @@ -33,6 +34,11 @@ export interface ICurrentPartialCommand { command?: string; } +interface ITerminalDimensions { + cols: number; + rows: number; +} + export class CommandDetectionCapability implements ICommandDetectionCapability { readonly type = TerminalCapability.CommandDetection; @@ -43,6 +49,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { private _isWindowsPty: boolean = false; private _onCursorMoveListener?: IDisposable; private _commandMarkers: IMarker[] = []; + private _dimensions: ITerminalDimensions; get commands(): readonly ITerminalCommand[] { return this._commands; } @@ -54,7 +61,86 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { constructor( private readonly _terminal: Terminal, @ILogService private readonly _logService: ILogService - ) { } + ) { + this._dimensions = { + cols: this._terminal.cols, + rows: this._terminal.rows + }; + this._terminal.onResize(e => this._handleResize(e)); + } + + private _handleResize(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 + // there was no scrollback). + // + // On resize this workaround will wait for a conpty reprint to occur by waiting for the + // cursor to move, it will then calculate the number of lines that the commands within the + // viewport _may have_ shifted. After verifying the content of the current line is + // incorrect, the line after shifting is checked and if that matches delete events are fired + // on the xterm.js buffer to move the markers. + // + // While a bit hacky, this approach is quite safe and seems to work great at least for pwsh. + if (this._isWindowsPty) { + const baseY = this._terminal.buffer.active.baseY; + const rowsDifference = e.rows - this._dimensions.rows; + // Only do when rows increase, do in the next frame as this needs to happen after + // conpty reprints the screen + if (rowsDifference > 0) { + this._waitForCursorMove().then(() => { + // Calculate the number of lines the content may have shifted, this will max out at + // scrollback count since the standard behavior will be used then + const potentialShiftedLineCount = Math.min(rowsDifference, baseY); + // For each command within the viewport, assume commands are in the correct order + for (let i = this.commands.length - 1; i >= 0; i--) { + const command = this.commands[i]; + if (!command.marker || command.marker.line < baseY || command.commandStartLineContent === undefined) { + break; + } + const line = this._terminal.buffer.active.getLine(command.marker.line); + if (!line || line.translateToString(true) === command.commandStartLineContent) { + continue; + } + const shiftedY = command.marker.line - potentialShiftedLineCount; + const shiftedLine = this._terminal.buffer.active.getLine(shiftedY); + if (shiftedLine?.translateToString(true) !== command.commandStartLineContent) { + continue; + } + // HACK: xterm.js doesn't expose this by design as it's an internal core + // function an embedder could easily do damage with. Additionally, this + // can't really be upstreamed since the event relies on shell integration to + // verify the shifting is necessary. + (this._terminal as any)._core._bufferService.buffer.lines.onDeleteEmitter.fire({ + index: this._terminal.buffer.active.baseY, + amount: potentialShiftedLineCount + }); + } + }); + } + } + this._dimensions.cols = e.cols; + this._dimensions.rows = e.rows; + } + + private _waitForCursorMove(): Promise { + const cursorX = this._terminal.buffer.active.cursorX; + const cursorY = this._terminal.buffer.active.cursorY; + let totalDelay = 0; + return new Promise((resolve, reject) => { + const interval = setInterval(() => { + if (cursorX !== this._terminal.buffer.active.cursorX || cursorY !== this._terminal.buffer.active.cursorY) { + resolve(); + clearInterval(interval); + return; + } + totalDelay += 10; + if (totalDelay > 1000) { + clearInterval(interval); + resolve(); + } + }, 10); + }); + } setCwd(value: string) { this._cwd = value; @@ -123,6 +209,12 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { }); } this._currentCommand.commandStartMarker = this._terminal.registerMarker(0); + if (this._currentCommand.commandStartMarker) { + const line = this._terminal.buffer.active.getLine(this._currentCommand.commandStartMarker.line); + if (line) { + this._currentCommand.commandStartLineContent = line.translateToString(true); + } + } this._onCommandStarted.fire({ marker: this._currentCommand.commandStartMarker } as ITerminalCommand); }); } else { @@ -192,6 +284,12 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { this._commandMarkers = this._commandMarkers.sort((a, b) => a.line - b.line); } this._currentCommand.commandStartMarker = this._commandMarkers[0]; + if (this._currentCommand.commandStartMarker) { + const line = this._terminal.buffer.active.getLine(this._currentCommand.commandStartMarker.line); + if (line) { + this._currentCommand.commandStartLineContent = line.translateToString(true); + } + } this._currentCommand.commandExecutedMarker = this._commandMarkers[this._commandMarkers.length - 1]; } @@ -229,6 +327,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { timestamp, cwd: this._cwd, exitCode: this._exitCode, + commandStartLineContent: this._currentCommand.commandStartLineContent, hasOutput: !!(executedMarker && endMarker && executedMarker?.line < endMarker!.line), getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer) }; @@ -246,6 +345,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { } serializeCommands(): ISerializedCommand[] { + // TODO: Support commandStartLineContent const serialized: ISerializedCommand[] = this.commands.map(e => { return { startLine: e.marker?.line, @@ -255,6 +355,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { command: e.command, cwd: e.cwd, exitCode: e.exitCode, + commandStartLineContent: e.commandStartLineContent, timestamp: e.timestamp }; }); @@ -267,6 +368,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { command: '', cwd: this._cwd, exitCode: undefined, + commandStartLineContent: undefined, timestamp: 0, }); } @@ -299,6 +401,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { executedMarker, timestamp: e.timestamp, cwd: e.cwd, + commandStartLineContent: e.commandStartLineContent, exitCode: e.exitCode, hasOutput: !!(executedMarker && endMarker && executedMarker.line < endMarker.line), getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer) diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts index 3e9613ce4ce..9f3e1b9be28 100644 --- a/src/vs/platform/terminal/common/terminalProcess.ts +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -76,6 +76,7 @@ export interface ISerializedCommand { endLine: number | undefined; executedLine: number | undefined; exitCode: number | undefined; + commandStartLineContent: string | undefined; timestamp: number; } export interface IPtyHostProcessReplayEvent { From d431b962011d6d29e496ed3506c08a6a1d23b5b1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 18 Mar 2022 05:34:51 -0700 Subject: [PATCH 2/2] Remove todo --- .../terminal/common/capabilities/commandDetectionCapability.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index ebec6b83900..37d65c2cb41 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -345,7 +345,6 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { } serializeCommands(): ISerializedCommand[] { - // TODO: Support commandStartLineContent const serialized: ISerializedCommand[] = this.commands.map(e => { return { startLine: e.marker?.line,