@mariozechner/pi-tui 0.7.21 → 0.7.23

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.
@@ -1 +1 @@
1
- {"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"AAuFA,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;IAEzB,YAAY,QAAQ,GAAE,CAAC,YAAY,GAAG,gBAAgB,CAAC,EAAO,EAAE,QAAQ,GAAE,MAAsB,EAG/F;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,CAmEtD;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,CA0D5D;IAGD,OAAO,CAAC,iBAAiB;IA0CzB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,kBAAkB;IAwI1B,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 { readdirSync, statSync } from \"fs\";\nimport mimeTypes from \"mime-types\";\nimport { homedir } from \"os\";\nimport { basename, dirname, extname, join } from \"path\";\n\nfunction isAttachableFile(filePath: string): boolean {\n\tconst mimeType = mimeTypes.lookup(filePath);\n\n\t// Check file extension for common text files that might be misidentified\n\tconst textExtensions = [\n\t\t\".txt\",\n\t\t\".md\",\n\t\t\".markdown\",\n\t\t\".js\",\n\t\t\".ts\",\n\t\t\".tsx\",\n\t\t\".jsx\",\n\t\t\".py\",\n\t\t\".java\",\n\t\t\".c\",\n\t\t\".cpp\",\n\t\t\".h\",\n\t\t\".hpp\",\n\t\t\".cs\",\n\t\t\".php\",\n\t\t\".rb\",\n\t\t\".go\",\n\t\t\".rs\",\n\t\t\".swift\",\n\t\t\".kt\",\n\t\t\".scala\",\n\t\t\".sh\",\n\t\t\".bash\",\n\t\t\".zsh\",\n\t\t\".fish\",\n\t\t\".html\",\n\t\t\".htm\",\n\t\t\".css\",\n\t\t\".scss\",\n\t\t\".sass\",\n\t\t\".less\",\n\t\t\".xml\",\n\t\t\".json\",\n\t\t\".yaml\",\n\t\t\".yml\",\n\t\t\".toml\",\n\t\t\".ini\",\n\t\t\".cfg\",\n\t\t\".conf\",\n\t\t\".log\",\n\t\t\".sql\",\n\t\t\".r\",\n\t\t\".R\",\n\t\t\".m\",\n\t\t\".pl\",\n\t\t\".lua\",\n\t\t\".vim\",\n\t\t\".dockerfile\",\n\t\t\".makefile\",\n\t\t\".cmake\",\n\t\t\".gradle\",\n\t\t\".maven\",\n\t\t\".properties\",\n\t\t\".env\",\n\t];\n\n\tconst ext = extname(filePath).toLowerCase();\n\tif (textExtensions.includes(ext)) return true;\n\n\tif (!mimeType) return false;\n\n\tif (mimeType.startsWith(\"image/\")) return true;\n\tif (mimeType.startsWith(\"text/\")) return true;\n\n\t// Special cases for common text files that might not be detected as text/\n\tconst commonTextTypes = [\n\t\t\"application/json\",\n\t\t\"application/javascript\",\n\t\t\"application/typescript\",\n\t\t\"application/xml\",\n\t\t\"application/yaml\",\n\t\t\"application/x-yaml\",\n\t];\n\n\treturn commonTextTypes.includes(mimeType);\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\n\tconstructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd()) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\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 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\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 \"/\")\n\t\tif (prefix.startsWith(\"/\")) {\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// Match paths - including those ending with /, ~/, or any word at end for forced extraction\n\t\t// This regex captures:\n\t\t// - Paths starting from beginning of line or after space/quote/equals\n\t\t// - Optional ./ or ../ or ~/ prefix (including the trailing slash for ~/)\n\t\t// - The path itself (can include / in the middle)\n\t\t// - For forced extraction, capture any word at the end\n\t\tconst matches = text.match(/(?:^|[\\s\"'=])((?:~\\/|\\.{0,2}\\/?)?(?:[^\\s\"'=]*\\/?)*[^\\s\"'=]*)$/);\n\t\tif (!matches) {\n\t\t\t// If forced extraction and no matches, return empty string to trigger from current dir\n\t\t\treturn forceExtract ? \"\" : null;\n\t\t}\n\n\t\tconst pathPrefix = matches[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\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.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 if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || (isAtPrefix && 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(\"~\") || (isAtPrefix && 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);\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst fullPath = join(searchDir, entry);\n\t\t\t\tconst isDirectory = statSync(fullPath).isDirectory();\n\n\t\t\t\t// For @ prefix, filter to only show directories and attachable files\n\t\t\t\tif (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tlet relativePath: string;\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 + entry;\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 === \".\" ? entry : join(dir, entry));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + join(dirname(pathWithoutAt), entry);\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 = \"@~/\" + entry;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + entry;\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 + entry;\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 === \".\" ? entry : join(dir, entry));\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), entry);\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 = \"~/\" + entry;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = entry;\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: entry,\n\t\t\t\t\tdescription: isDirectory ? \"directory\" : \"file\",\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.description === \"directory\";\n\t\t\t\tconst bIsDir = b.description === \"directory\";\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.slice(0, 10); // Limit to 10 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// 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 in a slash command\n\t\tif (textBeforeCursor.startsWith(\"/\") && !textBeforeCursor.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 in a slash command\n\t\tif (textBeforeCursor.startsWith(\"/\") && !textBeforeCursor.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":"AAuFA,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;IAEzB,YAAY,QAAQ,GAAE,CAAC,YAAY,GAAG,gBAAgB,CAAC,EAAO,EAAE,QAAQ,GAAE,MAAsB,EAG/F;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,CAmEtD;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,CA0D5D;IAGD,OAAO,CAAC,iBAAiB;IA4CzB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,kBAAkB;IAuJ1B,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 { readdirSync, statSync } from \"fs\";\nimport mimeTypes from \"mime-types\";\nimport { homedir } from \"os\";\nimport { basename, dirname, extname, join } from \"path\";\n\nfunction isAttachableFile(filePath: string): boolean {\n\tconst mimeType = mimeTypes.lookup(filePath);\n\n\t// Check file extension for common text files that might be misidentified\n\tconst textExtensions = [\n\t\t\".txt\",\n\t\t\".md\",\n\t\t\".markdown\",\n\t\t\".js\",\n\t\t\".ts\",\n\t\t\".tsx\",\n\t\t\".jsx\",\n\t\t\".py\",\n\t\t\".java\",\n\t\t\".c\",\n\t\t\".cpp\",\n\t\t\".h\",\n\t\t\".hpp\",\n\t\t\".cs\",\n\t\t\".php\",\n\t\t\".rb\",\n\t\t\".go\",\n\t\t\".rs\",\n\t\t\".swift\",\n\t\t\".kt\",\n\t\t\".scala\",\n\t\t\".sh\",\n\t\t\".bash\",\n\t\t\".zsh\",\n\t\t\".fish\",\n\t\t\".html\",\n\t\t\".htm\",\n\t\t\".css\",\n\t\t\".scss\",\n\t\t\".sass\",\n\t\t\".less\",\n\t\t\".xml\",\n\t\t\".json\",\n\t\t\".yaml\",\n\t\t\".yml\",\n\t\t\".toml\",\n\t\t\".ini\",\n\t\t\".cfg\",\n\t\t\".conf\",\n\t\t\".log\",\n\t\t\".sql\",\n\t\t\".r\",\n\t\t\".R\",\n\t\t\".m\",\n\t\t\".pl\",\n\t\t\".lua\",\n\t\t\".vim\",\n\t\t\".dockerfile\",\n\t\t\".makefile\",\n\t\t\".cmake\",\n\t\t\".gradle\",\n\t\t\".maven\",\n\t\t\".properties\",\n\t\t\".env\",\n\t];\n\n\tconst ext = extname(filePath).toLowerCase();\n\tif (textExtensions.includes(ext)) return true;\n\n\tif (!mimeType) return false;\n\n\tif (mimeType.startsWith(\"image/\")) return true;\n\tif (mimeType.startsWith(\"text/\")) return true;\n\n\t// Special cases for common text files that might not be detected as text/\n\tconst commonTextTypes = [\n\t\t\"application/json\",\n\t\t\"application/javascript\",\n\t\t\"application/typescript\",\n\t\t\"application/xml\",\n\t\t\"application/yaml\",\n\t\t\"application/x-yaml\",\n\t];\n\n\treturn commonTextTypes.includes(mimeType);\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\n\tconstructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd()) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\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 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\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 \"/\")\n\t\tif (prefix.startsWith(\"/\")) {\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// Match paths - including those ending with /, ~/, or any word at end for forced extraction\n\t\t// This regex captures:\n\t\t// - Paths starting from beginning of line or after space/quote/equals\n\t\t// - Absolute paths starting with /\n\t\t// - Relative paths with ./ or ../\n\t\t// - Home directory paths with ~/\n\t\t// - The path itself (can include / in the middle)\n\t\t// - For forced extraction, capture any word at the end\n\t\tconst matches = text.match(/(?:^|[\\s\"'=])((?:\\/|~\\/|\\.{1,2}\\/)?(?:[^\\s\"'=]*\\/?)*[^\\s\"'=]*)$/);\n\t\tif (!matches) {\n\t\t\t// If forced extraction and no matches, return empty string to trigger from current dir\n\t\t\treturn forceExtract ? \"\" : null;\n\t\t}\n\n\t\tconst pathPrefix = matches[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);\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst fullPath = join(searchDir, entry);\n\t\t\t\tlet isDirectory: boolean;\n\t\t\t\ttry {\n\t\t\t\t\tisDirectory = statSync(fullPath).isDirectory();\n\t\t\t\t} catch (e) {\n\t\t\t\t\t// Skip files we can't stat (permission issues, broken symlinks, etc.)\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// For @ prefix, filter to only show directories and attachable files\n\t\t\t\tif (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tlet relativePath: string;\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 + entry;\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 === \".\" ? entry : join(dir, entry));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + join(dirname(pathWithoutAt), entry);\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 = \"@~/\" + entry;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + entry;\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 + entry;\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 === \".\" ? entry : join(dir, entry));\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 = \"/\" + entry;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = dir + \"/\" + entry;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), entry);\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 = \"~/\" + entry;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = entry;\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: entry,\n\t\t\t\t\tdescription: isDirectory ? \"directory\" : \"file\",\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.description === \"directory\";\n\t\t\t\tconst bIsDir = b.description === \"directory\";\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// 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"]}
@@ -210,10 +210,12 @@ export class CombinedAutocompleteProvider {
210
210
  // Match paths - including those ending with /, ~/, or any word at end for forced extraction
211
211
  // This regex captures:
212
212
  // - Paths starting from beginning of line or after space/quote/equals
213
- // - Optional ./ or ../ or ~/ prefix (including the trailing slash for ~/)
213
+ // - Absolute paths starting with /
214
+ // - Relative paths with ./ or ../
215
+ // - Home directory paths with ~/
214
216
  // - The path itself (can include / in the middle)
215
217
  // - For forced extraction, capture any word at the end
216
- const matches = text.match(/(?:^|[\s"'=])((?:~\/|\.{0,2}\/?)?(?:[^\s"'=]*\/?)*[^\s"'=]*)$/);
218
+ const matches = text.match(/(?:^|[\s"'=])((?:\/|~\/|\.{1,2}\/)?(?:[^\s"'=]*\/?)*[^\s"'=]*)$/);
217
219
  if (!matches) {
218
220
  // If forced extraction and no matches, return empty string to trigger from current dir
219
221
  return forceExtract ? "" : null;
@@ -268,9 +270,10 @@ export class CombinedAutocompleteProvider {
268
270
  expandedPrefix === "../" ||
269
271
  expandedPrefix === "~" ||
270
272
  expandedPrefix === "~/" ||
273
+ expandedPrefix === "/" ||
271
274
  prefix === "@") {
272
275
  // Complete from specified position
273
- if (prefix.startsWith("~")) {
276
+ if (prefix.startsWith("~") || expandedPrefix === "/") {
274
277
  searchDir = expandedPrefix;
275
278
  }
276
279
  else {
@@ -280,7 +283,7 @@ export class CombinedAutocompleteProvider {
280
283
  }
281
284
  else if (expandedPrefix.endsWith("/")) {
282
285
  // If prefix ends with /, show contents of that directory
283
- if (prefix.startsWith("~") || (isAtPrefix && expandedPrefix.startsWith("/"))) {
286
+ if (prefix.startsWith("~") || expandedPrefix.startsWith("/")) {
284
287
  searchDir = expandedPrefix;
285
288
  }
286
289
  else {
@@ -292,7 +295,7 @@ export class CombinedAutocompleteProvider {
292
295
  // Split into directory and file prefix
293
296
  const dir = dirname(expandedPrefix);
294
297
  const file = basename(expandedPrefix);
295
- if (prefix.startsWith("~") || (isAtPrefix && expandedPrefix.startsWith("/"))) {
298
+ if (prefix.startsWith("~") || expandedPrefix.startsWith("/")) {
296
299
  searchDir = dir;
297
300
  }
298
301
  else {
@@ -307,7 +310,14 @@ export class CombinedAutocompleteProvider {
307
310
  continue;
308
311
  }
309
312
  const fullPath = join(searchDir, entry);
310
- const isDirectory = statSync(fullPath).isDirectory();
313
+ let isDirectory;
314
+ try {
315
+ isDirectory = statSync(fullPath).isDirectory();
316
+ }
317
+ catch (e) {
318
+ // Skip files we can't stat (permission issues, broken symlinks, etc.)
319
+ continue;
320
+ }
311
321
  // For @ prefix, filter to only show directories and attachable files
312
322
  if (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) {
313
323
  continue;
@@ -349,6 +359,16 @@ export class CombinedAutocompleteProvider {
349
359
  const dir = dirname(homeRelativeDir);
350
360
  relativePath = "~/" + (dir === "." ? entry : join(dir, entry));
351
361
  }
362
+ else if (prefix.startsWith("/")) {
363
+ // Absolute path - construct properly
364
+ const dir = dirname(prefix);
365
+ if (dir === "/") {
366
+ relativePath = "/" + entry;
367
+ }
368
+ else {
369
+ relativePath = dir + "/" + entry;
370
+ }
371
+ }
352
372
  else {
353
373
  relativePath = join(dirname(prefix), entry);
354
374
  }
@@ -378,7 +398,7 @@ export class CombinedAutocompleteProvider {
378
398
  return 1;
379
399
  return a.label.localeCompare(b.label);
380
400
  });
381
- return suggestions.slice(0, 10); // Limit to 10 suggestions
401
+ return suggestions;
382
402
  }
383
403
  catch (e) {
384
404
  // Directory doesn't exist or not accessible
@@ -389,8 +409,8 @@ export class CombinedAutocompleteProvider {
389
409
  getForceFileSuggestions(lines, cursorLine, cursorCol) {
390
410
  const currentLine = lines[cursorLine] || "";
391
411
  const textBeforeCursor = currentLine.slice(0, cursorCol);
392
- // Don't trigger if we're in a slash command
393
- if (textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
412
+ // Don't trigger if we're typing a slash command at the start of the line
413
+ if (textBeforeCursor.trim().startsWith("/") && !textBeforeCursor.trim().includes(" ")) {
394
414
  return null;
395
415
  }
396
416
  // Force extract path prefix - this will always return something
@@ -410,8 +430,8 @@ export class CombinedAutocompleteProvider {
410
430
  shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
411
431
  const currentLine = lines[cursorLine] || "";
412
432
  const textBeforeCursor = currentLine.slice(0, cursorCol);
413
- // Don't trigger if we're in a slash command
414
- if (textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
433
+ // Don't trigger if we're typing a slash command at the start of the line
434
+ if (textBeforeCursor.trim().startsWith("/") && !textBeforeCursor.trim().includes(" ")) {
415
435
  return false;
416
436
  }
417
437
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"autocomplete.js","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, statSync } from \"fs\";\nimport mimeTypes from \"mime-types\";\nimport { homedir } from \"os\";\nimport { basename, dirname, extname, join } from \"path\";\n\nfunction isAttachableFile(filePath: string): boolean {\n\tconst mimeType = mimeTypes.lookup(filePath);\n\n\t// Check file extension for common text files that might be misidentified\n\tconst textExtensions = [\n\t\t\".txt\",\n\t\t\".md\",\n\t\t\".markdown\",\n\t\t\".js\",\n\t\t\".ts\",\n\t\t\".tsx\",\n\t\t\".jsx\",\n\t\t\".py\",\n\t\t\".java\",\n\t\t\".c\",\n\t\t\".cpp\",\n\t\t\".h\",\n\t\t\".hpp\",\n\t\t\".cs\",\n\t\t\".php\",\n\t\t\".rb\",\n\t\t\".go\",\n\t\t\".rs\",\n\t\t\".swift\",\n\t\t\".kt\",\n\t\t\".scala\",\n\t\t\".sh\",\n\t\t\".bash\",\n\t\t\".zsh\",\n\t\t\".fish\",\n\t\t\".html\",\n\t\t\".htm\",\n\t\t\".css\",\n\t\t\".scss\",\n\t\t\".sass\",\n\t\t\".less\",\n\t\t\".xml\",\n\t\t\".json\",\n\t\t\".yaml\",\n\t\t\".yml\",\n\t\t\".toml\",\n\t\t\".ini\",\n\t\t\".cfg\",\n\t\t\".conf\",\n\t\t\".log\",\n\t\t\".sql\",\n\t\t\".r\",\n\t\t\".R\",\n\t\t\".m\",\n\t\t\".pl\",\n\t\t\".lua\",\n\t\t\".vim\",\n\t\t\".dockerfile\",\n\t\t\".makefile\",\n\t\t\".cmake\",\n\t\t\".gradle\",\n\t\t\".maven\",\n\t\t\".properties\",\n\t\t\".env\",\n\t];\n\n\tconst ext = extname(filePath).toLowerCase();\n\tif (textExtensions.includes(ext)) return true;\n\n\tif (!mimeType) return false;\n\n\tif (mimeType.startsWith(\"image/\")) return true;\n\tif (mimeType.startsWith(\"text/\")) return true;\n\n\t// Special cases for common text files that might not be detected as text/\n\tconst commonTextTypes = [\n\t\t\"application/json\",\n\t\t\"application/javascript\",\n\t\t\"application/typescript\",\n\t\t\"application/xml\",\n\t\t\"application/yaml\",\n\t\t\"application/x-yaml\",\n\t];\n\n\treturn commonTextTypes.includes(mimeType);\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\n\tconstructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd()) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\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 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\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 \"/\")\n\t\tif (prefix.startsWith(\"/\")) {\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// Match paths - including those ending with /, ~/, or any word at end for forced extraction\n\t\t// This regex captures:\n\t\t// - Paths starting from beginning of line or after space/quote/equals\n\t\t// - Optional ./ or ../ or ~/ prefix (including the trailing slash for ~/)\n\t\t// - The path itself (can include / in the middle)\n\t\t// - For forced extraction, capture any word at the end\n\t\tconst matches = text.match(/(?:^|[\\s\"'=])((?:~\\/|\\.{0,2}\\/?)?(?:[^\\s\"'=]*\\/?)*[^\\s\"'=]*)$/);\n\t\tif (!matches) {\n\t\t\t// If forced extraction and no matches, return empty string to trigger from current dir\n\t\t\treturn forceExtract ? \"\" : null;\n\t\t}\n\n\t\tconst pathPrefix = matches[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\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.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 if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || (isAtPrefix && 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(\"~\") || (isAtPrefix && 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);\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst fullPath = join(searchDir, entry);\n\t\t\t\tconst isDirectory = statSync(fullPath).isDirectory();\n\n\t\t\t\t// For @ prefix, filter to only show directories and attachable files\n\t\t\t\tif (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tlet relativePath: string;\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 + entry;\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 === \".\" ? entry : join(dir, entry));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + join(dirname(pathWithoutAt), entry);\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 = \"@~/\" + entry;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + entry;\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 + entry;\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 === \".\" ? entry : join(dir, entry));\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), entry);\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 = \"~/\" + entry;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = entry;\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: entry,\n\t\t\t\t\tdescription: isDirectory ? \"directory\" : \"file\",\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.description === \"directory\";\n\t\t\t\tconst bIsDir = b.description === \"directory\";\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.slice(0, 10); // Limit to 10 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// 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 in a slash command\n\t\tif (textBeforeCursor.startsWith(\"/\") && !textBeforeCursor.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 in a slash command\n\t\tif (textBeforeCursor.startsWith(\"/\") && !textBeforeCursor.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 { readdirSync, statSync } from \"fs\";\nimport mimeTypes from \"mime-types\";\nimport { homedir } from \"os\";\nimport { basename, dirname, extname, join } from \"path\";\n\nfunction isAttachableFile(filePath: string): boolean {\n\tconst mimeType = mimeTypes.lookup(filePath);\n\n\t// Check file extension for common text files that might be misidentified\n\tconst textExtensions = [\n\t\t\".txt\",\n\t\t\".md\",\n\t\t\".markdown\",\n\t\t\".js\",\n\t\t\".ts\",\n\t\t\".tsx\",\n\t\t\".jsx\",\n\t\t\".py\",\n\t\t\".java\",\n\t\t\".c\",\n\t\t\".cpp\",\n\t\t\".h\",\n\t\t\".hpp\",\n\t\t\".cs\",\n\t\t\".php\",\n\t\t\".rb\",\n\t\t\".go\",\n\t\t\".rs\",\n\t\t\".swift\",\n\t\t\".kt\",\n\t\t\".scala\",\n\t\t\".sh\",\n\t\t\".bash\",\n\t\t\".zsh\",\n\t\t\".fish\",\n\t\t\".html\",\n\t\t\".htm\",\n\t\t\".css\",\n\t\t\".scss\",\n\t\t\".sass\",\n\t\t\".less\",\n\t\t\".xml\",\n\t\t\".json\",\n\t\t\".yaml\",\n\t\t\".yml\",\n\t\t\".toml\",\n\t\t\".ini\",\n\t\t\".cfg\",\n\t\t\".conf\",\n\t\t\".log\",\n\t\t\".sql\",\n\t\t\".r\",\n\t\t\".R\",\n\t\t\".m\",\n\t\t\".pl\",\n\t\t\".lua\",\n\t\t\".vim\",\n\t\t\".dockerfile\",\n\t\t\".makefile\",\n\t\t\".cmake\",\n\t\t\".gradle\",\n\t\t\".maven\",\n\t\t\".properties\",\n\t\t\".env\",\n\t];\n\n\tconst ext = extname(filePath).toLowerCase();\n\tif (textExtensions.includes(ext)) return true;\n\n\tif (!mimeType) return false;\n\n\tif (mimeType.startsWith(\"image/\")) return true;\n\tif (mimeType.startsWith(\"text/\")) return true;\n\n\t// Special cases for common text files that might not be detected as text/\n\tconst commonTextTypes = [\n\t\t\"application/json\",\n\t\t\"application/javascript\",\n\t\t\"application/typescript\",\n\t\t\"application/xml\",\n\t\t\"application/yaml\",\n\t\t\"application/x-yaml\",\n\t];\n\n\treturn commonTextTypes.includes(mimeType);\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\n\tconstructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd()) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\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 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\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 \"/\")\n\t\tif (prefix.startsWith(\"/\")) {\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// Match paths - including those ending with /, ~/, or any word at end for forced extraction\n\t\t// This regex captures:\n\t\t// - Paths starting from beginning of line or after space/quote/equals\n\t\t// - Absolute paths starting with /\n\t\t// - Relative paths with ./ or ../\n\t\t// - Home directory paths with ~/\n\t\t// - The path itself (can include / in the middle)\n\t\t// - For forced extraction, capture any word at the end\n\t\tconst matches = text.match(/(?:^|[\\s\"'=])((?:\\/|~\\/|\\.{1,2}\\/)?(?:[^\\s\"'=]*\\/?)*[^\\s\"'=]*)$/);\n\t\tif (!matches) {\n\t\t\t// If forced extraction and no matches, return empty string to trigger from current dir\n\t\t\treturn forceExtract ? \"\" : null;\n\t\t}\n\n\t\tconst pathPrefix = matches[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);\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst fullPath = join(searchDir, entry);\n\t\t\t\tlet isDirectory: boolean;\n\t\t\t\ttry {\n\t\t\t\t\tisDirectory = statSync(fullPath).isDirectory();\n\t\t\t\t} catch (e) {\n\t\t\t\t\t// Skip files we can't stat (permission issues, broken symlinks, etc.)\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// For @ prefix, filter to only show directories and attachable files\n\t\t\t\tif (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tlet relativePath: string;\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 + entry;\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 === \".\" ? entry : join(dir, entry));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + join(dirname(pathWithoutAt), entry);\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 = \"@~/\" + entry;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + entry;\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 + entry;\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 === \".\" ? entry : join(dir, entry));\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 = \"/\" + entry;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = dir + \"/\" + entry;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), entry);\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 = \"~/\" + entry;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = entry;\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: entry,\n\t\t\t\t\tdescription: isDirectory ? \"directory\" : \"file\",\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.description === \"directory\";\n\t\t\t\tconst bIsDir = b.description === \"directory\";\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// 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 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGhD;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CActE;AAoDD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAkBrH","sourcesContent":["import { Chalk } from \"chalk\";\nimport stringWidth from \"string-width\";\n\nconst colorChalk = new Chalk({ level: 3 });\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoWordsWithAnsi(text: string): string[] {\n\tconst words: string[] = [];\n\tlet currentWord = \"\";\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst char = text[i];\n\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentWord += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \" \") {\n\t\t\tif (currentWord) {\n\t\t\t\twords.push(currentWord);\n\t\t\t\tcurrentWord = \"\";\n\t\t\t}\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrentWord += char;\n\t\ti++;\n\t}\n\n\tif (currentWord) {\n\t\twords.push(currentWord);\n\t}\n\n\treturn words;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst words = splitIntoWordsWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const word of words) {\n\t\tconst wordVisibleLength = visibleWidth(word);\n\n\t\t// Check if adding this word would exceed width\n\t\tconst spaceNeeded = currentVisibleLength > 0 ? 1 : 0;\n\t\tconst totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line\n\t\t\twrapped.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes() + word;\n\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tif (currentVisibleLength > 0) {\n\t\t\t\tcurrentLine += \" \" + word;\n\t\t\t\tcurrentVisibleLength += 1 + wordVisibleLength;\n\t\t\t} else {\n\t\t\t\tcurrentLine += word;\n\t\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t\t}\n\t\t}\n\n\t\tupdateTrackerFromText(word, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * Handles the tricky case where content contains \\x1b[0m resets that would\n * kill the background color. We reapply the background after any reset.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgRgb - Background RGB color\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgRgb: { r: number; g: number; b: number }): string {\n\tconst bgStart = `\\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;\n\tconst bgEnd = \"\\x1b[49m\";\n\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Strategy: wrap content + padding in background, then fix any 0m resets\n\tconst withPadding = line + padding;\n\tconst withBg = bgStart + withPadding + bgEnd;\n\n\t// Find all \\x1b[0m or \\x1b[49m that would kill background\n\t// Replace with reset + background reapplication\n\tconst fixedBg = withBg.replace(/\\x1b\\[0m/g, `\\x1b[0m${bgStart}`);\n\n\treturn fixedBg;\n}\n"]}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGhD;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CActE;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAkBrH","sourcesContent":["import { Chalk } from \"chalk\";\nimport stringWidth from \"string-width\";\n\nconst colorChalk = new Chalk({ level: 3 });\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoWordsWithAnsi(text: string): string[] {\n\tconst words: string[] = [];\n\tlet currentWord = \"\";\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst char = text[i];\n\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentWord += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \" \") {\n\t\t\tif (currentWord) {\n\t\t\t\twords.push(currentWord);\n\t\t\t\tcurrentWord = \"\";\n\t\t\t}\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrentWord += char;\n\t\ti++;\n\t}\n\n\tif (currentWord) {\n\t\twords.push(currentWord);\n\t}\n\n\treturn words;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst words = splitIntoWordsWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const word of words) {\n\t\tconst wordVisibleLength = visibleWidth(word);\n\n\t\t// Word itself is too long - break it character by character\n\t\tif (wordVisibleLength > width) {\n\t\t\tif (currentLine) {\n\t\t\t\twrapped.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break long word\n\t\t\tconst broken = breakLongWord(word, width, tracker);\n\t\t\twrapped.push(...broken.slice(0, -1));\n\t\t\tcurrentLine = broken[broken.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if adding this word would exceed width\n\t\tconst spaceNeeded = currentVisibleLength > 0 ? 1 : 0;\n\t\tconst totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line\n\t\t\twrapped.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes() + word;\n\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tif (currentVisibleLength > 0) {\n\t\t\t\tcurrentLine += \" \" + word;\n\t\t\t\tcurrentVisibleLength += 1 + wordVisibleLength;\n\t\t\t} else {\n\t\t\t\tcurrentLine += word;\n\t\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t\t}\n\t\t}\n\n\t\tupdateTrackerFromText(word, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\nfunction breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentWidth = 0;\n\tlet i = 0;\n\n\twhile (i < word.length) {\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentLine += ansiResult.code;\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = word[i];\n\t\tconst charWidth = visibleWidth(char);\n\n\t\tif (currentWidth + charWidth > width) {\n\t\t\tlines.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\tcurrentWidth = 0;\n\t\t}\n\n\t\tcurrentLine += char;\n\t\tcurrentWidth += charWidth;\n\t\ti++;\n\t}\n\n\tif (currentLine) {\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * Handles the tricky case where content contains \\x1b[0m resets that would\n * kill the background color. We reapply the background after any reset.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgRgb - Background RGB color\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgRgb: { r: number; g: number; b: number }): string {\n\tconst bgStart = `\\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;\n\tconst bgEnd = \"\\x1b[49m\";\n\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Strategy: wrap content + padding in background, then fix any 0m resets\n\tconst withPadding = line + padding;\n\tconst withBg = bgStart + withPadding + bgEnd;\n\n\t// Find all \\x1b[0m or \\x1b[49m that would kill background\n\t// Replace with reset + background reapplication\n\tconst fixedBg = withBg.replace(/\\x1b\\[0m/g, `\\x1b[0m${bgStart}`);\n\n\treturn fixedBg;\n}\n"]}
package/dist/utils.js CHANGED
@@ -133,6 +133,20 @@ function wrapSingleLine(line, width) {
133
133
  let currentVisibleLength = 0;
134
134
  for (const word of words) {
135
135
  const wordVisibleLength = visibleWidth(word);
136
+ // Word itself is too long - break it character by character
137
+ if (wordVisibleLength > width) {
138
+ if (currentLine) {
139
+ wrapped.push(currentLine);
140
+ currentLine = "";
141
+ currentVisibleLength = 0;
142
+ }
143
+ // Break long word
144
+ const broken = breakLongWord(word, width, tracker);
145
+ wrapped.push(...broken.slice(0, -1));
146
+ currentLine = broken[broken.length - 1];
147
+ currentVisibleLength = visibleWidth(currentLine);
148
+ continue;
149
+ }
136
150
  // Check if adding this word would exceed width
137
151
  const spaceNeeded = currentVisibleLength > 0 ? 1 : 0;
138
152
  const totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;
@@ -160,6 +174,35 @@ function wrapSingleLine(line, width) {
160
174
  }
161
175
  return wrapped.length > 0 ? wrapped : [""];
162
176
  }
177
+ function breakLongWord(word, width, tracker) {
178
+ const lines = [];
179
+ let currentLine = tracker.getActiveCodes();
180
+ let currentWidth = 0;
181
+ let i = 0;
182
+ while (i < word.length) {
183
+ const ansiResult = extractAnsiCode(word, i);
184
+ if (ansiResult) {
185
+ currentLine += ansiResult.code;
186
+ tracker.process(ansiResult.code);
187
+ i += ansiResult.length;
188
+ continue;
189
+ }
190
+ const char = word[i];
191
+ const charWidth = visibleWidth(char);
192
+ if (currentWidth + charWidth > width) {
193
+ lines.push(currentLine);
194
+ currentLine = tracker.getActiveCodes();
195
+ currentWidth = 0;
196
+ }
197
+ currentLine += char;
198
+ currentWidth += charWidth;
199
+ i++;
200
+ }
201
+ if (currentLine) {
202
+ lines.push(currentLine);
203
+ }
204
+ return lines.length > 0 ? lines : [""];
205
+ }
163
206
  /**
164
207
  * Apply background color to a line, padding to full width.
165
208
  *
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { Chalk } from \"chalk\";\nimport stringWidth from \"string-width\";\n\nconst colorChalk = new Chalk({ level: 3 });\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoWordsWithAnsi(text: string): string[] {\n\tconst words: string[] = [];\n\tlet currentWord = \"\";\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst char = text[i];\n\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentWord += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \" \") {\n\t\t\tif (currentWord) {\n\t\t\t\twords.push(currentWord);\n\t\t\t\tcurrentWord = \"\";\n\t\t\t}\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrentWord += char;\n\t\ti++;\n\t}\n\n\tif (currentWord) {\n\t\twords.push(currentWord);\n\t}\n\n\treturn words;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst words = splitIntoWordsWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const word of words) {\n\t\tconst wordVisibleLength = visibleWidth(word);\n\n\t\t// Check if adding this word would exceed width\n\t\tconst spaceNeeded = currentVisibleLength > 0 ? 1 : 0;\n\t\tconst totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line\n\t\t\twrapped.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes() + word;\n\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tif (currentVisibleLength > 0) {\n\t\t\t\tcurrentLine += \" \" + word;\n\t\t\t\tcurrentVisibleLength += 1 + wordVisibleLength;\n\t\t\t} else {\n\t\t\t\tcurrentLine += word;\n\t\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t\t}\n\t\t}\n\n\t\tupdateTrackerFromText(word, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * Handles the tricky case where content contains \\x1b[0m resets that would\n * kill the background color. We reapply the background after any reset.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgRgb - Background RGB color\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgRgb: { r: number; g: number; b: number }): string {\n\tconst bgStart = `\\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;\n\tconst bgEnd = \"\\x1b[49m\";\n\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Strategy: wrap content + padding in background, then fix any 0m resets\n\tconst withPadding = line + padding;\n\tconst withBg = bgStart + withPadding + bgEnd;\n\n\t// Find all \\x1b[0m or \\x1b[49m that would kill background\n\t// Replace with reset + background reapplication\n\tconst fixedBg = withBg.replace(/\\x1b\\[0m/g, `\\x1b[0m${bgStart}`);\n\n\treturn fixedBg;\n}\n"]}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { Chalk } from \"chalk\";\nimport stringWidth from \"string-width\";\n\nconst colorChalk = new Chalk({ level: 3 });\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoWordsWithAnsi(text: string): string[] {\n\tconst words: string[] = [];\n\tlet currentWord = \"\";\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst char = text[i];\n\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentWord += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \" \") {\n\t\t\tif (currentWord) {\n\t\t\t\twords.push(currentWord);\n\t\t\t\tcurrentWord = \"\";\n\t\t\t}\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrentWord += char;\n\t\ti++;\n\t}\n\n\tif (currentWord) {\n\t\twords.push(currentWord);\n\t}\n\n\treturn words;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst words = splitIntoWordsWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const word of words) {\n\t\tconst wordVisibleLength = visibleWidth(word);\n\n\t\t// Word itself is too long - break it character by character\n\t\tif (wordVisibleLength > width) {\n\t\t\tif (currentLine) {\n\t\t\t\twrapped.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break long word\n\t\t\tconst broken = breakLongWord(word, width, tracker);\n\t\t\twrapped.push(...broken.slice(0, -1));\n\t\t\tcurrentLine = broken[broken.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if adding this word would exceed width\n\t\tconst spaceNeeded = currentVisibleLength > 0 ? 1 : 0;\n\t\tconst totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line\n\t\t\twrapped.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes() + word;\n\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tif (currentVisibleLength > 0) {\n\t\t\t\tcurrentLine += \" \" + word;\n\t\t\t\tcurrentVisibleLength += 1 + wordVisibleLength;\n\t\t\t} else {\n\t\t\t\tcurrentLine += word;\n\t\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t\t}\n\t\t}\n\n\t\tupdateTrackerFromText(word, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\nfunction breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentWidth = 0;\n\tlet i = 0;\n\n\twhile (i < word.length) {\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentLine += ansiResult.code;\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = word[i];\n\t\tconst charWidth = visibleWidth(char);\n\n\t\tif (currentWidth + charWidth > width) {\n\t\t\tlines.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\tcurrentWidth = 0;\n\t\t}\n\n\t\tcurrentLine += char;\n\t\tcurrentWidth += charWidth;\n\t\ti++;\n\t}\n\n\tif (currentLine) {\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * Handles the tricky case where content contains \\x1b[0m resets that would\n * kill the background color. We reapply the background after any reset.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgRgb - Background RGB color\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgRgb: { r: number; g: number; b: number }): string {\n\tconst bgStart = `\\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;\n\tconst bgEnd = \"\\x1b[49m\";\n\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Strategy: wrap content + padding in background, then fix any 0m resets\n\tconst withPadding = line + padding;\n\tconst withBg = bgStart + withPadding + bgEnd;\n\n\t// Find all \\x1b[0m or \\x1b[49m that would kill background\n\t// Replace with reset + background reapplication\n\tconst fixedBg = withBg.replace(/\\x1b\\[0m/g, `\\x1b[0m${bgStart}`);\n\n\treturn fixedBg;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-tui",
3
- "version": "0.7.21",
3
+ "version": "0.7.23",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",