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); + }); + }); });