feat(tui): auto-apply single suggestion in force file autocomplete (#993)

When Tab triggers file autocomplete and there's exactly one matching
suggestion, apply it immediately without showing the menu. This makes
path completion faster by eliminating the extra Tab press to confirm.
This commit is contained in:
Sviatoslav Abakumov 2026-01-28 05:12:18 +04:00 committed by GitHub
parent 2cc2544809
commit 1224b31135
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 128 additions and 0 deletions

View file

@ -1791,6 +1791,25 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
);
if (suggestions && suggestions.items.length > 0) {
// If there's exactly one suggestion, apply it immediately
if (suggestions.items.length === 1) {
const item = suggestions.items[0]!;
this.pushUndoSnapshot();
this.lastAction = null;
const result = this.autocompleteProvider.applyCompletion(
this.state.lines,
this.state.cursorLine,
this.state.cursorCol,
item,
suggestions.prefix,
);
this.state.lines = result.lines;
this.state.cursorLine = result.cursorLine;
this.state.cursorCol = result.cursorCol;
if (this.onChange) this.onChange(this.getText());
return;
}
this.autocompletePrefix = suggestions.prefix;
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
this.isAutocompleting = true;

View file

@ -1726,4 +1726,113 @@ describe("Editor component", () => {
assert.strictEqual(editor.getText(), "di");
});
});
describe("Autocomplete", () => {
it("auto-applies single force-file suggestion without showing menu", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
// Create a mock provider with getForceFileSuggestions that returns single item
const mockProvider: AutocompleteProvider & {
getForceFileSuggestions: AutocompleteProvider["getSuggestions"];
} = {
getSuggestions: () => null,
getForceFileSuggestions: (lines, _cursorLine, cursorCol) => {
const text = lines[0] || "";
const prefix = text.slice(0, cursorCol);
if (prefix === "Work") {
return {
items: [{ value: "Workspace/", label: "Workspace/" }],
prefix: "Work",
};
}
return null;
},
applyCompletion: (lines, cursorLine, cursorCol, item, prefix) => {
const line = lines[cursorLine] || "";
const before = line.slice(0, cursorCol - prefix.length);
const after = line.slice(cursorCol);
const newLines = [...lines];
newLines[cursorLine] = before + item.value + after;
return {
lines: newLines,
cursorLine,
cursorCol: cursorCol - prefix.length + item.value.length,
};
},
};
editor.setAutocompleteProvider(mockProvider);
// Type "Work"
editor.handleInput("W");
editor.handleInput("o");
editor.handleInput("r");
editor.handleInput("k");
assert.strictEqual(editor.getText(), "Work");
// Press Tab - should auto-apply without showing menu
editor.handleInput("\t");
assert.strictEqual(editor.getText(), "Workspace/");
assert.strictEqual(editor.isShowingAutocomplete(), false);
// Undo should restore to "Work"
editor.handleInput("\x1b[45;5u"); // Ctrl+- (undo)
assert.strictEqual(editor.getText(), "Work");
});
it("shows menu when force-file has multiple suggestions", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);
// Create a mock provider with getForceFileSuggestions that returns multiple items
const mockProvider: AutocompleteProvider & {
getForceFileSuggestions: AutocompleteProvider["getSuggestions"];
} = {
getSuggestions: () => null,
getForceFileSuggestions: (lines, _cursorLine, cursorCol) => {
const text = lines[0] || "";
const prefix = text.slice(0, cursorCol);
if (prefix === "src") {
return {
items: [
{ value: "src/", label: "src/" },
{ value: "src.txt", label: "src.txt" },
],
prefix: "src",
};
}
return null;
},
applyCompletion: (lines, cursorLine, cursorCol, item, prefix) => {
const line = lines[cursorLine] || "";
const before = line.slice(0, cursorCol - prefix.length);
const after = line.slice(cursorCol);
const newLines = [...lines];
newLines[cursorLine] = before + item.value + after;
return {
lines: newLines,
cursorLine,
cursorCol: cursorCol - prefix.length + item.value.length,
};
},
};
editor.setAutocompleteProvider(mockProvider);
// Type "src"
editor.handleInput("s");
editor.handleInput("r");
editor.handleInput("c");
assert.strictEqual(editor.getText(), "src");
// Press Tab - should show menu because there are multiple suggestions
editor.handleInput("\t");
assert.strictEqual(editor.getText(), "src"); // Text unchanged
assert.strictEqual(editor.isShowingAutocomplete(), true);
// Press Tab again to accept first suggestion
editor.handleInput("\t");
assert.strictEqual(editor.getText(), "src/");
assert.strictEqual(editor.isShowingAutocomplete(), false);
});
});
});