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:
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2250,6 +2250,65 @@ describe("Editor component", () => {
|
|||||||
assert.strictEqual(editor.isShowingAutocomplete(), true);
|
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 () => {
|
it("debounces # autocomplete while typing", async () => {
|
||||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||||
let suggestionCalls = 0;
|
let suggestionCalls = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user