From f18138585968a9032f7484ee9e3dfa95c9aec0c7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 30 Jan 2026 00:26:24 +0100 Subject: [PATCH] fix(tui): avoid duplicating quotes during autocomplete --- packages/tui/src/autocomplete.ts | 13 ++++++--- packages/tui/test/autocomplete.test.ts | 40 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 23ae73ef..0aeda9d1 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -299,13 +299,18 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { const currentLine = lines[cursorLine] || ""; const beforePrefix = currentLine.slice(0, cursorCol - prefix.length); const afterCursor = currentLine.slice(cursorCol); + const isQuotedPrefix = prefix.startsWith('"') || prefix.startsWith('@"'); + const hasLeadingQuoteAfterCursor = afterCursor.startsWith('"'); + const hasTrailingQuoteInItem = item.value.endsWith('"'); + const adjustedAfterCursor = + isQuotedPrefix && hasTrailingQuoteInItem && hasLeadingQuoteAfterCursor ? afterCursor.slice(1) : afterCursor; // Check if we're completing a slash command (prefix starts with "/" but NOT a file path) // Slash commands are at the start of the line and don't contain path separators after the first / const isSlashCommand = prefix.startsWith("/") && beforePrefix.trim() === "" && !prefix.slice(1).includes("/"); if (isSlashCommand) { // This is a command name completion - const newLine = `${beforePrefix}/${item.value} ${afterCursor}`; + const newLine = `${beforePrefix}/${item.value} ${adjustedAfterCursor}`; const newLines = [...lines]; newLines[cursorLine] = newLine; @@ -322,7 +327,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { // Don't add space after directories so user can continue autocompleting const isDirectory = item.label.endsWith("/"); const suffix = isDirectory ? "" : " "; - const newLine = `${beforePrefix + item.value}${suffix}${afterCursor}`; + const newLine = `${beforePrefix + item.value}${suffix}${adjustedAfterCursor}`; const newLines = [...lines]; newLines[cursorLine] = newLine; @@ -340,7 +345,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { const textBeforeCursor = currentLine.slice(0, cursorCol); if (textBeforeCursor.includes("/") && textBeforeCursor.includes(" ")) { // This is likely a command argument completion - const newLine = beforePrefix + item.value + afterCursor; + const newLine = beforePrefix + item.value + adjustedAfterCursor; const newLines = [...lines]; newLines[cursorLine] = newLine; @@ -356,7 +361,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { } // For file paths, complete the path - const newLine = beforePrefix + item.value + afterCursor; + const newLine = beforePrefix + item.value + adjustedAfterCursor; const newLines = [...lines]; newLines[cursorLine] = newLine; diff --git a/packages/tui/test/autocomplete.test.ts b/packages/tui/test/autocomplete.test.ts index d619c846..089cc86b 100644 --- a/packages/tui/test/autocomplete.test.ts +++ b/packages/tui/test/autocomplete.test.ts @@ -264,6 +264,26 @@ describe("CombinedAutocompleteProvider", () => { assert.ok(values?.includes('@"my folder/test.txt"')); assert.ok(values?.includes('@"my folder/other.txt"')); }); + + test("applies quoted @ completion without duplicating closing quote", () => { + setupFolder(baseDir, { + files: { + "my folder/test.txt": "content", + }, + }); + + const provider = new CombinedAutocompleteProvider([], baseDir, requireFdPath()); + const line = '@"my folder/te"'; + const cursorCol = line.length - 1; + const result = provider.getSuggestions([line], 0, cursorCol); + + assert.notEqual(result, null, "Should return suggestions for quoted @ path"); + const item = result?.items.find((entry) => entry.value === '@"my folder/test.txt"'); + assert.ok(item, "Should find test.txt suggestion"); + + const applied = provider.applyCompletion([line], 0, cursorCol, item!, result!.prefix); + assert.strictEqual(applied.lines[0], '@"my folder/test.txt" '); + }); }); describe("quoted path completion", () => { @@ -311,5 +331,25 @@ describe("CombinedAutocompleteProvider", () => { assert.ok(values?.includes('"my folder/test.txt"')); assert.ok(values?.includes('"my folder/other.txt"')); }); + + test("applies quoted completion without duplicating closing quote", () => { + setupFolder(baseDir, { + files: { + "my folder/test.txt": "content", + }, + }); + + const provider = new CombinedAutocompleteProvider([], baseDir); + const line = '"my folder/te"'; + const cursorCol = line.length - 1; + const result = provider.getForceFileSuggestions([line], 0, cursorCol); + + assert.notEqual(result, null, "Should return suggestions for quoted path"); + const item = result?.items.find((entry) => entry.value === '"my folder/test.txt"'); + assert.ok(item, "Should find test.txt suggestion"); + + const applied = provider.applyCompletion([line], 0, cursorCol, item!, result!.prefix); + assert.strictEqual(applied.lines[0], '"my folder/test.txt"'); + }); }); });