@mariozechner/pi-tui 0.42.4 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,6 +74,8 @@ interface Component {
74
74
  | `handleInput?(data)` | Called when the component has focus and receives keyboard input. The `data` string contains raw terminal input (may include ANSI escape sequences). |
75
75
  | `invalidate?()` | Called to clear any cached render state. Components should re-render from scratch on the next `render()` call. |
76
76
 
77
+ The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
78
+
77
79
  ## Built-in Components
78
80
 
79
81
  ### Container
@@ -1 +1 @@
1
- {"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"AA4CA,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,sBAAsB,CAAC,CAAC,cAAc,EAAE,MAAM,GAAG,gBAAgB,EAAE,GAAG,IAAI,CAAC;CAC3E;AAED,MAAM,WAAW,oBAAoB;IAGpC,cAAc,CACb,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QACF,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC;KACf,GAAG,IAAI,CAAC;IAIT,eAAe,CACd,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,MAAM,GACZ;QACF,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;KAClB,CAAC;CACF;AAGD,qBAAa,4BAA6B,YAAW,oBAAoB;IACxE,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAgB;IAE9B,YACC,QAAQ,GAAE,CAAC,YAAY,GAAG,gBAAgB,CAAC,EAAO,EAClD,QAAQ,GAAE,MAAsB,EAChC,MAAM,GAAE,MAAM,GAAG,IAAW,EAK5B;IAED,cAAc,CACb,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CA6FtD;IAED,eAAe,CACd,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,MAAM,GACZ;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CA4D5D;IAGD,OAAO,CAAC,iBAAiB;IAwCzB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,kBAAkB;IAqJ1B,OAAO,CAAC,UAAU;IAuBlB,OAAO,CAAC,uBAAuB;IA0C/B,uBAAuB,CACtB,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAsBtD;IAGD,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAU3F;CACD","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { readdirSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join } from \"path\";\n\n// Use fd to walk directory tree (fast, respects .gitignore)\nfunction walkDirectoryWithFd(\n\tbaseDir: string,\n\tfdPath: string,\n\tquery: string,\n\tmaxResults: number,\n): Array<{ path: string; isDirectory: boolean }> {\n\tconst args = [\"--base-directory\", baseDir, \"--max-results\", String(maxResults), \"--type\", \"f\", \"--type\", \"d\"];\n\n\t// Add query as pattern if provided\n\tif (query) {\n\t\targs.push(query);\n\t}\n\n\tconst result = spawnSync(fdPath, args, {\n\t\tencoding: \"utf-8\",\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\tmaxBuffer: 10 * 1024 * 1024,\n\t});\n\n\tif (result.status !== 0 || !result.stdout) {\n\t\treturn [];\n\t}\n\n\tconst lines = result.stdout.trim().split(\"\\n\").filter(Boolean);\n\tconst results: Array<{ path: string; isDirectory: boolean }> = [];\n\n\tfor (const line of lines) {\n\t\t// fd outputs directories with trailing /\n\t\tconst isDirectory = line.endsWith(\"/\");\n\t\tresults.push({\n\t\t\tpath: line,\n\t\t\tisDirectory,\n\t\t});\n\t}\n\n\treturn results;\n}\n\nexport interface AutocompleteItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SlashCommand {\n\tname: string;\n\tdescription?: string;\n\t// Function to get argument completions for this command\n\t// Returns null if no argument completion is available\n\tgetArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;\n}\n\nexport interface AutocompleteProvider {\n\t// Get autocomplete suggestions for current text/cursor position\n\t// Returns null if no suggestions available\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): {\n\t\titems: AutocompleteItem[];\n\t\tprefix: string; // What we're matching against (e.g., \"/\" or \"src/\")\n\t} | null;\n\n\t// Apply the selected item\n\t// Returns the new text and cursor position\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): {\n\t\tlines: string[];\n\t\tcursorLine: number;\n\t\tcursorCol: number;\n\t};\n}\n\n// Combined provider that handles both slash commands and file paths\nexport class CombinedAutocompleteProvider implements AutocompleteProvider {\n\tprivate commands: (SlashCommand | AutocompleteItem)[];\n\tprivate basePath: string;\n\tprivate fdPath: string | null;\n\n\tconstructor(\n\t\tcommands: (SlashCommand | AutocompleteItem)[] = [],\n\t\tbasePath: string = process.cwd(),\n\t\tfdPath: string | null = null,\n\t) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\n\t\tthis.fdPath = fdPath;\n\t}\n\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Check for @ file reference (fuzzy search) - must be after a space or at start\n\t\tconst atMatch = textBeforeCursor.match(/(?:^|[\\s])(@[^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\tconst prefix = atMatch[1] ?? \"@\"; // The @... part\n\t\t\tconst query = prefix.slice(1); // Remove the @\n\t\t\tconst suggestions = this.getFuzzyFileSuggestions(query);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: prefix,\n\t\t\t};\n\t\t}\n\n\t\t// Check for slash commands\n\t\tif (textBeforeCursor.startsWith(\"/\")) {\n\t\t\tconst spaceIndex = textBeforeCursor.indexOf(\" \");\n\n\t\t\tif (spaceIndex === -1) {\n\t\t\t\t// No space yet - complete command names\n\t\t\t\tconst prefix = textBeforeCursor.slice(1); // Remove the \"/\"\n\t\t\t\tconst filtered = this.commands\n\t\t\t\t\t.filter((cmd) => {\n\t\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem\n\t\t\t\t\t\treturn name?.toLowerCase().startsWith(prefix.toLowerCase());\n\t\t\t\t\t})\n\t\t\t\t\t.map((cmd) => ({\n\t\t\t\t\t\tvalue: \"name\" in cmd ? cmd.name : cmd.value,\n\t\t\t\t\t\tlabel: \"name\" in cmd ? cmd.name : cmd.label,\n\t\t\t\t\t\t...(cmd.description && { description: cmd.description }),\n\t\t\t\t\t}));\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn {\n\t\t\t\t\titems: filtered,\n\t\t\t\t\tprefix: textBeforeCursor,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// Space found - complete command arguments\n\t\t\t\tconst commandName = textBeforeCursor.slice(1, spaceIndex); // Command without \"/\"\n\t\t\t\tconst argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space\n\n\t\t\t\tconst command = this.commands.find((cmd) => {\n\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value;\n\t\t\t\t\treturn name === commandName;\n\t\t\t\t});\n\t\t\t\tif (!command || !(\"getArgumentCompletions\" in command) || !command.getArgumentCompletions) {\n\t\t\t\t\treturn null; // No argument completion for this command\n\t\t\t\t}\n\n\t\t\t\tconst argumentSuggestions = command.getArgumentCompletions(argumentText);\n\t\t\t\tif (!argumentSuggestions || argumentSuggestions.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\titems: argumentSuggestions,\n\t\t\t\t\tprefix: argumentText,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Check for file paths - triggered by Tab or if we detect a path pattern\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, false);\n\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\t// Check if we have an exact match that is a directory\n\t\t\t// In that case, we might want to return suggestions for the directory content instead\n\t\t\t// But only if the prefix ends with /\n\t\t\tif (suggestions.length === 1 && suggestions[0]?.value === pathMatch && !pathMatch.endsWith(\"/\")) {\n\t\t\t\t// Exact match found (e.g. user typed \"src\" and \"src/\" is the only match)\n\t\t\t\t// We still return it so user can select it and add /\n\t\t\t\treturn {\n\t\t\t\t\titems: suggestions,\n\t\t\t\t\tprefix: pathMatch,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): { lines: string[]; cursorLine: number; cursorCol: number } {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst beforePrefix = currentLine.slice(0, cursorCol - prefix.length);\n\t\tconst afterCursor = currentLine.slice(cursorCol);\n\n\t\t// Check if we're completing a slash command (prefix starts with \"/\" but NOT a file path)\n\t\t// Slash commands are at the start of the line and don't contain path separators after the first /\n\t\tconst isSlashCommand = prefix.startsWith(\"/\") && beforePrefix.trim() === \"\" && !prefix.slice(1).includes(\"/\");\n\t\tif (isSlashCommand) {\n\t\t\t// This is a command name completion\n\t\t\tconst newLine = `${beforePrefix}/${item.value} ${afterCursor}`;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length + 2, // +2 for \"/\" and space\n\t\t\t};\n\t\t}\n\n\t\t// Check if we're completing a file attachment (prefix starts with \"@\")\n\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t// This is a file attachment completion\n\t\t\tconst newLine = `${beforePrefix + item.value} ${afterCursor}`;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length + 1, // +1 for space\n\t\t\t};\n\t\t}\n\n\t\t// Check if we're in a slash command context (beforePrefix contains \"/command \")\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\t\tif (textBeforeCursor.includes(\"/\") && textBeforeCursor.includes(\" \")) {\n\t\t\t// This is likely a command argument completion\n\t\t\tconst newLine = beforePrefix + item.value + afterCursor;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length,\n\t\t\t};\n\t\t}\n\n\t\t// For file paths, complete the path\n\t\tconst newLine = beforePrefix + item.value + afterCursor;\n\t\tconst newLines = [...lines];\n\t\tnewLines[cursorLine] = newLine;\n\n\t\treturn {\n\t\t\tlines: newLines,\n\t\t\tcursorLine,\n\t\t\tcursorCol: beforePrefix.length + item.value.length,\n\t\t};\n\t}\n\n\t// Extract a path-like prefix from the text before cursor\n\tprivate extractPathPrefix(text: string, forceExtract: boolean = false): string | null {\n\t\t// Check for @ file attachment syntax first\n\t\tconst atMatch = text.match(/@([^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\treturn atMatch[0]; // Return the full @path pattern\n\t\t}\n\n\t\t// Simple approach: find the last whitespace/delimiter and extract the word after it\n\t\t// This avoids catastrophic backtracking from nested quantifiers\n\t\tconst lastDelimiterIndex = Math.max(\n\t\t\ttext.lastIndexOf(\" \"),\n\t\t\ttext.lastIndexOf(\"\\t\"),\n\t\t\ttext.lastIndexOf('\"'),\n\t\t\ttext.lastIndexOf(\"'\"),\n\t\t\ttext.lastIndexOf(\"=\"),\n\t\t);\n\n\t\tconst pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);\n\n\t\t// For forced extraction (Tab key), always return something\n\t\tif (forceExtract) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// For natural triggers, return if it looks like a path, ends with /, starts with ~/, .\n\t\t// Only return empty string if the text looks like it's starting a path context\n\t\tif (pathPrefix.includes(\"/\") || pathPrefix.startsWith(\".\") || pathPrefix.startsWith(\"~/\")) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// Return empty string only if we're at the beginning of the line or after a space\n\t\t// (not after quotes or other delimiters that don't suggest file paths)\n\t\tif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Expand home directory (~/) to actual home path\n\tprivate expandHomePath(path: string): string {\n\t\tif (path.startsWith(\"~/\")) {\n\t\t\tconst expandedPath = join(homedir(), path.slice(2));\n\t\t\t// Preserve trailing slash if original path had one\n\t\t\treturn path.endsWith(\"/\") && !expandedPath.endsWith(\"/\") ? `${expandedPath}/` : expandedPath;\n\t\t} else if (path === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\treturn path;\n\t}\n\n\t// Get file/directory suggestions for a given path prefix\n\tprivate getFileSuggestions(prefix: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tlet searchDir: string;\n\t\t\tlet searchPrefix: string;\n\t\t\tlet expandedPrefix = prefix;\n\t\t\tlet isAtPrefix = false;\n\n\t\t\t// Handle @ file attachment prefix\n\t\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t\tisAtPrefix = true;\n\t\t\t\texpandedPrefix = prefix.slice(1); // Remove the @\n\t\t\t}\n\n\t\t\t// Handle home directory expansion\n\t\t\tif (expandedPrefix.startsWith(\"~\")) {\n\t\t\t\texpandedPrefix = this.expandHomePath(expandedPrefix);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\texpandedPrefix === \"\" ||\n\t\t\t\texpandedPrefix === \"./\" ||\n\t\t\t\texpandedPrefix === \"../\" ||\n\t\t\t\texpandedPrefix === \"~\" ||\n\t\t\t\texpandedPrefix === \"~/\" ||\n\t\t\t\texpandedPrefix === \"/\" ||\n\t\t\t\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix === \"/\") {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else {\n\t\t\t\t// Split into directory and file prefix\n\t\t\t\tconst dir = dirname(expandedPrefix);\n\t\t\t\tconst file = basename(expandedPrefix);\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = dir;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, dir);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = file;\n\t\t\t}\n\n\t\t\tconst entries = readdirSync(searchDir, { withFileTypes: true });\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Check if entry is a directory (or a symlink pointing to a directory)\n\t\t\t\tlet isDirectory = entry.isDirectory();\n\t\t\t\tif (!isDirectory && entry.isSymbolicLink()) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst fullPath = join(searchDir, entry.name);\n\t\t\t\t\t\tisDirectory = statSync(fullPath).isDirectory();\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Broken symlink or permission error - treat as file\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet relativePath: string;\n\t\t\t\tconst name = entry.name;\n\n\t\t\t\t// Handle @ prefix path construction\n\t\t\t\tif (isAtPrefix) {\n\t\t\t\t\tconst pathWithoutAt = expandedPrefix;\n\t\t\t\t\tif (pathWithoutAt.endsWith(\"/\")) {\n\t\t\t\t\t\trelativePath = `@${pathWithoutAt}${name}`;\n\t\t\t\t\t} else if (pathWithoutAt.includes(\"/\")) {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~/\")) {\n\t\t\t\t\t\t\tconst homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/\n\t\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\t\trelativePath = `@~/${dir === \".\" ? name : join(dir, name)}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `@${join(dirname(pathWithoutAt), name)}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~\")) {\n\t\t\t\t\t\t\trelativePath = `@~/${name}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `@${name}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (prefix.endsWith(\"/\")) {\n\t\t\t\t\t// If prefix ends with /, append entry to the prefix\n\t\t\t\t\trelativePath = prefix + name;\n\t\t\t\t} else if (prefix.includes(\"/\")) {\n\t\t\t\t\t// Preserve ~/ format for home directory paths\n\t\t\t\t\tif (prefix.startsWith(\"~/\")) {\n\t\t\t\t\t\tconst homeRelativeDir = prefix.slice(2); // Remove ~/\n\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\trelativePath = `~/${dir === \".\" ? name : join(dir, name)}`;\n\t\t\t\t\t} else if (prefix.startsWith(\"/\")) {\n\t\t\t\t\t\t// Absolute path - construct properly\n\t\t\t\t\t\tconst dir = dirname(prefix);\n\t\t\t\t\t\tif (dir === \"/\") {\n\t\t\t\t\t\t\trelativePath = `/${name}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `${dir}/${name}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), name);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For standalone entries, preserve ~/ if original prefix was ~/\n\t\t\t\t\tif (prefix.startsWith(\"~\")) {\n\t\t\t\t\t\trelativePath = `~/${name}`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = name;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: isDirectory ? `${relativePath}/` : relativePath,\n\t\t\t\t\tlabel: name + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Sort directories first, then alphabetically\n\t\t\tsuggestions.sort((a, b) => {\n\t\t\t\tconst aIsDir = a.value.endsWith(\"/\");\n\t\t\t\tconst bIsDir = b.value.endsWith(\"/\");\n\t\t\t\tif (aIsDir && !bIsDir) return -1;\n\t\t\t\tif (!aIsDir && bIsDir) return 1;\n\t\t\t\treturn a.label.localeCompare(b.label);\n\t\t\t});\n\n\t\t\treturn suggestions;\n\t\t} catch (_e) {\n\t\t\t// Directory doesn't exist or not accessible\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Score an entry against the query (higher = better match)\n\t// isDirectory adds bonus to prioritize folders\n\tprivate scoreEntry(filePath: string, query: string, isDirectory: boolean): number {\n\t\tconst fileName = basename(filePath);\n\t\tconst lowerFileName = fileName.toLowerCase();\n\t\tconst lowerQuery = query.toLowerCase();\n\n\t\tlet score = 0;\n\n\t\t// Exact filename match (highest)\n\t\tif (lowerFileName === lowerQuery) score = 100;\n\t\t// Filename starts with query\n\t\telse if (lowerFileName.startsWith(lowerQuery)) score = 80;\n\t\t// Substring match in filename\n\t\telse if (lowerFileName.includes(lowerQuery)) score = 50;\n\t\t// Substring match in full path\n\t\telse if (filePath.toLowerCase().includes(lowerQuery)) score = 30;\n\n\t\t// Directories get a bonus to appear first\n\t\tif (isDirectory && score > 0) score += 10;\n\n\t\treturn score;\n\t}\n\n\t// Fuzzy file search using fd (fast, respects .gitignore)\n\tprivate getFuzzyFileSuggestions(query: string): AutocompleteItem[] {\n\t\tif (!this.fdPath) {\n\t\t\t// fd not available, return empty results\n\t\t\treturn [];\n\t\t}\n\n\t\ttry {\n\t\t\tconst entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);\n\n\t\t\t// Score entries\n\t\t\tconst scoredEntries = entries\n\t\t\t\t.map((entry) => ({\n\t\t\t\t\t...entry,\n\t\t\t\t\tscore: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,\n\t\t\t\t}))\n\t\t\t\t.filter((entry) => entry.score > 0);\n\n\t\t\t// Sort by score (descending) and take top 20\n\t\t\tscoredEntries.sort((a, b) => b.score - a.score);\n\t\t\tconst topEntries = scoredEntries.slice(0, 20);\n\n\t\t\t// Build suggestions\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\t\t\tfor (const { path: entryPath, isDirectory } of topEntries) {\n\t\t\t\t// fd already includes trailing / for directories\n\t\t\t\tconst pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;\n\t\t\t\tconst entryName = basename(pathWithoutSlash);\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: `@${entryPath}`,\n\t\t\t\t\tlabel: entryName + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t\tdescription: pathWithoutSlash,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn suggestions;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Force file completion (called on Tab key) - always returns suggestions\n\tgetForceFileSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Force extract path prefix - this will always return something\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, true);\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Check if we should trigger file completion (called on Tab key)\n\tshouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
1
+ {"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"AA6CA,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,sBAAsB,CAAC,CAAC,cAAc,EAAE,MAAM,GAAG,gBAAgB,EAAE,GAAG,IAAI,CAAC;CAC3E;AAED,MAAM,WAAW,oBAAoB;IAGpC,cAAc,CACb,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QACF,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC;KACf,GAAG,IAAI,CAAC;IAIT,eAAe,CACd,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,MAAM,GACZ;QACF,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;KAClB,CAAC;CACF;AAGD,qBAAa,4BAA6B,YAAW,oBAAoB;IACxE,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAgB;IAE9B,YACC,QAAQ,GAAE,CAAC,YAAY,GAAG,gBAAgB,CAAC,EAAO,EAClD,QAAQ,GAAE,MAAsB,EAChC,MAAM,GAAE,MAAM,GAAG,IAAW,EAK5B;IAED,cAAc,CACb,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CA8FtD;IAED,eAAe,CACd,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,MAAM,GACZ;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CA4D5D;IAGD,OAAO,CAAC,iBAAiB;IAwCzB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,kBAAkB;IAqJ1B,OAAO,CAAC,UAAU;IAuBlB,OAAO,CAAC,uBAAuB;IA0C/B,uBAAuB,CACtB,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAsBtD;IAGD,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAU3F;CACD","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { readdirSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join } from \"path\";\nimport { fuzzyFilter } from \"./fuzzy.js\";\n\n// Use fd to walk directory tree (fast, respects .gitignore)\nfunction walkDirectoryWithFd(\n\tbaseDir: string,\n\tfdPath: string,\n\tquery: string,\n\tmaxResults: number,\n): Array<{ path: string; isDirectory: boolean }> {\n\tconst args = [\"--base-directory\", baseDir, \"--max-results\", String(maxResults), \"--type\", \"f\", \"--type\", \"d\"];\n\n\t// Add query as pattern if provided\n\tif (query) {\n\t\targs.push(query);\n\t}\n\n\tconst result = spawnSync(fdPath, args, {\n\t\tencoding: \"utf-8\",\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\tmaxBuffer: 10 * 1024 * 1024,\n\t});\n\n\tif (result.status !== 0 || !result.stdout) {\n\t\treturn [];\n\t}\n\n\tconst lines = result.stdout.trim().split(\"\\n\").filter(Boolean);\n\tconst results: Array<{ path: string; isDirectory: boolean }> = [];\n\n\tfor (const line of lines) {\n\t\t// fd outputs directories with trailing /\n\t\tconst isDirectory = line.endsWith(\"/\");\n\t\tresults.push({\n\t\t\tpath: line,\n\t\t\tisDirectory,\n\t\t});\n\t}\n\n\treturn results;\n}\n\nexport interface AutocompleteItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SlashCommand {\n\tname: string;\n\tdescription?: string;\n\t// Function to get argument completions for this command\n\t// Returns null if no argument completion is available\n\tgetArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;\n}\n\nexport interface AutocompleteProvider {\n\t// Get autocomplete suggestions for current text/cursor position\n\t// Returns null if no suggestions available\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): {\n\t\titems: AutocompleteItem[];\n\t\tprefix: string; // What we're matching against (e.g., \"/\" or \"src/\")\n\t} | null;\n\n\t// Apply the selected item\n\t// Returns the new text and cursor position\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): {\n\t\tlines: string[];\n\t\tcursorLine: number;\n\t\tcursorCol: number;\n\t};\n}\n\n// Combined provider that handles both slash commands and file paths\nexport class CombinedAutocompleteProvider implements AutocompleteProvider {\n\tprivate commands: (SlashCommand | AutocompleteItem)[];\n\tprivate basePath: string;\n\tprivate fdPath: string | null;\n\n\tconstructor(\n\t\tcommands: (SlashCommand | AutocompleteItem)[] = [],\n\t\tbasePath: string = process.cwd(),\n\t\tfdPath: string | null = null,\n\t) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\n\t\tthis.fdPath = fdPath;\n\t}\n\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Check for @ file reference (fuzzy search) - must be after a space or at start\n\t\tconst atMatch = textBeforeCursor.match(/(?:^|[\\s])(@[^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\tconst prefix = atMatch[1] ?? \"@\"; // The @... part\n\t\t\tconst query = prefix.slice(1); // Remove the @\n\t\t\tconst suggestions = this.getFuzzyFileSuggestions(query);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: prefix,\n\t\t\t};\n\t\t}\n\n\t\t// Check for slash commands\n\t\tif (textBeforeCursor.startsWith(\"/\")) {\n\t\t\tconst spaceIndex = textBeforeCursor.indexOf(\" \");\n\n\t\t\tif (spaceIndex === -1) {\n\t\t\t\t// No space yet - complete command names with fuzzy matching\n\t\t\t\tconst prefix = textBeforeCursor.slice(1); // Remove the \"/\"\n\t\t\t\tconst commandItems = this.commands.map((cmd) => ({\n\t\t\t\t\tname: \"name\" in cmd ? cmd.name : cmd.value,\n\t\t\t\t\tlabel: \"name\" in cmd ? cmd.name : cmd.label,\n\t\t\t\t\tdescription: cmd.description,\n\t\t\t\t}));\n\n\t\t\t\tconst filtered = fuzzyFilter(commandItems, prefix, (item) => item.name).map((item) => ({\n\t\t\t\t\tvalue: item.name,\n\t\t\t\t\tlabel: item.label,\n\t\t\t\t\t...(item.description && { description: item.description }),\n\t\t\t\t}));\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn {\n\t\t\t\t\titems: filtered,\n\t\t\t\t\tprefix: textBeforeCursor,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// Space found - complete command arguments\n\t\t\t\tconst commandName = textBeforeCursor.slice(1, spaceIndex); // Command without \"/\"\n\t\t\t\tconst argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space\n\n\t\t\t\tconst command = this.commands.find((cmd) => {\n\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value;\n\t\t\t\t\treturn name === commandName;\n\t\t\t\t});\n\t\t\t\tif (!command || !(\"getArgumentCompletions\" in command) || !command.getArgumentCompletions) {\n\t\t\t\t\treturn null; // No argument completion for this command\n\t\t\t\t}\n\n\t\t\t\tconst argumentSuggestions = command.getArgumentCompletions(argumentText);\n\t\t\t\tif (!argumentSuggestions || argumentSuggestions.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\titems: argumentSuggestions,\n\t\t\t\t\tprefix: argumentText,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Check for file paths - triggered by Tab or if we detect a path pattern\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, false);\n\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\t// Check if we have an exact match that is a directory\n\t\t\t// In that case, we might want to return suggestions for the directory content instead\n\t\t\t// But only if the prefix ends with /\n\t\t\tif (suggestions.length === 1 && suggestions[0]?.value === pathMatch && !pathMatch.endsWith(\"/\")) {\n\t\t\t\t// Exact match found (e.g. user typed \"src\" and \"src/\" is the only match)\n\t\t\t\t// We still return it so user can select it and add /\n\t\t\t\treturn {\n\t\t\t\t\titems: suggestions,\n\t\t\t\t\tprefix: pathMatch,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): { lines: string[]; cursorLine: number; cursorCol: number } {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst beforePrefix = currentLine.slice(0, cursorCol - prefix.length);\n\t\tconst afterCursor = currentLine.slice(cursorCol);\n\n\t\t// Check if we're completing a slash command (prefix starts with \"/\" but NOT a file path)\n\t\t// Slash commands are at the start of the line and don't contain path separators after the first /\n\t\tconst isSlashCommand = prefix.startsWith(\"/\") && beforePrefix.trim() === \"\" && !prefix.slice(1).includes(\"/\");\n\t\tif (isSlashCommand) {\n\t\t\t// This is a command name completion\n\t\t\tconst newLine = `${beforePrefix}/${item.value} ${afterCursor}`;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length + 2, // +2 for \"/\" and space\n\t\t\t};\n\t\t}\n\n\t\t// Check if we're completing a file attachment (prefix starts with \"@\")\n\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t// This is a file attachment completion\n\t\t\tconst newLine = `${beforePrefix + item.value} ${afterCursor}`;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length + 1, // +1 for space\n\t\t\t};\n\t\t}\n\n\t\t// Check if we're in a slash command context (beforePrefix contains \"/command \")\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\t\tif (textBeforeCursor.includes(\"/\") && textBeforeCursor.includes(\" \")) {\n\t\t\t// This is likely a command argument completion\n\t\t\tconst newLine = beforePrefix + item.value + afterCursor;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length,\n\t\t\t};\n\t\t}\n\n\t\t// For file paths, complete the path\n\t\tconst newLine = beforePrefix + item.value + afterCursor;\n\t\tconst newLines = [...lines];\n\t\tnewLines[cursorLine] = newLine;\n\n\t\treturn {\n\t\t\tlines: newLines,\n\t\t\tcursorLine,\n\t\t\tcursorCol: beforePrefix.length + item.value.length,\n\t\t};\n\t}\n\n\t// Extract a path-like prefix from the text before cursor\n\tprivate extractPathPrefix(text: string, forceExtract: boolean = false): string | null {\n\t\t// Check for @ file attachment syntax first\n\t\tconst atMatch = text.match(/@([^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\treturn atMatch[0]; // Return the full @path pattern\n\t\t}\n\n\t\t// Simple approach: find the last whitespace/delimiter and extract the word after it\n\t\t// This avoids catastrophic backtracking from nested quantifiers\n\t\tconst lastDelimiterIndex = Math.max(\n\t\t\ttext.lastIndexOf(\" \"),\n\t\t\ttext.lastIndexOf(\"\\t\"),\n\t\t\ttext.lastIndexOf('\"'),\n\t\t\ttext.lastIndexOf(\"'\"),\n\t\t\ttext.lastIndexOf(\"=\"),\n\t\t);\n\n\t\tconst pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);\n\n\t\t// For forced extraction (Tab key), always return something\n\t\tif (forceExtract) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// For natural triggers, return if it looks like a path, ends with /, starts with ~/, .\n\t\t// Only return empty string if the text looks like it's starting a path context\n\t\tif (pathPrefix.includes(\"/\") || pathPrefix.startsWith(\".\") || pathPrefix.startsWith(\"~/\")) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// Return empty string only if we're at the beginning of the line or after a space\n\t\t// (not after quotes or other delimiters that don't suggest file paths)\n\t\tif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Expand home directory (~/) to actual home path\n\tprivate expandHomePath(path: string): string {\n\t\tif (path.startsWith(\"~/\")) {\n\t\t\tconst expandedPath = join(homedir(), path.slice(2));\n\t\t\t// Preserve trailing slash if original path had one\n\t\t\treturn path.endsWith(\"/\") && !expandedPath.endsWith(\"/\") ? `${expandedPath}/` : expandedPath;\n\t\t} else if (path === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\treturn path;\n\t}\n\n\t// Get file/directory suggestions for a given path prefix\n\tprivate getFileSuggestions(prefix: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tlet searchDir: string;\n\t\t\tlet searchPrefix: string;\n\t\t\tlet expandedPrefix = prefix;\n\t\t\tlet isAtPrefix = false;\n\n\t\t\t// Handle @ file attachment prefix\n\t\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t\tisAtPrefix = true;\n\t\t\t\texpandedPrefix = prefix.slice(1); // Remove the @\n\t\t\t}\n\n\t\t\t// Handle home directory expansion\n\t\t\tif (expandedPrefix.startsWith(\"~\")) {\n\t\t\t\texpandedPrefix = this.expandHomePath(expandedPrefix);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\texpandedPrefix === \"\" ||\n\t\t\t\texpandedPrefix === \"./\" ||\n\t\t\t\texpandedPrefix === \"../\" ||\n\t\t\t\texpandedPrefix === \"~\" ||\n\t\t\t\texpandedPrefix === \"~/\" ||\n\t\t\t\texpandedPrefix === \"/\" ||\n\t\t\t\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix === \"/\") {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else {\n\t\t\t\t// Split into directory and file prefix\n\t\t\t\tconst dir = dirname(expandedPrefix);\n\t\t\t\tconst file = basename(expandedPrefix);\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = dir;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, dir);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = file;\n\t\t\t}\n\n\t\t\tconst entries = readdirSync(searchDir, { withFileTypes: true });\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Check if entry is a directory (or a symlink pointing to a directory)\n\t\t\t\tlet isDirectory = entry.isDirectory();\n\t\t\t\tif (!isDirectory && entry.isSymbolicLink()) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst fullPath = join(searchDir, entry.name);\n\t\t\t\t\t\tisDirectory = statSync(fullPath).isDirectory();\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Broken symlink or permission error - treat as file\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet relativePath: string;\n\t\t\t\tconst name = entry.name;\n\n\t\t\t\t// Handle @ prefix path construction\n\t\t\t\tif (isAtPrefix) {\n\t\t\t\t\tconst pathWithoutAt = expandedPrefix;\n\t\t\t\t\tif (pathWithoutAt.endsWith(\"/\")) {\n\t\t\t\t\t\trelativePath = `@${pathWithoutAt}${name}`;\n\t\t\t\t\t} else if (pathWithoutAt.includes(\"/\")) {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~/\")) {\n\t\t\t\t\t\t\tconst homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/\n\t\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\t\trelativePath = `@~/${dir === \".\" ? name : join(dir, name)}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `@${join(dirname(pathWithoutAt), name)}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~\")) {\n\t\t\t\t\t\t\trelativePath = `@~/${name}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `@${name}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (prefix.endsWith(\"/\")) {\n\t\t\t\t\t// If prefix ends with /, append entry to the prefix\n\t\t\t\t\trelativePath = prefix + name;\n\t\t\t\t} else if (prefix.includes(\"/\")) {\n\t\t\t\t\t// Preserve ~/ format for home directory paths\n\t\t\t\t\tif (prefix.startsWith(\"~/\")) {\n\t\t\t\t\t\tconst homeRelativeDir = prefix.slice(2); // Remove ~/\n\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\trelativePath = `~/${dir === \".\" ? name : join(dir, name)}`;\n\t\t\t\t\t} else if (prefix.startsWith(\"/\")) {\n\t\t\t\t\t\t// Absolute path - construct properly\n\t\t\t\t\t\tconst dir = dirname(prefix);\n\t\t\t\t\t\tif (dir === \"/\") {\n\t\t\t\t\t\t\trelativePath = `/${name}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `${dir}/${name}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), name);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For standalone entries, preserve ~/ if original prefix was ~/\n\t\t\t\t\tif (prefix.startsWith(\"~\")) {\n\t\t\t\t\t\trelativePath = `~/${name}`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = name;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: isDirectory ? `${relativePath}/` : relativePath,\n\t\t\t\t\tlabel: name + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Sort directories first, then alphabetically\n\t\t\tsuggestions.sort((a, b) => {\n\t\t\t\tconst aIsDir = a.value.endsWith(\"/\");\n\t\t\t\tconst bIsDir = b.value.endsWith(\"/\");\n\t\t\t\tif (aIsDir && !bIsDir) return -1;\n\t\t\t\tif (!aIsDir && bIsDir) return 1;\n\t\t\t\treturn a.label.localeCompare(b.label);\n\t\t\t});\n\n\t\t\treturn suggestions;\n\t\t} catch (_e) {\n\t\t\t// Directory doesn't exist or not accessible\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Score an entry against the query (higher = better match)\n\t// isDirectory adds bonus to prioritize folders\n\tprivate scoreEntry(filePath: string, query: string, isDirectory: boolean): number {\n\t\tconst fileName = basename(filePath);\n\t\tconst lowerFileName = fileName.toLowerCase();\n\t\tconst lowerQuery = query.toLowerCase();\n\n\t\tlet score = 0;\n\n\t\t// Exact filename match (highest)\n\t\tif (lowerFileName === lowerQuery) score = 100;\n\t\t// Filename starts with query\n\t\telse if (lowerFileName.startsWith(lowerQuery)) score = 80;\n\t\t// Substring match in filename\n\t\telse if (lowerFileName.includes(lowerQuery)) score = 50;\n\t\t// Substring match in full path\n\t\telse if (filePath.toLowerCase().includes(lowerQuery)) score = 30;\n\n\t\t// Directories get a bonus to appear first\n\t\tif (isDirectory && score > 0) score += 10;\n\n\t\treturn score;\n\t}\n\n\t// Fuzzy file search using fd (fast, respects .gitignore)\n\tprivate getFuzzyFileSuggestions(query: string): AutocompleteItem[] {\n\t\tif (!this.fdPath) {\n\t\t\t// fd not available, return empty results\n\t\t\treturn [];\n\t\t}\n\n\t\ttry {\n\t\t\tconst entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);\n\n\t\t\t// Score entries\n\t\t\tconst scoredEntries = entries\n\t\t\t\t.map((entry) => ({\n\t\t\t\t\t...entry,\n\t\t\t\t\tscore: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,\n\t\t\t\t}))\n\t\t\t\t.filter((entry) => entry.score > 0);\n\n\t\t\t// Sort by score (descending) and take top 20\n\t\t\tscoredEntries.sort((a, b) => b.score - a.score);\n\t\t\tconst topEntries = scoredEntries.slice(0, 20);\n\n\t\t\t// Build suggestions\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\t\t\tfor (const { path: entryPath, isDirectory } of topEntries) {\n\t\t\t\t// fd already includes trailing / for directories\n\t\t\t\tconst pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;\n\t\t\t\tconst entryName = basename(pathWithoutSlash);\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: `@${entryPath}`,\n\t\t\t\t\tlabel: entryName + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t\tdescription: pathWithoutSlash,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn suggestions;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Force file completion (called on Tab key) - always returns suggestions\n\tgetForceFileSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Force extract path prefix - this will always return something\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, true);\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Check if we should trigger file completion (called on Tab key)\n\tshouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
@@ -2,6 +2,7 @@ import { spawnSync } from "child_process";
2
2
  import { readdirSync, statSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { basename, dirname, join } from "path";
5
+ import { fuzzyFilter } from "./fuzzy.js";
5
6
  // Use fd to walk directory tree (fast, respects .gitignore)
6
7
  function walkDirectoryWithFd(baseDir, fdPath, query, maxResults) {
7
8
  const args = ["--base-directory", baseDir, "--max-results", String(maxResults), "--type", "f", "--type", "d"];
@@ -59,17 +60,17 @@ export class CombinedAutocompleteProvider {
59
60
  if (textBeforeCursor.startsWith("/")) {
60
61
  const spaceIndex = textBeforeCursor.indexOf(" ");
61
62
  if (spaceIndex === -1) {
62
- // No space yet - complete command names
63
+ // No space yet - complete command names with fuzzy matching
63
64
  const prefix = textBeforeCursor.slice(1); // Remove the "/"
64
- const filtered = this.commands
65
- .filter((cmd) => {
66
- const name = "name" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem
67
- return name?.toLowerCase().startsWith(prefix.toLowerCase());
68
- })
69
- .map((cmd) => ({
70
- value: "name" in cmd ? cmd.name : cmd.value,
65
+ const commandItems = this.commands.map((cmd) => ({
66
+ name: "name" in cmd ? cmd.name : cmd.value,
71
67
  label: "name" in cmd ? cmd.name : cmd.label,
72
- ...(cmd.description && { description: cmd.description }),
68
+ description: cmd.description,
69
+ }));
70
+ const filtered = fuzzyFilter(commandItems, prefix, (item) => item.name).map((item) => ({
71
+ value: item.name,
72
+ label: item.label,
73
+ ...(item.description && { description: item.description }),
73
74
  }));
74
75
  if (filtered.length === 0)
75
76
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"autocomplete.js","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { readdirSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join } from \"path\";\n\n// Use fd to walk directory tree (fast, respects .gitignore)\nfunction walkDirectoryWithFd(\n\tbaseDir: string,\n\tfdPath: string,\n\tquery: string,\n\tmaxResults: number,\n): Array<{ path: string; isDirectory: boolean }> {\n\tconst args = [\"--base-directory\", baseDir, \"--max-results\", String(maxResults), \"--type\", \"f\", \"--type\", \"d\"];\n\n\t// Add query as pattern if provided\n\tif (query) {\n\t\targs.push(query);\n\t}\n\n\tconst result = spawnSync(fdPath, args, {\n\t\tencoding: \"utf-8\",\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\tmaxBuffer: 10 * 1024 * 1024,\n\t});\n\n\tif (result.status !== 0 || !result.stdout) {\n\t\treturn [];\n\t}\n\n\tconst lines = result.stdout.trim().split(\"\\n\").filter(Boolean);\n\tconst results: Array<{ path: string; isDirectory: boolean }> = [];\n\n\tfor (const line of lines) {\n\t\t// fd outputs directories with trailing /\n\t\tconst isDirectory = line.endsWith(\"/\");\n\t\tresults.push({\n\t\t\tpath: line,\n\t\t\tisDirectory,\n\t\t});\n\t}\n\n\treturn results;\n}\n\nexport interface AutocompleteItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SlashCommand {\n\tname: string;\n\tdescription?: string;\n\t// Function to get argument completions for this command\n\t// Returns null if no argument completion is available\n\tgetArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;\n}\n\nexport interface AutocompleteProvider {\n\t// Get autocomplete suggestions for current text/cursor position\n\t// Returns null if no suggestions available\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): {\n\t\titems: AutocompleteItem[];\n\t\tprefix: string; // What we're matching against (e.g., \"/\" or \"src/\")\n\t} | null;\n\n\t// Apply the selected item\n\t// Returns the new text and cursor position\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): {\n\t\tlines: string[];\n\t\tcursorLine: number;\n\t\tcursorCol: number;\n\t};\n}\n\n// Combined provider that handles both slash commands and file paths\nexport class CombinedAutocompleteProvider implements AutocompleteProvider {\n\tprivate commands: (SlashCommand | AutocompleteItem)[];\n\tprivate basePath: string;\n\tprivate fdPath: string | null;\n\n\tconstructor(\n\t\tcommands: (SlashCommand | AutocompleteItem)[] = [],\n\t\tbasePath: string = process.cwd(),\n\t\tfdPath: string | null = null,\n\t) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\n\t\tthis.fdPath = fdPath;\n\t}\n\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Check for @ file reference (fuzzy search) - must be after a space or at start\n\t\tconst atMatch = textBeforeCursor.match(/(?:^|[\\s])(@[^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\tconst prefix = atMatch[1] ?? \"@\"; // The @... part\n\t\t\tconst query = prefix.slice(1); // Remove the @\n\t\t\tconst suggestions = this.getFuzzyFileSuggestions(query);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: prefix,\n\t\t\t};\n\t\t}\n\n\t\t// Check for slash commands\n\t\tif (textBeforeCursor.startsWith(\"/\")) {\n\t\t\tconst spaceIndex = textBeforeCursor.indexOf(\" \");\n\n\t\t\tif (spaceIndex === -1) {\n\t\t\t\t// No space yet - complete command names\n\t\t\t\tconst prefix = textBeforeCursor.slice(1); // Remove the \"/\"\n\t\t\t\tconst filtered = this.commands\n\t\t\t\t\t.filter((cmd) => {\n\t\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem\n\t\t\t\t\t\treturn name?.toLowerCase().startsWith(prefix.toLowerCase());\n\t\t\t\t\t})\n\t\t\t\t\t.map((cmd) => ({\n\t\t\t\t\t\tvalue: \"name\" in cmd ? cmd.name : cmd.value,\n\t\t\t\t\t\tlabel: \"name\" in cmd ? cmd.name : cmd.label,\n\t\t\t\t\t\t...(cmd.description && { description: cmd.description }),\n\t\t\t\t\t}));\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn {\n\t\t\t\t\titems: filtered,\n\t\t\t\t\tprefix: textBeforeCursor,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// Space found - complete command arguments\n\t\t\t\tconst commandName = textBeforeCursor.slice(1, spaceIndex); // Command without \"/\"\n\t\t\t\tconst argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space\n\n\t\t\t\tconst command = this.commands.find((cmd) => {\n\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value;\n\t\t\t\t\treturn name === commandName;\n\t\t\t\t});\n\t\t\t\tif (!command || !(\"getArgumentCompletions\" in command) || !command.getArgumentCompletions) {\n\t\t\t\t\treturn null; // No argument completion for this command\n\t\t\t\t}\n\n\t\t\t\tconst argumentSuggestions = command.getArgumentCompletions(argumentText);\n\t\t\t\tif (!argumentSuggestions || argumentSuggestions.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\titems: argumentSuggestions,\n\t\t\t\t\tprefix: argumentText,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Check for file paths - triggered by Tab or if we detect a path pattern\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, false);\n\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\t// Check if we have an exact match that is a directory\n\t\t\t// In that case, we might want to return suggestions for the directory content instead\n\t\t\t// But only if the prefix ends with /\n\t\t\tif (suggestions.length === 1 && suggestions[0]?.value === pathMatch && !pathMatch.endsWith(\"/\")) {\n\t\t\t\t// Exact match found (e.g. user typed \"src\" and \"src/\" is the only match)\n\t\t\t\t// We still return it so user can select it and add /\n\t\t\t\treturn {\n\t\t\t\t\titems: suggestions,\n\t\t\t\t\tprefix: pathMatch,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): { lines: string[]; cursorLine: number; cursorCol: number } {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst beforePrefix = currentLine.slice(0, cursorCol - prefix.length);\n\t\tconst afterCursor = currentLine.slice(cursorCol);\n\n\t\t// Check if we're completing a slash command (prefix starts with \"/\" but NOT a file path)\n\t\t// Slash commands are at the start of the line and don't contain path separators after the first /\n\t\tconst isSlashCommand = prefix.startsWith(\"/\") && beforePrefix.trim() === \"\" && !prefix.slice(1).includes(\"/\");\n\t\tif (isSlashCommand) {\n\t\t\t// This is a command name completion\n\t\t\tconst newLine = `${beforePrefix}/${item.value} ${afterCursor}`;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length + 2, // +2 for \"/\" and space\n\t\t\t};\n\t\t}\n\n\t\t// Check if we're completing a file attachment (prefix starts with \"@\")\n\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t// This is a file attachment completion\n\t\t\tconst newLine = `${beforePrefix + item.value} ${afterCursor}`;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length + 1, // +1 for space\n\t\t\t};\n\t\t}\n\n\t\t// Check if we're in a slash command context (beforePrefix contains \"/command \")\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\t\tif (textBeforeCursor.includes(\"/\") && textBeforeCursor.includes(\" \")) {\n\t\t\t// This is likely a command argument completion\n\t\t\tconst newLine = beforePrefix + item.value + afterCursor;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length,\n\t\t\t};\n\t\t}\n\n\t\t// For file paths, complete the path\n\t\tconst newLine = beforePrefix + item.value + afterCursor;\n\t\tconst newLines = [...lines];\n\t\tnewLines[cursorLine] = newLine;\n\n\t\treturn {\n\t\t\tlines: newLines,\n\t\t\tcursorLine,\n\t\t\tcursorCol: beforePrefix.length + item.value.length,\n\t\t};\n\t}\n\n\t// Extract a path-like prefix from the text before cursor\n\tprivate extractPathPrefix(text: string, forceExtract: boolean = false): string | null {\n\t\t// Check for @ file attachment syntax first\n\t\tconst atMatch = text.match(/@([^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\treturn atMatch[0]; // Return the full @path pattern\n\t\t}\n\n\t\t// Simple approach: find the last whitespace/delimiter and extract the word after it\n\t\t// This avoids catastrophic backtracking from nested quantifiers\n\t\tconst lastDelimiterIndex = Math.max(\n\t\t\ttext.lastIndexOf(\" \"),\n\t\t\ttext.lastIndexOf(\"\\t\"),\n\t\t\ttext.lastIndexOf('\"'),\n\t\t\ttext.lastIndexOf(\"'\"),\n\t\t\ttext.lastIndexOf(\"=\"),\n\t\t);\n\n\t\tconst pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);\n\n\t\t// For forced extraction (Tab key), always return something\n\t\tif (forceExtract) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// For natural triggers, return if it looks like a path, ends with /, starts with ~/, .\n\t\t// Only return empty string if the text looks like it's starting a path context\n\t\tif (pathPrefix.includes(\"/\") || pathPrefix.startsWith(\".\") || pathPrefix.startsWith(\"~/\")) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// Return empty string only if we're at the beginning of the line or after a space\n\t\t// (not after quotes or other delimiters that don't suggest file paths)\n\t\tif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Expand home directory (~/) to actual home path\n\tprivate expandHomePath(path: string): string {\n\t\tif (path.startsWith(\"~/\")) {\n\t\t\tconst expandedPath = join(homedir(), path.slice(2));\n\t\t\t// Preserve trailing slash if original path had one\n\t\t\treturn path.endsWith(\"/\") && !expandedPath.endsWith(\"/\") ? `${expandedPath}/` : expandedPath;\n\t\t} else if (path === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\treturn path;\n\t}\n\n\t// Get file/directory suggestions for a given path prefix\n\tprivate getFileSuggestions(prefix: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tlet searchDir: string;\n\t\t\tlet searchPrefix: string;\n\t\t\tlet expandedPrefix = prefix;\n\t\t\tlet isAtPrefix = false;\n\n\t\t\t// Handle @ file attachment prefix\n\t\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t\tisAtPrefix = true;\n\t\t\t\texpandedPrefix = prefix.slice(1); // Remove the @\n\t\t\t}\n\n\t\t\t// Handle home directory expansion\n\t\t\tif (expandedPrefix.startsWith(\"~\")) {\n\t\t\t\texpandedPrefix = this.expandHomePath(expandedPrefix);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\texpandedPrefix === \"\" ||\n\t\t\t\texpandedPrefix === \"./\" ||\n\t\t\t\texpandedPrefix === \"../\" ||\n\t\t\t\texpandedPrefix === \"~\" ||\n\t\t\t\texpandedPrefix === \"~/\" ||\n\t\t\t\texpandedPrefix === \"/\" ||\n\t\t\t\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix === \"/\") {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else {\n\t\t\t\t// Split into directory and file prefix\n\t\t\t\tconst dir = dirname(expandedPrefix);\n\t\t\t\tconst file = basename(expandedPrefix);\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = dir;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, dir);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = file;\n\t\t\t}\n\n\t\t\tconst entries = readdirSync(searchDir, { withFileTypes: true });\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Check if entry is a directory (or a symlink pointing to a directory)\n\t\t\t\tlet isDirectory = entry.isDirectory();\n\t\t\t\tif (!isDirectory && entry.isSymbolicLink()) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst fullPath = join(searchDir, entry.name);\n\t\t\t\t\t\tisDirectory = statSync(fullPath).isDirectory();\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Broken symlink or permission error - treat as file\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet relativePath: string;\n\t\t\t\tconst name = entry.name;\n\n\t\t\t\t// Handle @ prefix path construction\n\t\t\t\tif (isAtPrefix) {\n\t\t\t\t\tconst pathWithoutAt = expandedPrefix;\n\t\t\t\t\tif (pathWithoutAt.endsWith(\"/\")) {\n\t\t\t\t\t\trelativePath = `@${pathWithoutAt}${name}`;\n\t\t\t\t\t} else if (pathWithoutAt.includes(\"/\")) {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~/\")) {\n\t\t\t\t\t\t\tconst homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/\n\t\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\t\trelativePath = `@~/${dir === \".\" ? name : join(dir, name)}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `@${join(dirname(pathWithoutAt), name)}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~\")) {\n\t\t\t\t\t\t\trelativePath = `@~/${name}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `@${name}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (prefix.endsWith(\"/\")) {\n\t\t\t\t\t// If prefix ends with /, append entry to the prefix\n\t\t\t\t\trelativePath = prefix + name;\n\t\t\t\t} else if (prefix.includes(\"/\")) {\n\t\t\t\t\t// Preserve ~/ format for home directory paths\n\t\t\t\t\tif (prefix.startsWith(\"~/\")) {\n\t\t\t\t\t\tconst homeRelativeDir = prefix.slice(2); // Remove ~/\n\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\trelativePath = `~/${dir === \".\" ? name : join(dir, name)}`;\n\t\t\t\t\t} else if (prefix.startsWith(\"/\")) {\n\t\t\t\t\t\t// Absolute path - construct properly\n\t\t\t\t\t\tconst dir = dirname(prefix);\n\t\t\t\t\t\tif (dir === \"/\") {\n\t\t\t\t\t\t\trelativePath = `/${name}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `${dir}/${name}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), name);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For standalone entries, preserve ~/ if original prefix was ~/\n\t\t\t\t\tif (prefix.startsWith(\"~\")) {\n\t\t\t\t\t\trelativePath = `~/${name}`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = name;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: isDirectory ? `${relativePath}/` : relativePath,\n\t\t\t\t\tlabel: name + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Sort directories first, then alphabetically\n\t\t\tsuggestions.sort((a, b) => {\n\t\t\t\tconst aIsDir = a.value.endsWith(\"/\");\n\t\t\t\tconst bIsDir = b.value.endsWith(\"/\");\n\t\t\t\tif (aIsDir && !bIsDir) return -1;\n\t\t\t\tif (!aIsDir && bIsDir) return 1;\n\t\t\t\treturn a.label.localeCompare(b.label);\n\t\t\t});\n\n\t\t\treturn suggestions;\n\t\t} catch (_e) {\n\t\t\t// Directory doesn't exist or not accessible\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Score an entry against the query (higher = better match)\n\t// isDirectory adds bonus to prioritize folders\n\tprivate scoreEntry(filePath: string, query: string, isDirectory: boolean): number {\n\t\tconst fileName = basename(filePath);\n\t\tconst lowerFileName = fileName.toLowerCase();\n\t\tconst lowerQuery = query.toLowerCase();\n\n\t\tlet score = 0;\n\n\t\t// Exact filename match (highest)\n\t\tif (lowerFileName === lowerQuery) score = 100;\n\t\t// Filename starts with query\n\t\telse if (lowerFileName.startsWith(lowerQuery)) score = 80;\n\t\t// Substring match in filename\n\t\telse if (lowerFileName.includes(lowerQuery)) score = 50;\n\t\t// Substring match in full path\n\t\telse if (filePath.toLowerCase().includes(lowerQuery)) score = 30;\n\n\t\t// Directories get a bonus to appear first\n\t\tif (isDirectory && score > 0) score += 10;\n\n\t\treturn score;\n\t}\n\n\t// Fuzzy file search using fd (fast, respects .gitignore)\n\tprivate getFuzzyFileSuggestions(query: string): AutocompleteItem[] {\n\t\tif (!this.fdPath) {\n\t\t\t// fd not available, return empty results\n\t\t\treturn [];\n\t\t}\n\n\t\ttry {\n\t\t\tconst entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);\n\n\t\t\t// Score entries\n\t\t\tconst scoredEntries = entries\n\t\t\t\t.map((entry) => ({\n\t\t\t\t\t...entry,\n\t\t\t\t\tscore: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,\n\t\t\t\t}))\n\t\t\t\t.filter((entry) => entry.score > 0);\n\n\t\t\t// Sort by score (descending) and take top 20\n\t\t\tscoredEntries.sort((a, b) => b.score - a.score);\n\t\t\tconst topEntries = scoredEntries.slice(0, 20);\n\n\t\t\t// Build suggestions\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\t\t\tfor (const { path: entryPath, isDirectory } of topEntries) {\n\t\t\t\t// fd already includes trailing / for directories\n\t\t\t\tconst pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;\n\t\t\t\tconst entryName = basename(pathWithoutSlash);\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: `@${entryPath}`,\n\t\t\t\t\tlabel: entryName + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t\tdescription: pathWithoutSlash,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn suggestions;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Force file completion (called on Tab key) - always returns suggestions\n\tgetForceFileSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Force extract path prefix - this will always return something\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, true);\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Check if we should trigger file completion (called on Tab key)\n\tshouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
1
+ {"version":3,"file":"autocomplete.js","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { readdirSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join } from \"path\";\nimport { fuzzyFilter } from \"./fuzzy.js\";\n\n// Use fd to walk directory tree (fast, respects .gitignore)\nfunction walkDirectoryWithFd(\n\tbaseDir: string,\n\tfdPath: string,\n\tquery: string,\n\tmaxResults: number,\n): Array<{ path: string; isDirectory: boolean }> {\n\tconst args = [\"--base-directory\", baseDir, \"--max-results\", String(maxResults), \"--type\", \"f\", \"--type\", \"d\"];\n\n\t// Add query as pattern if provided\n\tif (query) {\n\t\targs.push(query);\n\t}\n\n\tconst result = spawnSync(fdPath, args, {\n\t\tencoding: \"utf-8\",\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\tmaxBuffer: 10 * 1024 * 1024,\n\t});\n\n\tif (result.status !== 0 || !result.stdout) {\n\t\treturn [];\n\t}\n\n\tconst lines = result.stdout.trim().split(\"\\n\").filter(Boolean);\n\tconst results: Array<{ path: string; isDirectory: boolean }> = [];\n\n\tfor (const line of lines) {\n\t\t// fd outputs directories with trailing /\n\t\tconst isDirectory = line.endsWith(\"/\");\n\t\tresults.push({\n\t\t\tpath: line,\n\t\t\tisDirectory,\n\t\t});\n\t}\n\n\treturn results;\n}\n\nexport interface AutocompleteItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SlashCommand {\n\tname: string;\n\tdescription?: string;\n\t// Function to get argument completions for this command\n\t// Returns null if no argument completion is available\n\tgetArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;\n}\n\nexport interface AutocompleteProvider {\n\t// Get autocomplete suggestions for current text/cursor position\n\t// Returns null if no suggestions available\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): {\n\t\titems: AutocompleteItem[];\n\t\tprefix: string; // What we're matching against (e.g., \"/\" or \"src/\")\n\t} | null;\n\n\t// Apply the selected item\n\t// Returns the new text and cursor position\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): {\n\t\tlines: string[];\n\t\tcursorLine: number;\n\t\tcursorCol: number;\n\t};\n}\n\n// Combined provider that handles both slash commands and file paths\nexport class CombinedAutocompleteProvider implements AutocompleteProvider {\n\tprivate commands: (SlashCommand | AutocompleteItem)[];\n\tprivate basePath: string;\n\tprivate fdPath: string | null;\n\n\tconstructor(\n\t\tcommands: (SlashCommand | AutocompleteItem)[] = [],\n\t\tbasePath: string = process.cwd(),\n\t\tfdPath: string | null = null,\n\t) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\n\t\tthis.fdPath = fdPath;\n\t}\n\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Check for @ file reference (fuzzy search) - must be after a space or at start\n\t\tconst atMatch = textBeforeCursor.match(/(?:^|[\\s])(@[^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\tconst prefix = atMatch[1] ?? \"@\"; // The @... part\n\t\t\tconst query = prefix.slice(1); // Remove the @\n\t\t\tconst suggestions = this.getFuzzyFileSuggestions(query);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: prefix,\n\t\t\t};\n\t\t}\n\n\t\t// Check for slash commands\n\t\tif (textBeforeCursor.startsWith(\"/\")) {\n\t\t\tconst spaceIndex = textBeforeCursor.indexOf(\" \");\n\n\t\t\tif (spaceIndex === -1) {\n\t\t\t\t// No space yet - complete command names with fuzzy matching\n\t\t\t\tconst prefix = textBeforeCursor.slice(1); // Remove the \"/\"\n\t\t\t\tconst commandItems = this.commands.map((cmd) => ({\n\t\t\t\t\tname: \"name\" in cmd ? cmd.name : cmd.value,\n\t\t\t\t\tlabel: \"name\" in cmd ? cmd.name : cmd.label,\n\t\t\t\t\tdescription: cmd.description,\n\t\t\t\t}));\n\n\t\t\t\tconst filtered = fuzzyFilter(commandItems, prefix, (item) => item.name).map((item) => ({\n\t\t\t\t\tvalue: item.name,\n\t\t\t\t\tlabel: item.label,\n\t\t\t\t\t...(item.description && { description: item.description }),\n\t\t\t\t}));\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn {\n\t\t\t\t\titems: filtered,\n\t\t\t\t\tprefix: textBeforeCursor,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// Space found - complete command arguments\n\t\t\t\tconst commandName = textBeforeCursor.slice(1, spaceIndex); // Command without \"/\"\n\t\t\t\tconst argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space\n\n\t\t\t\tconst command = this.commands.find((cmd) => {\n\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value;\n\t\t\t\t\treturn name === commandName;\n\t\t\t\t});\n\t\t\t\tif (!command || !(\"getArgumentCompletions\" in command) || !command.getArgumentCompletions) {\n\t\t\t\t\treturn null; // No argument completion for this command\n\t\t\t\t}\n\n\t\t\t\tconst argumentSuggestions = command.getArgumentCompletions(argumentText);\n\t\t\t\tif (!argumentSuggestions || argumentSuggestions.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\titems: argumentSuggestions,\n\t\t\t\t\tprefix: argumentText,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Check for file paths - triggered by Tab or if we detect a path pattern\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, false);\n\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\t// Check if we have an exact match that is a directory\n\t\t\t// In that case, we might want to return suggestions for the directory content instead\n\t\t\t// But only if the prefix ends with /\n\t\t\tif (suggestions.length === 1 && suggestions[0]?.value === pathMatch && !pathMatch.endsWith(\"/\")) {\n\t\t\t\t// Exact match found (e.g. user typed \"src\" and \"src/\" is the only match)\n\t\t\t\t// We still return it so user can select it and add /\n\t\t\t\treturn {\n\t\t\t\t\titems: suggestions,\n\t\t\t\t\tprefix: pathMatch,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): { lines: string[]; cursorLine: number; cursorCol: number } {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst beforePrefix = currentLine.slice(0, cursorCol - prefix.length);\n\t\tconst afterCursor = currentLine.slice(cursorCol);\n\n\t\t// Check if we're completing a slash command (prefix starts with \"/\" but NOT a file path)\n\t\t// Slash commands are at the start of the line and don't contain path separators after the first /\n\t\tconst isSlashCommand = prefix.startsWith(\"/\") && beforePrefix.trim() === \"\" && !prefix.slice(1).includes(\"/\");\n\t\tif (isSlashCommand) {\n\t\t\t// This is a command name completion\n\t\t\tconst newLine = `${beforePrefix}/${item.value} ${afterCursor}`;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length + 2, // +2 for \"/\" and space\n\t\t\t};\n\t\t}\n\n\t\t// Check if we're completing a file attachment (prefix starts with \"@\")\n\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t// This is a file attachment completion\n\t\t\tconst newLine = `${beforePrefix + item.value} ${afterCursor}`;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length + 1, // +1 for space\n\t\t\t};\n\t\t}\n\n\t\t// Check if we're in a slash command context (beforePrefix contains \"/command \")\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\t\tif (textBeforeCursor.includes(\"/\") && textBeforeCursor.includes(\" \")) {\n\t\t\t// This is likely a command argument completion\n\t\t\tconst newLine = beforePrefix + item.value + afterCursor;\n\t\t\tconst newLines = [...lines];\n\t\t\tnewLines[cursorLine] = newLine;\n\n\t\t\treturn {\n\t\t\t\tlines: newLines,\n\t\t\t\tcursorLine,\n\t\t\t\tcursorCol: beforePrefix.length + item.value.length,\n\t\t\t};\n\t\t}\n\n\t\t// For file paths, complete the path\n\t\tconst newLine = beforePrefix + item.value + afterCursor;\n\t\tconst newLines = [...lines];\n\t\tnewLines[cursorLine] = newLine;\n\n\t\treturn {\n\t\t\tlines: newLines,\n\t\t\tcursorLine,\n\t\t\tcursorCol: beforePrefix.length + item.value.length,\n\t\t};\n\t}\n\n\t// Extract a path-like prefix from the text before cursor\n\tprivate extractPathPrefix(text: string, forceExtract: boolean = false): string | null {\n\t\t// Check for @ file attachment syntax first\n\t\tconst atMatch = text.match(/@([^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\treturn atMatch[0]; // Return the full @path pattern\n\t\t}\n\n\t\t// Simple approach: find the last whitespace/delimiter and extract the word after it\n\t\t// This avoids catastrophic backtracking from nested quantifiers\n\t\tconst lastDelimiterIndex = Math.max(\n\t\t\ttext.lastIndexOf(\" \"),\n\t\t\ttext.lastIndexOf(\"\\t\"),\n\t\t\ttext.lastIndexOf('\"'),\n\t\t\ttext.lastIndexOf(\"'\"),\n\t\t\ttext.lastIndexOf(\"=\"),\n\t\t);\n\n\t\tconst pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);\n\n\t\t// For forced extraction (Tab key), always return something\n\t\tif (forceExtract) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// For natural triggers, return if it looks like a path, ends with /, starts with ~/, .\n\t\t// Only return empty string if the text looks like it's starting a path context\n\t\tif (pathPrefix.includes(\"/\") || pathPrefix.startsWith(\".\") || pathPrefix.startsWith(\"~/\")) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// Return empty string only if we're at the beginning of the line or after a space\n\t\t// (not after quotes or other delimiters that don't suggest file paths)\n\t\tif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Expand home directory (~/) to actual home path\n\tprivate expandHomePath(path: string): string {\n\t\tif (path.startsWith(\"~/\")) {\n\t\t\tconst expandedPath = join(homedir(), path.slice(2));\n\t\t\t// Preserve trailing slash if original path had one\n\t\t\treturn path.endsWith(\"/\") && !expandedPath.endsWith(\"/\") ? `${expandedPath}/` : expandedPath;\n\t\t} else if (path === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\treturn path;\n\t}\n\n\t// Get file/directory suggestions for a given path prefix\n\tprivate getFileSuggestions(prefix: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tlet searchDir: string;\n\t\t\tlet searchPrefix: string;\n\t\t\tlet expandedPrefix = prefix;\n\t\t\tlet isAtPrefix = false;\n\n\t\t\t// Handle @ file attachment prefix\n\t\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t\tisAtPrefix = true;\n\t\t\t\texpandedPrefix = prefix.slice(1); // Remove the @\n\t\t\t}\n\n\t\t\t// Handle home directory expansion\n\t\t\tif (expandedPrefix.startsWith(\"~\")) {\n\t\t\t\texpandedPrefix = this.expandHomePath(expandedPrefix);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\texpandedPrefix === \"\" ||\n\t\t\t\texpandedPrefix === \"./\" ||\n\t\t\t\texpandedPrefix === \"../\" ||\n\t\t\t\texpandedPrefix === \"~\" ||\n\t\t\t\texpandedPrefix === \"~/\" ||\n\t\t\t\texpandedPrefix === \"/\" ||\n\t\t\t\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix === \"/\") {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else {\n\t\t\t\t// Split into directory and file prefix\n\t\t\t\tconst dir = dirname(expandedPrefix);\n\t\t\t\tconst file = basename(expandedPrefix);\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = dir;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, dir);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = file;\n\t\t\t}\n\n\t\t\tconst entries = readdirSync(searchDir, { withFileTypes: true });\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Check if entry is a directory (or a symlink pointing to a directory)\n\t\t\t\tlet isDirectory = entry.isDirectory();\n\t\t\t\tif (!isDirectory && entry.isSymbolicLink()) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst fullPath = join(searchDir, entry.name);\n\t\t\t\t\t\tisDirectory = statSync(fullPath).isDirectory();\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Broken symlink or permission error - treat as file\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet relativePath: string;\n\t\t\t\tconst name = entry.name;\n\n\t\t\t\t// Handle @ prefix path construction\n\t\t\t\tif (isAtPrefix) {\n\t\t\t\t\tconst pathWithoutAt = expandedPrefix;\n\t\t\t\t\tif (pathWithoutAt.endsWith(\"/\")) {\n\t\t\t\t\t\trelativePath = `@${pathWithoutAt}${name}`;\n\t\t\t\t\t} else if (pathWithoutAt.includes(\"/\")) {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~/\")) {\n\t\t\t\t\t\t\tconst homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/\n\t\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\t\trelativePath = `@~/${dir === \".\" ? name : join(dir, name)}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `@${join(dirname(pathWithoutAt), name)}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~\")) {\n\t\t\t\t\t\t\trelativePath = `@~/${name}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `@${name}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (prefix.endsWith(\"/\")) {\n\t\t\t\t\t// If prefix ends with /, append entry to the prefix\n\t\t\t\t\trelativePath = prefix + name;\n\t\t\t\t} else if (prefix.includes(\"/\")) {\n\t\t\t\t\t// Preserve ~/ format for home directory paths\n\t\t\t\t\tif (prefix.startsWith(\"~/\")) {\n\t\t\t\t\t\tconst homeRelativeDir = prefix.slice(2); // Remove ~/\n\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\trelativePath = `~/${dir === \".\" ? name : join(dir, name)}`;\n\t\t\t\t\t} else if (prefix.startsWith(\"/\")) {\n\t\t\t\t\t\t// Absolute path - construct properly\n\t\t\t\t\t\tconst dir = dirname(prefix);\n\t\t\t\t\t\tif (dir === \"/\") {\n\t\t\t\t\t\t\trelativePath = `/${name}`;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = `${dir}/${name}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), name);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For standalone entries, preserve ~/ if original prefix was ~/\n\t\t\t\t\tif (prefix.startsWith(\"~\")) {\n\t\t\t\t\t\trelativePath = `~/${name}`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = name;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: isDirectory ? `${relativePath}/` : relativePath,\n\t\t\t\t\tlabel: name + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Sort directories first, then alphabetically\n\t\t\tsuggestions.sort((a, b) => {\n\t\t\t\tconst aIsDir = a.value.endsWith(\"/\");\n\t\t\t\tconst bIsDir = b.value.endsWith(\"/\");\n\t\t\t\tif (aIsDir && !bIsDir) return -1;\n\t\t\t\tif (!aIsDir && bIsDir) return 1;\n\t\t\t\treturn a.label.localeCompare(b.label);\n\t\t\t});\n\n\t\t\treturn suggestions;\n\t\t} catch (_e) {\n\t\t\t// Directory doesn't exist or not accessible\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Score an entry against the query (higher = better match)\n\t// isDirectory adds bonus to prioritize folders\n\tprivate scoreEntry(filePath: string, query: string, isDirectory: boolean): number {\n\t\tconst fileName = basename(filePath);\n\t\tconst lowerFileName = fileName.toLowerCase();\n\t\tconst lowerQuery = query.toLowerCase();\n\n\t\tlet score = 0;\n\n\t\t// Exact filename match (highest)\n\t\tif (lowerFileName === lowerQuery) score = 100;\n\t\t// Filename starts with query\n\t\telse if (lowerFileName.startsWith(lowerQuery)) score = 80;\n\t\t// Substring match in filename\n\t\telse if (lowerFileName.includes(lowerQuery)) score = 50;\n\t\t// Substring match in full path\n\t\telse if (filePath.toLowerCase().includes(lowerQuery)) score = 30;\n\n\t\t// Directories get a bonus to appear first\n\t\tif (isDirectory && score > 0) score += 10;\n\n\t\treturn score;\n\t}\n\n\t// Fuzzy file search using fd (fast, respects .gitignore)\n\tprivate getFuzzyFileSuggestions(query: string): AutocompleteItem[] {\n\t\tif (!this.fdPath) {\n\t\t\t// fd not available, return empty results\n\t\t\treturn [];\n\t\t}\n\n\t\ttry {\n\t\t\tconst entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);\n\n\t\t\t// Score entries\n\t\t\tconst scoredEntries = entries\n\t\t\t\t.map((entry) => ({\n\t\t\t\t\t...entry,\n\t\t\t\t\tscore: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,\n\t\t\t\t}))\n\t\t\t\t.filter((entry) => entry.score > 0);\n\n\t\t\t// Sort by score (descending) and take top 20\n\t\t\tscoredEntries.sort((a, b) => b.score - a.score);\n\t\t\tconst topEntries = scoredEntries.slice(0, 20);\n\n\t\t\t// Build suggestions\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\t\t\tfor (const { path: entryPath, isDirectory } of topEntries) {\n\t\t\t\t// fd already includes trailing / for directories\n\t\t\t\tconst pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;\n\t\t\t\tconst entryName = basename(pathWithoutSlash);\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: `@${entryPath}`,\n\t\t\t\t\tlabel: entryName + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t\tdescription: pathWithoutSlash,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn suggestions;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Force file completion (called on Tab key) - always returns suggestions\n\tgetForceFileSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Force extract path prefix - this will always return something\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, true);\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Check if we should trigger file completion (called on Tab key)\n\tshouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
@@ -18,6 +18,7 @@ export declare class Editor implements Component {
18
18
  private pasteCounter;
19
19
  private pasteBuffer;
20
20
  private isInPaste;
21
+ private pendingShiftEnter;
21
22
  private history;
22
23
  private historyIndex;
23
24
  onSubmit?: (text: string) => void;