fix(tui): re-query autocomplete picker on cursor movement

moveCursor() repositioned the cursor but never called updateAutocomplete()
or cancelAutocomplete(), unlike insertCharacter()/handleBackspace(). So an
open autocomplete picker went stale when the cursor moved: e.g. typing
`/cmd ` (argument menu open) then arrowing Left back into the command name
left the argument list showing against a `/cmd` prefix, and a Tab there
concatenated the stale suggestion onto the partial command name
(e.g. `/swarepo`).

Sync the picker at the end of moveCursor(): when a picker is open, re-query
via updateAutocomplete(), which refreshes the list for the new cursor
position or closes it when nothing matches.

Adds a regression test in test/editor.test.ts (verified to fail without the
fix). Fixes earendil-works/pi#5496.
This commit is contained in:
Roman Galeev
2026-06-08 10:57:50 +02:00
Unverified
parent 130ae577ac
commit 9d007fe642
2 changed files with 71 additions and 0 deletions
+12
View File
@@ -1742,6 +1742,18 @@ export class Editor implements Component, Focusable {
}
}
}
// Keep an open autocomplete picker in sync with the new cursor
// position: cursor movement changes the text before the cursor, so a
// picker computed for the old position is stale. Re-query so it
// refreshes — or closes when the new position yields no suggestions —
// mirroring insertCharacter()/handleBackspace(). Without this, arrowing
// left from `/cmd ` back into the command name leaves the argument
// picker showing against a `/cmd` prefix (and a Tab there would
// concatenate the stale suggestion onto the partial command name).
if (this.autocompleteState) {
this.updateAutocomplete();
}
}
/**
+59
View File
@@ -2250,6 +2250,65 @@ describe("Editor component", () => {
assert.strictEqual(editor.isShowingAutocomplete(), true);
});
it("re-queries the autocomplete picker when the cursor moves back into the command name", async () => {
// Regression for earendil-works/pi#5496: arrowing left out of a slash
// command's argument region must re-query the picker, not leave the
// stale argument list showing. Before the fix, moveCursor() never
// called updateAutocomplete(), so `/cmd ` (argument menu) + Left kept
// displaying the arguments against a `/cmd` prefix — and a Tab there
// would concatenate the stale suggestion onto the partial command name.
const editor = new Editor(createTestTUI(), defaultEditorTheme);
const mockProvider: AutocompleteProvider = {
getSuggestions: async (lines, _cursorLine, cursorCol) => {
const before = (lines[0] || "").slice(0, cursorCol);
if (!before.startsWith("/")) return null;
// Past the command name (a space before the cursor): offer arguments.
if (before.includes(" ")) {
return {
items: [
{ value: "repo", label: "repo" },
{ value: "message", label: "message" },
{ value: "help", label: "help" },
],
prefix: before.slice(before.indexOf(" ") + 1),
};
}
// Inside the command name: offer the command name only.
return { items: [{ value: "cmd", label: "cmd" }], prefix: before };
},
applyCompletion,
};
editor.setAutocompleteProvider(mockProvider);
// Type `/cmd ` so the picker ends up showing the argument list.
for (const ch of "/cmd ") {
editor.handleInput(ch);
await flushAutocomplete();
}
assert.strictEqual(editor.getText(), "/cmd ");
assert.strictEqual(editor.isShowingAutocomplete(), true);
const atArg = editor
.render(80)
.map((l) => stripVTControlCharacters(l))
.join("\n");
assert.ok(atArg.includes("repo"), "argument menu should be visible at `/cmd `");
// Arrow Left back into the command name (`/cmd`).
editor.handleInput("\x1b[D");
await flushAutocomplete();
// The picker must have re-queried: the stale argument items are gone
// (replaced by the command-name suggestion, or the picker closed).
const afterMove = editor
.render(80)
.map((l) => stripVTControlCharacters(l))
.join("\n");
assert.ok(!afterMove.includes("repo"), "stale argument menu must not survive the cursor move");
assert.ok(!afterMove.includes("message"), "stale argument menu must not survive the cursor move");
});
it("debounces # autocomplete while typing", async () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
let suggestionCalls = 0;