From dc01e4e97b0c4426e9b8017eef182e6b10e24e47 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 May 2026 21:00:49 -0700 Subject: [PATCH] Add test around viewport scrolling behavior --- addons/addon-search/src/SearchAddon.ts | 16 ++++++- addons/addon-search/test/SearchAddon.test.ts | 47 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index 24b51b699..a6b02792b 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -99,7 +99,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp const nextIndex = this._resolveResultIndex(matches, term, searchOptions, direction, searchKey); const activeMatch = matches[nextIndex]; terminal.select(activeMatch.startX, activeMatch.startY, activeMatch.cellLength); - terminal.scrollToLine(activeMatch.startY); + this._revealResult(activeMatch); this._lastResolvedNavigation = { searchKey, matches, @@ -539,6 +539,20 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp return byRow.get(selection.start.y)?.get(selection.start.x) ?? -1; } + private _revealResult(match: IMatch): void { + const terminal = this._terminal; + if (!terminal) { + return; + } + const viewportTop = terminal.buffer.active.viewportY; + const viewportBottom = viewportTop + terminal.rows - 1; + if (match.startY >= viewportTop && match.endY <= viewportBottom) { + return; + } + const middleY = Math.floor((match.startY + match.endY) / 2); + terminal.scrollToLine(Math.max(0, middleY - Math.floor(terminal.rows / 2))); + } + private _refreshDecorations(matches: IMatch[], decorationOptions: ISearchDecorationOptions, activeMatch: IMatch | undefined): void { const terminal = this._terminal; if (!terminal) { diff --git a/addons/addon-search/test/SearchAddon.test.ts b/addons/addon-search/test/SearchAddon.test.ts index 3d0dbfdb2..dca1c436e 100644 --- a/addons/addon-search/test/SearchAddon.test.ts +++ b/addons/addon-search/test/SearchAddon.test.ts @@ -45,6 +45,53 @@ test.describe('Search Tests', () => { deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true); deepStrictEqual(await ctx.proxy.getSelection(), '$^1_3{}test$#'); }); + test('Search should not scroll when match is already in viewport', async () => { + let dataString = ''; + for (let i = 0; i < 100; i++) { + if (i === 40) { + dataString += `line ${i} needle-in-view`; + } else { + dataString += `line ${i}`; + } + dataString += '\n\r'; + } + await ctx.proxy.write(dataString); + await ctx.page.evaluate('window.term.scrollToLine(35)'); + strictEqual(await ctx.page.evaluate('window.term.buffer.active.viewportY'), 35); + + strictEqual(await ctx.page.evaluate(`window.search.findNext('needle-in-view')`), true); + deepStrictEqual(await ctx.proxy.getSelection(), 'needle-in-view'); + strictEqual(await ctx.page.evaluate('window.term.buffer.active.viewportY'), 35); + }); + test('Search should center the match when it is outside the viewport', async () => { + let dataString = ''; + for (let i = 0; i < 100; i++) { + if (i === 60) { + dataString += `line ${i} needle-center`; + } else { + dataString += `line ${i}`; + } + dataString += '\n\r'; + } + await ctx.proxy.write(dataString); + await ctx.page.evaluate('window.term.scrollToLine(0)'); + strictEqual(await ctx.page.evaluate('window.term.buffer.active.viewportY'), 0); + + strictEqual(await ctx.page.evaluate(`window.search.findNext('needle-center')`), true); + deepStrictEqual(await ctx.proxy.getSelection(), 'needle-center'); + deepStrictEqual(await ctx.page.evaluate(` + const viewportY = window.term.buffer.active.viewportY; + const rows = window.term.rows; + const selection = window.term.getSelectionPosition(); + return { + viewportY, + expectedViewportY: selection.start.y - Math.floor(rows / 2) + }; + `), { + viewportY: 48, + expectedViewportY: 48 + }); + }); test('Incremental Find Previous', async () => { await ctx.proxy.writeln(`package.jsonc\n`); await ctx.proxy.write('package.json pack package.lock');