Merge pull request #3744 from mastertyko/fix/3721-tui-autocomplete-ghost-lines

fix(pi-tui): clear autocomplete rows from content bottom
This commit is contained in:
Jeremy McSpadden 2026-04-07 21:32:14 -05:00 committed by GitHub
commit f9a6cac958
2 changed files with 88 additions and 1 deletions

View file

@ -616,9 +616,10 @@ export class TUI extends Container {
const height = this.terminal.rows;
let viewportTop = Math.max(0, this.maxLinesRendered - height);
let prevViewportTop = this.previousViewportTop;
let contentCursorRow = this.cursorRow;
let hardwareCursorRow = this.hardwareCursorRow;
const computeLineDiff = (targetRow: number): number => {
const currentScreenRow = hardwareCursorRow - prevViewportTop;
const currentScreenRow = contentCursorRow - prevViewportTop;
const targetScreenRow = targetRow - viewportTop;
return targetScreenRow - currentScreenRow;
};
@ -805,6 +806,7 @@ export class TUI extends Container {
buffer += "\r\n".repeat(scroll);
prevViewportTop += scroll;
viewportTop += scroll;
contentCursorRow = moveTargetRow;
hardwareCursorRow = moveTargetRow;
}

View file

@ -0,0 +1,85 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { CURSOR_MARKER, TUI, type Component, type Terminal } from "@gsd/pi-tui";
class MockTTYTerminal implements Terminal {
public writtenData: string[] = [];
readonly isTTY = true;
start(_onInput: (data: string) => void, _onResize: () => void): void {}
stop(): void {}
async drainInput(_maxMs?: number, _idleMs?: number): Promise<void> {}
write(data: string): void {
this.writtenData.push(data);
}
get columns(): number {
return 80;
}
get rows(): number {
return 24;
}
get kittyProtocolActive(): boolean {
return false;
}
moveBy(_lines: number): void {}
hideCursor(): void {}
showCursor(): void {}
clearLine(): void {}
clearFromCursor(): void {}
clearScreen(): void {}
setTitle(_title: string): void {}
}
class DynamicLinesComponent implements Component {
public lines: string[];
constructor(lines: string[]) {
this.lines = lines;
}
render(_width: number): string[] {
return this.lines;
}
invalidate(): void {}
}
describe("TUI autocomplete shrink clearing (#3721)", () => {
it("clears deleted autocomplete rows relative to the content bottom, not the IME cursor row", () => {
const terminal = new MockTTYTerminal();
const tui = new TUI(terminal, false);
const component = new DynamicLinesComponent([
"top border",
`prompt${CURSOR_MARKER}`,
"editor body",
"autocomplete row 1",
"autocomplete row 2",
"autocomplete row 3",
]);
tui.addChild(component);
(tui as any).doRender();
terminal.writtenData = [];
component.lines = [
"top border",
`prompt${CURSOR_MARKER}`,
"editor body",
"autocomplete row 1",
];
(tui as any).doRender();
assert.ok(terminal.writtenData.length >= 1, "shrink render should write a differential buffer");
assert.ok(
terminal.writtenData[0].startsWith("\x1b[?2026h\x1b[2A\r"),
`expected shrink diff to move up from prior content bottom, got ${JSON.stringify(terminal.writtenData[0])}`,
);
});
});