From 1224b3113507ea334c878b0e3c010e7c43e7873c Mon Sep 17 00:00:00 2001 From: Sviatoslav Abakumov Date: Wed, 28 Jan 2026 05:12:18 +0400 Subject: [PATCH] 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. --- packages/tui/src/components/editor.ts | 19 +++++ packages/tui/test/editor.test.ts | 109 ++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 32829367..30ab1632 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -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; diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index d233f421..12859797 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -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); + }); + }); });