fix(tui): position cursor for prompt history navigation

closes #5454
This commit is contained in:
Mario Zechner
2026-06-07 10:56:09 +02:00
parent 22ac2713a3
commit 59d7025668
3 changed files with 41 additions and 43 deletions

View File

@@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Fixed
- Fixed prompt history navigation to place the cursor at the start when browsing upward and at the end when browsing downward, so repeated Up/Down traverses multiline prompts immediately ([#5454](https://github.com/earendil-works/pi/issues/5454)).
## [0.78.1] - 2026-06-04 ## [0.78.1] - 2026-06-04
### Fixed ### Fixed

View File

@@ -390,16 +390,16 @@ export class Editor implements Component, Focusable {
// Returned to "current" state - clear editor // Returned to "current" state - clear editor
this.setTextInternal(""); this.setTextInternal("");
} else { } else {
this.setTextInternal(this.history[this.historyIndex] || ""); this.setTextInternal(this.history[this.historyIndex] || "", direction === -1 ? "start" : "end");
} }
} }
/** Internal setText that doesn't reset history state - used by navigateHistory */ /** Internal setText that doesn't reset history state - used by navigateHistory */
private setTextInternal(text: string): void { private setTextInternal(text: string, cursorPlacement: "start" | "end" = "end"): void {
const lines = text.split("\n"); const lines = text.split("\n");
this.state.lines = lines.length === 0 ? [""] : lines; this.state.lines = lines.length === 0 ? [""] : lines;
this.state.cursorLine = this.state.lines.length - 1; this.state.cursorLine = cursorPlacement === "start" ? 0 : this.state.lines.length - 1;
this.setCursorCol(this.state.lines[this.state.cursorLine]?.length || 0); this.setCursorCol(cursorPlacement === "start" ? 0 : this.state.lines[this.state.cursorLine]?.length || 0);
// Reset scroll - render() will adjust to show cursor // Reset scroll - render() will adjust to show cursor
this.scrollOffset = 0; this.scrollOffset = 0;

View File

@@ -122,7 +122,7 @@ describe("Editor component", () => {
editor.handleInput("\x1b[A"); // Up - shows "old prompt" editor.handleInput("\x1b[A"); // Up - shows "old prompt"
editor.handleInput("x"); // Type a character - exits history mode editor.handleInput("x"); // Type a character - exits history mode
assert.strictEqual(editor.getText(), "old promptx"); assert.strictEqual(editor.getText(), "xold prompt");
}); });
it("exits history mode on setText", () => { it("exits history mode on setText", () => {
@@ -222,61 +222,55 @@ describe("Editor component", () => {
assert.strictEqual(editor.getText(), "prompt 5"); assert.strictEqual(editor.getText(), "prompt 5");
}); });
it("allows cursor movement within multi-line history entry with Down", () => { it("places cursor at start after browsing history upward", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
editor.addToHistory("line1\nline2\nline3");
// Browse to the multi-line entry
editor.handleInput("\x1b[A"); // Up - shows entry, cursor at end of line3
assert.strictEqual(editor.getText(), "line1\nline2\nline3");
// Down should exit history since cursor is on last line
editor.handleInput("\x1b[B"); // Down
assert.strictEqual(editor.getText(), ""); // Exited to empty
});
it("allows cursor movement within multi-line history entry with Up", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme); const editor = new Editor(createTestTUI(), defaultEditorTheme);
editor.addToHistory("older entry"); editor.addToHistory("older entry");
editor.addToHistory("line1\nline2\nline3"); editor.addToHistory("line1\nline2\nline3");
// Browse to the multi-line entry editor.handleInput("\x1b[A"); // Up - shows multi-line entry at start
editor.handleInput("\x1b[A"); // Up - shows multi-line, cursor at end of line3 assert.strictEqual(editor.getText(), "line1\nline2\nline3");
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 0 });
// Up should move cursor within the entry (not on first line yet) editor.handleInput("\x1b[A"); // Up again - immediately navigates to older entry
editor.handleInput("\x1b[A"); // Up - cursor moves to line2
assert.strictEqual(editor.getText(), "line1\nline2\nline3"); // Still same entry
editor.handleInput("\x1b[A"); // Up - cursor moves to line1 (now on first visual line)
assert.strictEqual(editor.getText(), "line1\nline2\nline3"); // Still same entry
// Now Up should navigate to older history entry
editor.handleInput("\x1b[A"); // Up - navigate to older
assert.strictEqual(editor.getText(), "older entry"); assert.strictEqual(editor.getText(), "older entry");
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 0 });
}); });
it("navigates from multi-line entry back to newer via Down after cursor movement", () => { it("places cursor at end after browsing history downward", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
editor.addToHistory("older entry");
editor.addToHistory("line1\nline2\nline3");
editor.addToHistory("newer entry");
editor.handleInput("\x1b[A"); // newer entry
editor.handleInput("\x1b[A"); // multi-line entry
editor.handleInput("\x1b[A"); // older entry
editor.handleInput("\x1b[B"); // Down - shows multi-line entry at end
assert.strictEqual(editor.getText(), "line1\nline2\nline3");
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 5 });
editor.handleInput("\x1b[B"); // Down again - immediately navigates to newer entry
assert.strictEqual(editor.getText(), "newer entry");
});
it("allows opposite-direction cursor movement within multi-line history entry", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme); const editor = new Editor(createTestTUI(), defaultEditorTheme);
editor.addToHistory("line1\nline2\nline3"); editor.addToHistory("line1\nline2\nline3");
// Browse to entry and move cursor up editor.handleInput("\x1b[A"); // Up - shows entry at start
editor.handleInput("\x1b[A"); // Up - shows entry, cursor at end assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 0 });
editor.handleInput("\x1b[A"); // Up - cursor to line2
editor.handleInput("\x1b[A"); // Up - cursor to line1
// Now Down should move cursor down within the entry editor.handleInput("\x1b[B"); // Down - cursor moves to line2
editor.handleInput("\x1b[B"); // Down - cursor to line2
assert.strictEqual(editor.getText(), "line1\nline2\nline3"); assert.strictEqual(editor.getText(), "line1\nline2\nline3");
assert.deepStrictEqual(editor.getCursor(), { line: 1, col: 0 });
editor.handleInput("\x1b[B"); // Down - cursor to line3 editor.handleInput("\x1b[A"); // Up - cursor moves back to line1
assert.strictEqual(editor.getText(), "line1\nline2\nline3"); assert.strictEqual(editor.getText(), "line1\nline2\nline3");
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 0 });
// Now on last line, Down should exit history
editor.handleInput("\x1b[B"); // Down - exit to empty
assert.strictEqual(editor.getText(), "");
}); });
}); });