From d6cfe3db5dfdbfd2a08c3f9bcfcde8a283aa479d Mon Sep 17 00:00:00 2001 From: Taran <156827032+tarann26@users.noreply.github.com> Date: Mon, 10 Aug 2026 07:38:58 -0400 Subject: [PATCH] Fix viewport staying pinned to the top after ESC[3J clears the scrollback (#6081) --- src/common/InputHandler.test.ts | 48 +++++++++++++++++++++++++++++++++ src/common/InputHandler.ts | 5 ++++ 2 files changed, 53 insertions(+) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 9eab8cfa2..3534ff5ab 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -2394,6 +2394,54 @@ describe('InputHandler', () => { }); }); + describe('ED3 - erase saved lines', () => { + beforeEach(() => { + bufferService.resize(10, 5); + }); + it('should stop locking the viewport when the scrollback the user scrolled into is erased', async () => { + for (let i = 0; i < 20; ++i) { + await inputHandler.parseP(`old ${i}\r\n`); + } + bufferService.scrollLines(-5); + assert.strictEqual(bufferService.isUserScrolling, true); + + await inputHandler.parseP('\x1b[3J'); + + assert.strictEqual(bufferService.isUserScrolling, false); + assert.strictEqual(bufferService.buffer.ybase, 0); + assert.strictEqual(bufferService.buffer.ydisp, 0); + + for (let i = 0; i < 20; ++i) { + await inputHandler.parseP(`new ${i}\r\n`); + } + + assert.strictEqual(bufferService.buffer.ydisp, bufferService.buffer.ybase); + }); + it('should not reset the scroll position when erasing scrollback on a resized alt buffer', async () => { + bufferService.resize(10, 50); + bufferService.resize(10, 5); + for (let i = 0; i < 20; ++i) { + await inputHandler.parseP(`old ${i}\r\n`); + } + bufferService.scrollLines(-5); + const ydisp = bufferService.buffer.ydisp; + + await inputHandler.parseP('\x1b[?1049h'); + for (let i = 0; i < 20; ++i) { + await inputHandler.parseP(`new ${i}\r\n`); + } + await inputHandler.parseP('\x1b[3J\x1b[?1049l'); + + assert.strictEqual(bufferService.isUserScrolling, true); + assert.strictEqual(bufferService.buffer.ydisp, ydisp); + + await inputHandler.parseP('more\r\n'); + + assert.strictEqual(bufferService.isUserScrolling, true); + assert.strictEqual(bufferService.buffer.ydisp, ydisp); + }); + }); + describe('DECSCA and DECSED/DECSEL', () => { it('default is unprotected', async () => { await inputHandler.parseP('some text'); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 0b12dd8e1..995fe7670 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1286,6 +1286,11 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.trimStart(scrollBackSize); this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0); this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0); + // isUserScrolling tracks the normal buffer's viewport, so ED3 on the alt + // screen must not touch it + if (this._activeBuffer === this._bufferService.buffers.normal) { + this._bufferService.isUserScrolling = false; + } // Force a scroll event to refresh viewport this._onScroll.fire(0); }