diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 1280417b..78ce646a 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -438,6 +438,37 @@ describe('InputHandler', () => { inputHandler.eraseInLine(Params.fromArray([2])); assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false); }); + it('ED2 with scrollOnEraseInDisplay turned on', async () => { + const inputHandler = new TestInputHandler( + bufferService, + new MockCharsetService(), + new MockCoreService(), + new MockLogService(), + new MockOptionsService({ scrollOnEraseInDisplay: true }), + new MockOscLinkService(), + new MockCoreMouseService(), + new MockUnicodeService() + ); + const aLine = Array(bufferService.cols + 1).join('a'); + // add 2 full lines of text. + await inputHandler.parseP(aLine); + await inputHandler.parseP(aLine); + + inputHandler.eraseInDisplay(Params.fromArray([2])); + // those 2 lines should have been pushed to scrollback. + assert.equal(bufferService.rows + 2, bufferService.buffer.lines.length); + assert.equal(bufferService.buffer.ybase, 2); + assert.equal(bufferService.buffer.lines.get(0)?.translateToString(), aLine); + assert.equal(bufferService.buffer.lines.get(1)?.translateToString(), aLine); + + // Move to last line and add more text. + bufferService.buffer.y = bufferService.rows - 1; + bufferService.buffer.x = 0; + await inputHandler.parseP(aLine); + inputHandler.eraseInDisplay(Params.fromArray([2])); + // Screen should have been scrolled by a full screen size. + assert.equal(bufferService.rows * 2 + 2, bufferService.buffer.lines.length); + }); it('eraseInDisplay', async () => { const bufferService = new MockBufferService(80, 7); const inputHandler = new TestInputHandler( diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 8308e6de..0e151173 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1220,12 +1220,27 @@ export class InputHandler extends Disposable implements IInputHandler { this._dirtyRowTracker.markDirty(0); break; case 2: - j = this._bufferService.rows; - this._dirtyRowTracker.markDirty(j - 1); - while (j--) { - this._resetBufferLine(j, respectProtect); + if (this._optionsService.rawOptions.scrollOnEraseInDisplay) { + j = this._bufferService.rows; + this._dirtyRowTracker.markRangeDirty(0, j - 1); + while (j--) { + const currentLine = this._activeBuffer.lines.get(this._activeBuffer.ybase + j); + if (currentLine?.getTrimmedLength()) { + break; + } + } + for (; j >= 0; j--) { + this._bufferService.scroll(this._eraseAttrData()); + } + } + else { + j = this._bufferService.rows; + this._dirtyRowTracker.markDirty(j - 1); + while (j--) { + this._resetBufferLine(j, respectProtect); + } + this._dirtyRowTracker.markDirty(0); } - this._dirtyRowTracker.markDirty(0); break; case 3: // Clear scrollback (everything not in viewport) diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 86d9ca71..6ad48b93 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -32,6 +32,7 @@ export const DEFAULT_OPTIONS: Readonly> = { logLevel: 'info', logger: null, scrollback: 1000, + scrollOnEraseInDisplay: false, scrollOnUserInput: true, scrollSensitivity: 1, screenReaderMode: false, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index be1dec06..9c3aebf7 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -254,6 +254,7 @@ export interface ITerminalOptions { windowOptions?: IWindowOptions; wordSeparator?: string; overviewRuler?: IOverviewRulerOptions; + scrollOnEraseInDisplay?: boolean; [key: string]: any; cancelEvents: boolean; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 1085db9b..8d1facc7 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -186,6 +186,13 @@ declare module '@xterm/headless' { */ scrollback?: number; + /** + * If enabled the Erase in Display All (ED2) escape sequence will push + * erased text to scrollback, instead of clearing only the viewport portion. + * This emulates PuTTY's default clear screen behavior. + */ + scrollOnEraseInDisplay?: boolean; + /** * The scrolling speed multiplier used for adjusting normal scrolling speed. */ diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 15a03327..1310009e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -257,6 +257,13 @@ declare module '@xterm/xterm' { */ scrollback?: number; + /** + * If enabled the Erase in Display All (ED2) escape sequence will push + * erased text to scrollback, instead of clearing only the viewport portion. + * This emulates PuTTY's default clear screen behavior. + */ + scrollOnEraseInDisplay?: boolean; + /** * Whether to scroll to the bottom whenever there is some user input. The * default is true.