@mariozechner/pi-tui 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autocomplete.d.ts +2 -1
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +37 -95
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +5 -9
- package/dist/components/select-list.js.map +1 -1
- package/package.json +1 -2
package/dist/autocomplete.d.ts
CHANGED
|
@@ -22,7 +22,8 @@ export interface AutocompleteProvider {
|
|
|
22
22
|
export declare class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
23
23
|
private commands;
|
|
24
24
|
private basePath;
|
|
25
|
-
|
|
25
|
+
private fdPath;
|
|
26
|
+
constructor(commands?: (SlashCommand | AutocompleteItem)[], basePath?: string, fdPath?: string | null);
|
|
26
27
|
getSuggestions(lines: string[], cursorLine: number, cursorCol: number): {
|
|
27
28
|
items: AutocompleteItem[];
|
|
28
29
|
prefix: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"AA8GA,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,CAiFtD;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;IAwCzB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,kBAAkB;IA6I1B,OAAO,CAAC,UAAU;IAuBlB,OAAO,CAAC,uBAAuB;IAqC/B,uBAAuB,CACtB,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAsBtD;IAGD,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAU3F;CACD","sourcesContent":["import { type Dirent, readdirSync, readFileSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join, relative } from \"path\";\n\n// Parse gitignore-style file into patterns\nfunction parseIgnoreFile(filePath: string): string[] {\n\ttry {\n\t\tconst content = readFileSync(filePath, \"utf-8\");\n\t\treturn content\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trim())\n\t\t\t.filter((line) => line && !line.startsWith(\"#\"));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n// Check if a path matches gitignore patterns\nfunction isIgnored(filePath: string, patterns: string[]): boolean {\n\tconst pathWithoutSlash = filePath.endsWith(\"/\") ? filePath.slice(0, -1) : filePath;\n\tconst isDir = filePath.endsWith(\"/\");\n\n\tlet ignored = false;\n\n\tfor (const pattern of patterns) {\n\t\tlet p = pattern;\n\t\tconst negated = p.startsWith(\"!\");\n\t\tif (negated) p = p.slice(1);\n\n\t\t// Directory-only pattern\n\t\tconst dirOnly = p.endsWith(\"/\");\n\t\tif (dirOnly) {\n\t\t\tif (!isDir) continue;\n\t\t\tp = p.slice(0, -1);\n\t\t}\n\n\t\t// Remove leading slash (means anchored to root)\n\t\tconst anchored = p.startsWith(\"/\");\n\t\tif (anchored) p = p.slice(1);\n\n\t\t// Match - either at any level or anchored\n\t\tconst matchPattern = anchored ? p : \"**/\" + p;\n\t\tconst matches = minimatch(pathWithoutSlash, matchPattern, { dot: true });\n\n\t\tif (matches) {\n\t\t\tignored = !negated;\n\t\t}\n\t}\n\n\treturn ignored;\n}\n\n// Walk directory tree respecting .gitignore, similar to fd\nfunction walkDirectory(\n\tbaseDir: string,\n\tquery: string,\n\tmaxResults: number,\n): Array<{ path: string; isDirectory: boolean }> {\n\tconst results: Array<{ path: string; isDirectory: boolean }> = [];\n\tconst rootIgnorePatterns = parseIgnoreFile(join(baseDir, \".gitignore\"));\n\n\tfunction walk(currentDir: string, ignorePatterns: string[]): void {\n\t\tif (results.length >= maxResults) return;\n\n\t\t// Load local .gitignore if exists\n\t\tconst localPatterns = parseIgnoreFile(join(currentDir, \".gitignore\"));\n\t\tconst combinedPatterns = [...ignorePatterns, ...localPatterns];\n\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(currentDir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn; // Can't read directory, skip\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (results.length >= maxResults) return;\n\n\t\t\t// Skip hidden files/dirs\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\n\t\t\tconst fullPath = join(currentDir, entry.name);\n\t\t\tconst relativePath = relative(baseDir, fullPath);\n\n\t\t\t// Check if ignored\n\t\t\tconst pathToCheck = entry.isDirectory() ? relativePath + \"/\" : relativePath;\n\t\t\tif (isIgnored(pathToCheck, combinedPatterns)) continue;\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Check if dir matches query\n\t\t\t\tif (!query || entry.name.toLowerCase().includes(query.toLowerCase())) {\n\t\t\t\t\tresults.push({ path: relativePath + \"/\", isDirectory: true });\n\t\t\t\t}\n\n\t\t\t\t// Recurse\n\t\t\t\twalk(fullPath, combinedPatterns);\n\t\t\t} else {\n\t\t\t\t// Check if file matches query\n\t\t\t\tif (!query || entry.name.toLowerCase().includes(query.toLowerCase())) {\n\t\t\t\t\tresults.push({ path: relativePath, isDirectory: false });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\twalk(baseDir, rootIgnorePatterns);\n\treturn results;\n}\n\nexport interface AutocompleteItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SlashCommand {\n\tname: string;\n\tdescription?: string;\n\t// Function to get argument completions for this command\n\t// Returns null if no argument completion is available\n\tgetArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;\n}\n\nexport interface AutocompleteProvider {\n\t// Get autocomplete suggestions for current text/cursor position\n\t// Returns null if no suggestions available\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): {\n\t\titems: AutocompleteItem[];\n\t\tprefix: string; // What we're matching against (e.g., \"/\" or \"src/\")\n\t} | null;\n\n\t// Apply the selected item\n\t// Returns the new text and cursor position\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): {\n\t\tlines: string[];\n\t\tcursorLine: number;\n\t\tcursorCol: number;\n\t};\n}\n\n// Combined provider that handles both slash commands and file paths\nexport class CombinedAutocompleteProvider implements AutocompleteProvider {\n\tprivate commands: (SlashCommand | AutocompleteItem)[];\n\tprivate basePath: string;\n\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 @ file reference (fuzzy search) - must be after a space or at start\n\t\tconst atMatch = textBeforeCursor.match(/(?:^|[\\s])(@[^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\tconst prefix = atMatch[1] ?? \"@\"; // The @... part\n\t\t\tconst query = prefix.slice(1); // Remove the @\n\t\t\tconst suggestions = this.getFuzzyFileSuggestions(query);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: prefix,\n\t\t\t};\n\t\t}\n\n\t\t// Check for slash commands\n\t\tif (textBeforeCursor.startsWith(\"/\")) {\n\t\t\tconst spaceIndex = textBeforeCursor.indexOf(\" \");\n\n\t\t\tif (spaceIndex === -1) {\n\t\t\t\t// No space yet - complete command names\n\t\t\t\tconst prefix = textBeforeCursor.slice(1); // Remove the \"/\"\n\t\t\t\tconst filtered = this.commands\n\t\t\t\t\t.filter((cmd) => {\n\t\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem\n\t\t\t\t\t\treturn name?.toLowerCase().startsWith(prefix.toLowerCase());\n\t\t\t\t\t})\n\t\t\t\t\t.map((cmd) => ({\n\t\t\t\t\t\tvalue: \"name\" in cmd ? cmd.name : cmd.value,\n\t\t\t\t\t\tlabel: \"name\" in cmd ? cmd.name : cmd.label,\n\t\t\t\t\t\t...(cmd.description && { description: cmd.description }),\n\t\t\t\t\t}));\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn {\n\t\t\t\t\titems: filtered,\n\t\t\t\t\tprefix: textBeforeCursor,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// Space found - complete command arguments\n\t\t\t\tconst commandName = textBeforeCursor.slice(1, spaceIndex); // Command without \"/\"\n\t\t\t\tconst argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space\n\n\t\t\t\tconst command = this.commands.find((cmd) => {\n\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value;\n\t\t\t\t\treturn name === commandName;\n\t\t\t\t});\n\t\t\t\tif (!command || !(\"getArgumentCompletions\" in command) || !command.getArgumentCompletions) {\n\t\t\t\t\treturn null; // No argument completion for this command\n\t\t\t\t}\n\n\t\t\t\tconst argumentSuggestions = command.getArgumentCompletions(argumentText);\n\t\t\t\tif (!argumentSuggestions || argumentSuggestions.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\titems: argumentSuggestions,\n\t\t\t\t\tprefix: argumentText,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Check for file paths - triggered by Tab or if we detect a path pattern\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, false);\n\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\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// Simple approach: find the last whitespace/delimiter and extract the word after it\n\t\t// This avoids catastrophic backtracking from nested quantifiers\n\t\tconst lastDelimiterIndex = Math.max(\n\t\t\ttext.lastIndexOf(\" \"),\n\t\t\ttext.lastIndexOf(\"\\t\"),\n\t\t\ttext.lastIndexOf('\"'),\n\t\t\ttext.lastIndexOf(\"'\"),\n\t\t\ttext.lastIndexOf(\"=\"),\n\t\t);\n\n\t\tconst pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);\n\n\t\t// For forced extraction (Tab key), always return something\n\t\tif (forceExtract) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// For natural triggers, return if it looks like a path, ends with /, starts with ~/, .\n\t\t// Only return empty string if the text looks like it's starting a path context\n\t\tif (pathPrefix.includes(\"/\") || pathPrefix.startsWith(\".\") || pathPrefix.startsWith(\"~/\")) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// Return empty string only if we're at the beginning of the line or after a space\n\t\t// (not after quotes or other delimiters that don't suggest file paths)\n\t\tif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Expand home directory (~/) to actual home path\n\tprivate expandHomePath(path: string): string {\n\t\tif (path.startsWith(\"~/\")) {\n\t\t\tconst expandedPath = join(homedir(), path.slice(2));\n\t\t\t// Preserve trailing slash if original path had one\n\t\t\treturn path.endsWith(\"/\") && !expandedPath.endsWith(\"/\") ? expandedPath + \"/\" : expandedPath;\n\t\t} else if (path === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\treturn path;\n\t}\n\n\t// Get file/directory suggestions for a given path prefix\n\tprivate getFileSuggestions(prefix: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tlet searchDir: string;\n\t\t\tlet searchPrefix: string;\n\t\t\tlet expandedPrefix = prefix;\n\t\t\tlet isAtPrefix = false;\n\n\t\t\t// Handle @ file attachment prefix\n\t\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t\tisAtPrefix = true;\n\t\t\t\texpandedPrefix = prefix.slice(1); // Remove the @\n\t\t\t}\n\n\t\t\t// Handle home directory expansion\n\t\t\tif (expandedPrefix.startsWith(\"~\")) {\n\t\t\t\texpandedPrefix = this.expandHomePath(expandedPrefix);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\texpandedPrefix === \"\" ||\n\t\t\t\texpandedPrefix === \"./\" ||\n\t\t\t\texpandedPrefix === \"../\" ||\n\t\t\t\texpandedPrefix === \"~\" ||\n\t\t\t\texpandedPrefix === \"~/\" ||\n\t\t\t\texpandedPrefix === \"/\" ||\n\t\t\t\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix === \"/\") {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else {\n\t\t\t\t// Split into directory and file prefix\n\t\t\t\tconst dir = dirname(expandedPrefix);\n\t\t\t\tconst file = basename(expandedPrefix);\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = dir;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, dir);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = file;\n\t\t\t}\n\n\t\t\tconst entries = readdirSync(searchDir, { withFileTypes: true });\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst isDirectory = entry.isDirectory();\n\n\t\t\t\tlet relativePath: string;\n\t\t\t\tconst name = entry.name;\n\n\t\t\t\t// Handle @ prefix path construction\n\t\t\t\tif (isAtPrefix) {\n\t\t\t\t\tconst pathWithoutAt = expandedPrefix;\n\t\t\t\t\tif (pathWithoutAt.endsWith(\"/\")) {\n\t\t\t\t\t\trelativePath = \"@\" + pathWithoutAt + name;\n\t\t\t\t\t} else if (pathWithoutAt.includes(\"/\")) {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~/\")) {\n\t\t\t\t\t\t\tconst homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/\n\t\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\t\trelativePath = \"@~/\" + (dir === \".\" ? name : join(dir, name));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + join(dirname(pathWithoutAt), name);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~\")) {\n\t\t\t\t\t\t\trelativePath = \"@~/\" + name;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + name;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (prefix.endsWith(\"/\")) {\n\t\t\t\t\t// If prefix ends with /, append entry to the prefix\n\t\t\t\t\trelativePath = prefix + name;\n\t\t\t\t} else if (prefix.includes(\"/\")) {\n\t\t\t\t\t// Preserve ~/ format for home directory paths\n\t\t\t\t\tif (prefix.startsWith(\"~/\")) {\n\t\t\t\t\t\tconst homeRelativeDir = prefix.slice(2); // Remove ~/\n\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\trelativePath = \"~/\" + (dir === \".\" ? name : join(dir, name));\n\t\t\t\t\t} else if (prefix.startsWith(\"/\")) {\n\t\t\t\t\t\t// Absolute path - construct properly\n\t\t\t\t\t\tconst dir = dirname(prefix);\n\t\t\t\t\t\tif (dir === \"/\") {\n\t\t\t\t\t\t\trelativePath = \"/\" + name;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = dir + \"/\" + name;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), name);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For standalone entries, preserve ~/ if original prefix was ~/\n\t\t\t\t\tif (prefix.startsWith(\"~\")) {\n\t\t\t\t\t\trelativePath = \"~/\" + name;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = name;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: isDirectory ? relativePath + \"/\" : relativePath,\n\t\t\t\t\tlabel: name,\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// Score an entry against the query (higher = better match)\n\t// isDirectory adds bonus to prioritize folders\n\tprivate scoreEntry(filePath: string, query: string, isDirectory: boolean): number {\n\t\tconst fileName = basename(filePath);\n\t\tconst lowerFileName = fileName.toLowerCase();\n\t\tconst lowerQuery = query.toLowerCase();\n\n\t\tlet score = 0;\n\n\t\t// Exact filename match (highest)\n\t\tif (lowerFileName === lowerQuery) score = 100;\n\t\t// Filename starts with query\n\t\telse if (lowerFileName.startsWith(lowerQuery)) score = 80;\n\t\t// Substring match in filename\n\t\telse if (lowerFileName.includes(lowerQuery)) score = 50;\n\t\t// Substring match in full path\n\t\telse if (filePath.toLowerCase().includes(lowerQuery)) score = 30;\n\n\t\t// Directories get a bonus to appear first\n\t\tif (isDirectory && score > 0) score += 10;\n\n\t\treturn score;\n\t}\n\n\t// Fuzzy file search using pure Node.js directory walking (respects .gitignore)\n\tprivate getFuzzyFileSuggestions(query: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tconst entries = walkDirectory(this.basePath, query, 100);\n\n\t\t\t// Score entries\n\t\t\tconst scoredEntries = entries\n\t\t\t\t.map((entry) => ({\n\t\t\t\t\t...entry,\n\t\t\t\t\tscore: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,\n\t\t\t\t}))\n\t\t\t\t.filter((entry) => entry.score > 0);\n\n\t\t\t// Sort by score (descending) and take top 20\n\t\t\tscoredEntries.sort((a, b) => b.score - a.score);\n\t\t\tconst topEntries = scoredEntries.slice(0, 20);\n\n\t\t\t// Build suggestions\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\t\t\tfor (const { path: entryPath, isDirectory } of topEntries) {\n\t\t\t\tconst entryName = basename(entryPath.endsWith(\"/\") ? entryPath.slice(0, -1) : entryPath);\n\t\t\t\tconst normalizedPath = entryPath.endsWith(\"/\") ? entryPath.slice(0, -1) : entryPath;\n\t\t\t\tconst valuePath = isDirectory ? normalizedPath + \"/\" : normalizedPath;\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: \"@\" + valuePath,\n\t\t\t\t\tlabel: entryName + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t\tdescription: normalizedPath,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn suggestions;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Force file completion (called on Tab key) - always returns suggestions\n\tgetForceFileSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Force extract path prefix - this will always return something\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, true);\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Check if we should trigger file completion (called on Tab key)\n\tshouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"AA4CA,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,sBAAsB,CAAC,CAAC,cAAc,EAAE,MAAM,GAAG,gBAAgB,EAAE,GAAG,IAAI,CAAC;CAC3E;AAED,MAAM,WAAW,oBAAoB;IAGpC,cAAc,CACb,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QACF,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC;KACf,GAAG,IAAI,CAAC;IAIT,eAAe,CACd,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,MAAM,GACZ;QACF,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;KAClB,CAAC;CACF;AAGD,qBAAa,4BAA6B,YAAW,oBAAoB;IACxE,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAgB;IAE9B,YACC,QAAQ,GAAE,CAAC,YAAY,GAAG,gBAAgB,CAAC,EAAO,EAClD,QAAQ,GAAE,MAAsB,EAChC,MAAM,GAAE,MAAM,GAAG,IAAW,EAK5B;IAED,cAAc,CACb,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAiFtD;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;IAwCzB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,kBAAkB;IA6I1B,OAAO,CAAC,UAAU;IAuBlB,OAAO,CAAC,uBAAuB;IA0C/B,uBAAuB,CACtB,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GACf;QAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAsBtD;IAGD,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAU3F;CACD","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { readdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join } from \"path\";\n\n// Use fd to walk directory tree (fast, respects .gitignore)\nfunction walkDirectoryWithFd(\n\tbaseDir: string,\n\tfdPath: string,\n\tquery: string,\n\tmaxResults: number,\n): Array<{ path: string; isDirectory: boolean }> {\n\tconst args = [\"--base-directory\", baseDir, \"--max-results\", String(maxResults), \"--type\", \"f\", \"--type\", \"d\"];\n\n\t// Add query as pattern if provided\n\tif (query) {\n\t\targs.push(query);\n\t}\n\n\tconst result = spawnSync(fdPath, args, {\n\t\tencoding: \"utf-8\",\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\tmaxBuffer: 10 * 1024 * 1024,\n\t});\n\n\tif (result.status !== 0 || !result.stdout) {\n\t\treturn [];\n\t}\n\n\tconst lines = result.stdout.trim().split(\"\\n\").filter(Boolean);\n\tconst results: Array<{ path: string; isDirectory: boolean }> = [];\n\n\tfor (const line of lines) {\n\t\t// fd outputs directories with trailing /\n\t\tconst isDirectory = line.endsWith(\"/\");\n\t\tresults.push({\n\t\t\tpath: line,\n\t\t\tisDirectory,\n\t\t});\n\t}\n\n\treturn results;\n}\n\nexport interface AutocompleteItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SlashCommand {\n\tname: string;\n\tdescription?: string;\n\t// Function to get argument completions for this command\n\t// Returns null if no argument completion is available\n\tgetArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;\n}\n\nexport interface AutocompleteProvider {\n\t// Get autocomplete suggestions for current text/cursor position\n\t// Returns null if no suggestions available\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): {\n\t\titems: AutocompleteItem[];\n\t\tprefix: string; // What we're matching against (e.g., \"/\" or \"src/\")\n\t} | null;\n\n\t// Apply the selected item\n\t// Returns the new text and cursor position\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): {\n\t\tlines: string[];\n\t\tcursorLine: number;\n\t\tcursorCol: number;\n\t};\n}\n\n// Combined provider that handles both slash commands and file paths\nexport class CombinedAutocompleteProvider implements AutocompleteProvider {\n\tprivate commands: (SlashCommand | AutocompleteItem)[];\n\tprivate basePath: string;\n\tprivate fdPath: string | null;\n\n\tconstructor(\n\t\tcommands: (SlashCommand | AutocompleteItem)[] = [],\n\t\tbasePath: string = process.cwd(),\n\t\tfdPath: string | null = null,\n\t) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\n\t\tthis.fdPath = fdPath;\n\t}\n\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Check for @ file reference (fuzzy search) - must be after a space or at start\n\t\tconst atMatch = textBeforeCursor.match(/(?:^|[\\s])(@[^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\tconst prefix = atMatch[1] ?? \"@\"; // The @... part\n\t\t\tconst query = prefix.slice(1); // Remove the @\n\t\t\tconst suggestions = this.getFuzzyFileSuggestions(query);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: prefix,\n\t\t\t};\n\t\t}\n\n\t\t// Check for slash commands\n\t\tif (textBeforeCursor.startsWith(\"/\")) {\n\t\t\tconst spaceIndex = textBeforeCursor.indexOf(\" \");\n\n\t\t\tif (spaceIndex === -1) {\n\t\t\t\t// No space yet - complete command names\n\t\t\t\tconst prefix = textBeforeCursor.slice(1); // Remove the \"/\"\n\t\t\t\tconst filtered = this.commands\n\t\t\t\t\t.filter((cmd) => {\n\t\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem\n\t\t\t\t\t\treturn name?.toLowerCase().startsWith(prefix.toLowerCase());\n\t\t\t\t\t})\n\t\t\t\t\t.map((cmd) => ({\n\t\t\t\t\t\tvalue: \"name\" in cmd ? cmd.name : cmd.value,\n\t\t\t\t\t\tlabel: \"name\" in cmd ? cmd.name : cmd.label,\n\t\t\t\t\t\t...(cmd.description && { description: cmd.description }),\n\t\t\t\t\t}));\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn {\n\t\t\t\t\titems: filtered,\n\t\t\t\t\tprefix: textBeforeCursor,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// Space found - complete command arguments\n\t\t\t\tconst commandName = textBeforeCursor.slice(1, spaceIndex); // Command without \"/\"\n\t\t\t\tconst argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space\n\n\t\t\t\tconst command = this.commands.find((cmd) => {\n\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value;\n\t\t\t\t\treturn name === commandName;\n\t\t\t\t});\n\t\t\t\tif (!command || !(\"getArgumentCompletions\" in command) || !command.getArgumentCompletions) {\n\t\t\t\t\treturn null; // No argument completion for this command\n\t\t\t\t}\n\n\t\t\t\tconst argumentSuggestions = command.getArgumentCompletions(argumentText);\n\t\t\t\tif (!argumentSuggestions || argumentSuggestions.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\titems: argumentSuggestions,\n\t\t\t\t\tprefix: argumentText,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Check for file paths - triggered by Tab or if we detect a path pattern\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, false);\n\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\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// Simple approach: find the last whitespace/delimiter and extract the word after it\n\t\t// This avoids catastrophic backtracking from nested quantifiers\n\t\tconst lastDelimiterIndex = Math.max(\n\t\t\ttext.lastIndexOf(\" \"),\n\t\t\ttext.lastIndexOf(\"\\t\"),\n\t\t\ttext.lastIndexOf('\"'),\n\t\t\ttext.lastIndexOf(\"'\"),\n\t\t\ttext.lastIndexOf(\"=\"),\n\t\t);\n\n\t\tconst pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);\n\n\t\t// For forced extraction (Tab key), always return something\n\t\tif (forceExtract) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// For natural triggers, return if it looks like a path, ends with /, starts with ~/, .\n\t\t// Only return empty string if the text looks like it's starting a path context\n\t\tif (pathPrefix.includes(\"/\") || pathPrefix.startsWith(\".\") || pathPrefix.startsWith(\"~/\")) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// Return empty string only if we're at the beginning of the line or after a space\n\t\t// (not after quotes or other delimiters that don't suggest file paths)\n\t\tif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Expand home directory (~/) to actual home path\n\tprivate expandHomePath(path: string): string {\n\t\tif (path.startsWith(\"~/\")) {\n\t\t\tconst expandedPath = join(homedir(), path.slice(2));\n\t\t\t// Preserve trailing slash if original path had one\n\t\t\treturn path.endsWith(\"/\") && !expandedPath.endsWith(\"/\") ? expandedPath + \"/\" : expandedPath;\n\t\t} else if (path === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\treturn path;\n\t}\n\n\t// Get file/directory suggestions for a given path prefix\n\tprivate getFileSuggestions(prefix: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tlet searchDir: string;\n\t\t\tlet searchPrefix: string;\n\t\t\tlet expandedPrefix = prefix;\n\t\t\tlet isAtPrefix = false;\n\n\t\t\t// Handle @ file attachment prefix\n\t\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t\tisAtPrefix = true;\n\t\t\t\texpandedPrefix = prefix.slice(1); // Remove the @\n\t\t\t}\n\n\t\t\t// Handle home directory expansion\n\t\t\tif (expandedPrefix.startsWith(\"~\")) {\n\t\t\t\texpandedPrefix = this.expandHomePath(expandedPrefix);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\texpandedPrefix === \"\" ||\n\t\t\t\texpandedPrefix === \"./\" ||\n\t\t\t\texpandedPrefix === \"../\" ||\n\t\t\t\texpandedPrefix === \"~\" ||\n\t\t\t\texpandedPrefix === \"~/\" ||\n\t\t\t\texpandedPrefix === \"/\" ||\n\t\t\t\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix === \"/\") {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else {\n\t\t\t\t// Split into directory and file prefix\n\t\t\t\tconst dir = dirname(expandedPrefix);\n\t\t\t\tconst file = basename(expandedPrefix);\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = dir;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, dir);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = file;\n\t\t\t}\n\n\t\t\tconst entries = readdirSync(searchDir, { withFileTypes: true });\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst isDirectory = entry.isDirectory();\n\n\t\t\t\tlet relativePath: string;\n\t\t\t\tconst name = entry.name;\n\n\t\t\t\t// Handle @ prefix path construction\n\t\t\t\tif (isAtPrefix) {\n\t\t\t\t\tconst pathWithoutAt = expandedPrefix;\n\t\t\t\t\tif (pathWithoutAt.endsWith(\"/\")) {\n\t\t\t\t\t\trelativePath = \"@\" + pathWithoutAt + name;\n\t\t\t\t\t} else if (pathWithoutAt.includes(\"/\")) {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~/\")) {\n\t\t\t\t\t\t\tconst homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/\n\t\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\t\trelativePath = \"@~/\" + (dir === \".\" ? name : join(dir, name));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + join(dirname(pathWithoutAt), name);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~\")) {\n\t\t\t\t\t\t\trelativePath = \"@~/\" + name;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + name;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (prefix.endsWith(\"/\")) {\n\t\t\t\t\t// If prefix ends with /, append entry to the prefix\n\t\t\t\t\trelativePath = prefix + name;\n\t\t\t\t} else if (prefix.includes(\"/\")) {\n\t\t\t\t\t// Preserve ~/ format for home directory paths\n\t\t\t\t\tif (prefix.startsWith(\"~/\")) {\n\t\t\t\t\t\tconst homeRelativeDir = prefix.slice(2); // Remove ~/\n\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\trelativePath = \"~/\" + (dir === \".\" ? name : join(dir, name));\n\t\t\t\t\t} else if (prefix.startsWith(\"/\")) {\n\t\t\t\t\t\t// Absolute path - construct properly\n\t\t\t\t\t\tconst dir = dirname(prefix);\n\t\t\t\t\t\tif (dir === \"/\") {\n\t\t\t\t\t\t\trelativePath = \"/\" + name;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = dir + \"/\" + name;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), name);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For standalone entries, preserve ~/ if original prefix was ~/\n\t\t\t\t\tif (prefix.startsWith(\"~\")) {\n\t\t\t\t\t\trelativePath = \"~/\" + name;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = name;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: isDirectory ? relativePath + \"/\" : relativePath,\n\t\t\t\t\tlabel: name,\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// Score an entry against the query (higher = better match)\n\t// isDirectory adds bonus to prioritize folders\n\tprivate scoreEntry(filePath: string, query: string, isDirectory: boolean): number {\n\t\tconst fileName = basename(filePath);\n\t\tconst lowerFileName = fileName.toLowerCase();\n\t\tconst lowerQuery = query.toLowerCase();\n\n\t\tlet score = 0;\n\n\t\t// Exact filename match (highest)\n\t\tif (lowerFileName === lowerQuery) score = 100;\n\t\t// Filename starts with query\n\t\telse if (lowerFileName.startsWith(lowerQuery)) score = 80;\n\t\t// Substring match in filename\n\t\telse if (lowerFileName.includes(lowerQuery)) score = 50;\n\t\t// Substring match in full path\n\t\telse if (filePath.toLowerCase().includes(lowerQuery)) score = 30;\n\n\t\t// Directories get a bonus to appear first\n\t\tif (isDirectory && score > 0) score += 10;\n\n\t\treturn score;\n\t}\n\n\t// Fuzzy file search using fd (fast, respects .gitignore)\n\tprivate getFuzzyFileSuggestions(query: string): AutocompleteItem[] {\n\t\tif (!this.fdPath) {\n\t\t\t// fd not available, return empty results\n\t\t\treturn [];\n\t\t}\n\n\t\ttry {\n\t\t\tconst entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);\n\n\t\t\t// Score entries\n\t\t\tconst scoredEntries = entries\n\t\t\t\t.map((entry) => ({\n\t\t\t\t\t...entry,\n\t\t\t\t\tscore: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,\n\t\t\t\t}))\n\t\t\t\t.filter((entry) => entry.score > 0);\n\n\t\t\t// Sort by score (descending) and take top 20\n\t\t\tscoredEntries.sort((a, b) => b.score - a.score);\n\t\t\tconst topEntries = scoredEntries.slice(0, 20);\n\n\t\t\t// Build suggestions\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\t\t\tfor (const { path: entryPath, isDirectory } of topEntries) {\n\t\t\t\t// fd already includes trailing / for directories\n\t\t\t\tconst pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;\n\t\t\t\tconst entryName = basename(pathWithoutSlash);\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: \"@\" + entryPath,\n\t\t\t\t\tlabel: entryName + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t\tdescription: pathWithoutSlash,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn suggestions;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Force file completion (called on Tab key) - always returns suggestions\n\tgetForceFileSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Force extract path prefix - this will always return something\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, true);\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Check if we should trigger file completion (called on Tab key)\n\tshouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
|
package/dist/autocomplete.js
CHANGED
|
@@ -1,105 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import { readdirSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
-
import { basename, dirname, join
|
|
5
|
-
//
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
.map((line) => line.trim())
|
|
12
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
4
|
+
import { basename, dirname, join } from "path";
|
|
5
|
+
// Use fd to walk directory tree (fast, respects .gitignore)
|
|
6
|
+
function walkDirectoryWithFd(baseDir, fdPath, query, maxResults) {
|
|
7
|
+
const args = ["--base-directory", baseDir, "--max-results", String(maxResults), "--type", "f", "--type", "d"];
|
|
8
|
+
// Add query as pattern if provided
|
|
9
|
+
if (query) {
|
|
10
|
+
args.push(query);
|
|
13
11
|
}
|
|
14
|
-
|
|
12
|
+
const result = spawnSync(fdPath, args, {
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
16
|
+
});
|
|
17
|
+
if (result.status !== 0 || !result.stdout) {
|
|
15
18
|
return [];
|
|
16
19
|
}
|
|
17
|
-
|
|
18
|
-
// Check if a path matches gitignore patterns
|
|
19
|
-
function isIgnored(filePath, patterns) {
|
|
20
|
-
const pathWithoutSlash = filePath.endsWith("/") ? filePath.slice(0, -1) : filePath;
|
|
21
|
-
const isDir = filePath.endsWith("/");
|
|
22
|
-
let ignored = false;
|
|
23
|
-
for (const pattern of patterns) {
|
|
24
|
-
let p = pattern;
|
|
25
|
-
const negated = p.startsWith("!");
|
|
26
|
-
if (negated)
|
|
27
|
-
p = p.slice(1);
|
|
28
|
-
// Directory-only pattern
|
|
29
|
-
const dirOnly = p.endsWith("/");
|
|
30
|
-
if (dirOnly) {
|
|
31
|
-
if (!isDir)
|
|
32
|
-
continue;
|
|
33
|
-
p = p.slice(0, -1);
|
|
34
|
-
}
|
|
35
|
-
// Remove leading slash (means anchored to root)
|
|
36
|
-
const anchored = p.startsWith("/");
|
|
37
|
-
if (anchored)
|
|
38
|
-
p = p.slice(1);
|
|
39
|
-
// Match - either at any level or anchored
|
|
40
|
-
const matchPattern = anchored ? p : "**/" + p;
|
|
41
|
-
const matches = minimatch(pathWithoutSlash, matchPattern, { dot: true });
|
|
42
|
-
if (matches) {
|
|
43
|
-
ignored = !negated;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return ignored;
|
|
47
|
-
}
|
|
48
|
-
// Walk directory tree respecting .gitignore, similar to fd
|
|
49
|
-
function walkDirectory(baseDir, query, maxResults) {
|
|
20
|
+
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
|
50
21
|
const results = [];
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let entries;
|
|
59
|
-
try {
|
|
60
|
-
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return; // Can't read directory, skip
|
|
64
|
-
}
|
|
65
|
-
for (const entry of entries) {
|
|
66
|
-
if (results.length >= maxResults)
|
|
67
|
-
return;
|
|
68
|
-
// Skip hidden files/dirs
|
|
69
|
-
if (entry.name.startsWith("."))
|
|
70
|
-
continue;
|
|
71
|
-
const fullPath = join(currentDir, entry.name);
|
|
72
|
-
const relativePath = relative(baseDir, fullPath);
|
|
73
|
-
// Check if ignored
|
|
74
|
-
const pathToCheck = entry.isDirectory() ? relativePath + "/" : relativePath;
|
|
75
|
-
if (isIgnored(pathToCheck, combinedPatterns))
|
|
76
|
-
continue;
|
|
77
|
-
if (entry.isDirectory()) {
|
|
78
|
-
// Check if dir matches query
|
|
79
|
-
if (!query || entry.name.toLowerCase().includes(query.toLowerCase())) {
|
|
80
|
-
results.push({ path: relativePath + "/", isDirectory: true });
|
|
81
|
-
}
|
|
82
|
-
// Recurse
|
|
83
|
-
walk(fullPath, combinedPatterns);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
// Check if file matches query
|
|
87
|
-
if (!query || entry.name.toLowerCase().includes(query.toLowerCase())) {
|
|
88
|
-
results.push({ path: relativePath, isDirectory: false });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
// fd outputs directories with trailing /
|
|
24
|
+
const isDirectory = line.endsWith("/");
|
|
25
|
+
results.push({
|
|
26
|
+
path: line,
|
|
27
|
+
isDirectory,
|
|
28
|
+
});
|
|
92
29
|
}
|
|
93
|
-
walk(baseDir, rootIgnorePatterns);
|
|
94
30
|
return results;
|
|
95
31
|
}
|
|
96
32
|
// Combined provider that handles both slash commands and file paths
|
|
97
33
|
export class CombinedAutocompleteProvider {
|
|
98
34
|
commands;
|
|
99
35
|
basePath;
|
|
100
|
-
|
|
36
|
+
fdPath;
|
|
37
|
+
constructor(commands = [], basePath = process.cwd(), fdPath = null) {
|
|
101
38
|
this.commands = commands;
|
|
102
39
|
this.basePath = basePath;
|
|
40
|
+
this.fdPath = fdPath;
|
|
103
41
|
}
|
|
104
42
|
getSuggestions(lines, cursorLine, cursorCol) {
|
|
105
43
|
const currentLine = lines[cursorLine] || "";
|
|
@@ -433,10 +371,14 @@ export class CombinedAutocompleteProvider {
|
|
|
433
371
|
score += 10;
|
|
434
372
|
return score;
|
|
435
373
|
}
|
|
436
|
-
// Fuzzy file search using
|
|
374
|
+
// Fuzzy file search using fd (fast, respects .gitignore)
|
|
437
375
|
getFuzzyFileSuggestions(query) {
|
|
376
|
+
if (!this.fdPath) {
|
|
377
|
+
// fd not available, return empty results
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
438
380
|
try {
|
|
439
|
-
const entries =
|
|
381
|
+
const entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);
|
|
440
382
|
// Score entries
|
|
441
383
|
const scoredEntries = entries
|
|
442
384
|
.map((entry) => ({
|
|
@@ -450,13 +392,13 @@ export class CombinedAutocompleteProvider {
|
|
|
450
392
|
// Build suggestions
|
|
451
393
|
const suggestions = [];
|
|
452
394
|
for (const { path: entryPath, isDirectory } of topEntries) {
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
const
|
|
395
|
+
// fd already includes trailing / for directories
|
|
396
|
+
const pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;
|
|
397
|
+
const entryName = basename(pathWithoutSlash);
|
|
456
398
|
suggestions.push({
|
|
457
|
-
value: "@" +
|
|
399
|
+
value: "@" + entryPath,
|
|
458
400
|
label: entryName + (isDirectory ? "/" : ""),
|
|
459
|
-
description:
|
|
401
|
+
description: pathWithoutSlash,
|
|
460
402
|
});
|
|
461
403
|
}
|
|
462
404
|
return suggestions;
|
package/dist/autocomplete.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autocomplete.js","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Dirent, readdirSync, readFileSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join, relative } from \"path\";\n\n// Parse gitignore-style file into patterns\nfunction parseIgnoreFile(filePath: string): string[] {\n\ttry {\n\t\tconst content = readFileSync(filePath, \"utf-8\");\n\t\treturn content\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trim())\n\t\t\t.filter((line) => line && !line.startsWith(\"#\"));\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n// Check if a path matches gitignore patterns\nfunction isIgnored(filePath: string, patterns: string[]): boolean {\n\tconst pathWithoutSlash = filePath.endsWith(\"/\") ? filePath.slice(0, -1) : filePath;\n\tconst isDir = filePath.endsWith(\"/\");\n\n\tlet ignored = false;\n\n\tfor (const pattern of patterns) {\n\t\tlet p = pattern;\n\t\tconst negated = p.startsWith(\"!\");\n\t\tif (negated) p = p.slice(1);\n\n\t\t// Directory-only pattern\n\t\tconst dirOnly = p.endsWith(\"/\");\n\t\tif (dirOnly) {\n\t\t\tif (!isDir) continue;\n\t\t\tp = p.slice(0, -1);\n\t\t}\n\n\t\t// Remove leading slash (means anchored to root)\n\t\tconst anchored = p.startsWith(\"/\");\n\t\tif (anchored) p = p.slice(1);\n\n\t\t// Match - either at any level or anchored\n\t\tconst matchPattern = anchored ? p : \"**/\" + p;\n\t\tconst matches = minimatch(pathWithoutSlash, matchPattern, { dot: true });\n\n\t\tif (matches) {\n\t\t\tignored = !negated;\n\t\t}\n\t}\n\n\treturn ignored;\n}\n\n// Walk directory tree respecting .gitignore, similar to fd\nfunction walkDirectory(\n\tbaseDir: string,\n\tquery: string,\n\tmaxResults: number,\n): Array<{ path: string; isDirectory: boolean }> {\n\tconst results: Array<{ path: string; isDirectory: boolean }> = [];\n\tconst rootIgnorePatterns = parseIgnoreFile(join(baseDir, \".gitignore\"));\n\n\tfunction walk(currentDir: string, ignorePatterns: string[]): void {\n\t\tif (results.length >= maxResults) return;\n\n\t\t// Load local .gitignore if exists\n\t\tconst localPatterns = parseIgnoreFile(join(currentDir, \".gitignore\"));\n\t\tconst combinedPatterns = [...ignorePatterns, ...localPatterns];\n\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(currentDir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn; // Can't read directory, skip\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (results.length >= maxResults) return;\n\n\t\t\t// Skip hidden files/dirs\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\n\t\t\tconst fullPath = join(currentDir, entry.name);\n\t\t\tconst relativePath = relative(baseDir, fullPath);\n\n\t\t\t// Check if ignored\n\t\t\tconst pathToCheck = entry.isDirectory() ? relativePath + \"/\" : relativePath;\n\t\t\tif (isIgnored(pathToCheck, combinedPatterns)) continue;\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Check if dir matches query\n\t\t\t\tif (!query || entry.name.toLowerCase().includes(query.toLowerCase())) {\n\t\t\t\t\tresults.push({ path: relativePath + \"/\", isDirectory: true });\n\t\t\t\t}\n\n\t\t\t\t// Recurse\n\t\t\t\twalk(fullPath, combinedPatterns);\n\t\t\t} else {\n\t\t\t\t// Check if file matches query\n\t\t\t\tif (!query || entry.name.toLowerCase().includes(query.toLowerCase())) {\n\t\t\t\t\tresults.push({ path: relativePath, isDirectory: false });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\twalk(baseDir, rootIgnorePatterns);\n\treturn results;\n}\n\nexport interface AutocompleteItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SlashCommand {\n\tname: string;\n\tdescription?: string;\n\t// Function to get argument completions for this command\n\t// Returns null if no argument completion is available\n\tgetArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;\n}\n\nexport interface AutocompleteProvider {\n\t// Get autocomplete suggestions for current text/cursor position\n\t// Returns null if no suggestions available\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): {\n\t\titems: AutocompleteItem[];\n\t\tprefix: string; // What we're matching against (e.g., \"/\" or \"src/\")\n\t} | null;\n\n\t// Apply the selected item\n\t// Returns the new text and cursor position\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): {\n\t\tlines: string[];\n\t\tcursorLine: number;\n\t\tcursorCol: number;\n\t};\n}\n\n// Combined provider that handles both slash commands and file paths\nexport class CombinedAutocompleteProvider implements AutocompleteProvider {\n\tprivate commands: (SlashCommand | AutocompleteItem)[];\n\tprivate basePath: string;\n\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 @ file reference (fuzzy search) - must be after a space or at start\n\t\tconst atMatch = textBeforeCursor.match(/(?:^|[\\s])(@[^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\tconst prefix = atMatch[1] ?? \"@\"; // The @... part\n\t\t\tconst query = prefix.slice(1); // Remove the @\n\t\t\tconst suggestions = this.getFuzzyFileSuggestions(query);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: prefix,\n\t\t\t};\n\t\t}\n\n\t\t// Check for slash commands\n\t\tif (textBeforeCursor.startsWith(\"/\")) {\n\t\t\tconst spaceIndex = textBeforeCursor.indexOf(\" \");\n\n\t\t\tif (spaceIndex === -1) {\n\t\t\t\t// No space yet - complete command names\n\t\t\t\tconst prefix = textBeforeCursor.slice(1); // Remove the \"/\"\n\t\t\t\tconst filtered = this.commands\n\t\t\t\t\t.filter((cmd) => {\n\t\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem\n\t\t\t\t\t\treturn name?.toLowerCase().startsWith(prefix.toLowerCase());\n\t\t\t\t\t})\n\t\t\t\t\t.map((cmd) => ({\n\t\t\t\t\t\tvalue: \"name\" in cmd ? cmd.name : cmd.value,\n\t\t\t\t\t\tlabel: \"name\" in cmd ? cmd.name : cmd.label,\n\t\t\t\t\t\t...(cmd.description && { description: cmd.description }),\n\t\t\t\t\t}));\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn {\n\t\t\t\t\titems: filtered,\n\t\t\t\t\tprefix: textBeforeCursor,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// Space found - complete command arguments\n\t\t\t\tconst commandName = textBeforeCursor.slice(1, spaceIndex); // Command without \"/\"\n\t\t\t\tconst argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space\n\n\t\t\t\tconst command = this.commands.find((cmd) => {\n\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value;\n\t\t\t\t\treturn name === commandName;\n\t\t\t\t});\n\t\t\t\tif (!command || !(\"getArgumentCompletions\" in command) || !command.getArgumentCompletions) {\n\t\t\t\t\treturn null; // No argument completion for this command\n\t\t\t\t}\n\n\t\t\t\tconst argumentSuggestions = command.getArgumentCompletions(argumentText);\n\t\t\t\tif (!argumentSuggestions || argumentSuggestions.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\titems: argumentSuggestions,\n\t\t\t\t\tprefix: argumentText,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Check for file paths - triggered by Tab or if we detect a path pattern\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, false);\n\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\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// Simple approach: find the last whitespace/delimiter and extract the word after it\n\t\t// This avoids catastrophic backtracking from nested quantifiers\n\t\tconst lastDelimiterIndex = Math.max(\n\t\t\ttext.lastIndexOf(\" \"),\n\t\t\ttext.lastIndexOf(\"\\t\"),\n\t\t\ttext.lastIndexOf('\"'),\n\t\t\ttext.lastIndexOf(\"'\"),\n\t\t\ttext.lastIndexOf(\"=\"),\n\t\t);\n\n\t\tconst pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);\n\n\t\t// For forced extraction (Tab key), always return something\n\t\tif (forceExtract) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// For natural triggers, return if it looks like a path, ends with /, starts with ~/, .\n\t\t// Only return empty string if the text looks like it's starting a path context\n\t\tif (pathPrefix.includes(\"/\") || pathPrefix.startsWith(\".\") || pathPrefix.startsWith(\"~/\")) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// Return empty string only if we're at the beginning of the line or after a space\n\t\t// (not after quotes or other delimiters that don't suggest file paths)\n\t\tif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Expand home directory (~/) to actual home path\n\tprivate expandHomePath(path: string): string {\n\t\tif (path.startsWith(\"~/\")) {\n\t\t\tconst expandedPath = join(homedir(), path.slice(2));\n\t\t\t// Preserve trailing slash if original path had one\n\t\t\treturn path.endsWith(\"/\") && !expandedPath.endsWith(\"/\") ? expandedPath + \"/\" : expandedPath;\n\t\t} else if (path === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\treturn path;\n\t}\n\n\t// Get file/directory suggestions for a given path prefix\n\tprivate getFileSuggestions(prefix: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tlet searchDir: string;\n\t\t\tlet searchPrefix: string;\n\t\t\tlet expandedPrefix = prefix;\n\t\t\tlet isAtPrefix = false;\n\n\t\t\t// Handle @ file attachment prefix\n\t\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t\tisAtPrefix = true;\n\t\t\t\texpandedPrefix = prefix.slice(1); // Remove the @\n\t\t\t}\n\n\t\t\t// Handle home directory expansion\n\t\t\tif (expandedPrefix.startsWith(\"~\")) {\n\t\t\t\texpandedPrefix = this.expandHomePath(expandedPrefix);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\texpandedPrefix === \"\" ||\n\t\t\t\texpandedPrefix === \"./\" ||\n\t\t\t\texpandedPrefix === \"../\" ||\n\t\t\t\texpandedPrefix === \"~\" ||\n\t\t\t\texpandedPrefix === \"~/\" ||\n\t\t\t\texpandedPrefix === \"/\" ||\n\t\t\t\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix === \"/\") {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else {\n\t\t\t\t// Split into directory and file prefix\n\t\t\t\tconst dir = dirname(expandedPrefix);\n\t\t\t\tconst file = basename(expandedPrefix);\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = dir;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, dir);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = file;\n\t\t\t}\n\n\t\t\tconst entries = readdirSync(searchDir, { withFileTypes: true });\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst isDirectory = entry.isDirectory();\n\n\t\t\t\tlet relativePath: string;\n\t\t\t\tconst name = entry.name;\n\n\t\t\t\t// Handle @ prefix path construction\n\t\t\t\tif (isAtPrefix) {\n\t\t\t\t\tconst pathWithoutAt = expandedPrefix;\n\t\t\t\t\tif (pathWithoutAt.endsWith(\"/\")) {\n\t\t\t\t\t\trelativePath = \"@\" + pathWithoutAt + name;\n\t\t\t\t\t} else if (pathWithoutAt.includes(\"/\")) {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~/\")) {\n\t\t\t\t\t\t\tconst homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/\n\t\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\t\trelativePath = \"@~/\" + (dir === \".\" ? name : join(dir, name));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + join(dirname(pathWithoutAt), name);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~\")) {\n\t\t\t\t\t\t\trelativePath = \"@~/\" + name;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + name;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (prefix.endsWith(\"/\")) {\n\t\t\t\t\t// If prefix ends with /, append entry to the prefix\n\t\t\t\t\trelativePath = prefix + name;\n\t\t\t\t} else if (prefix.includes(\"/\")) {\n\t\t\t\t\t// Preserve ~/ format for home directory paths\n\t\t\t\t\tif (prefix.startsWith(\"~/\")) {\n\t\t\t\t\t\tconst homeRelativeDir = prefix.slice(2); // Remove ~/\n\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\trelativePath = \"~/\" + (dir === \".\" ? name : join(dir, name));\n\t\t\t\t\t} else if (prefix.startsWith(\"/\")) {\n\t\t\t\t\t\t// Absolute path - construct properly\n\t\t\t\t\t\tconst dir = dirname(prefix);\n\t\t\t\t\t\tif (dir === \"/\") {\n\t\t\t\t\t\t\trelativePath = \"/\" + name;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = dir + \"/\" + name;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), name);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For standalone entries, preserve ~/ if original prefix was ~/\n\t\t\t\t\tif (prefix.startsWith(\"~\")) {\n\t\t\t\t\t\trelativePath = \"~/\" + name;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = name;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: isDirectory ? relativePath + \"/\" : relativePath,\n\t\t\t\t\tlabel: name,\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// Score an entry against the query (higher = better match)\n\t// isDirectory adds bonus to prioritize folders\n\tprivate scoreEntry(filePath: string, query: string, isDirectory: boolean): number {\n\t\tconst fileName = basename(filePath);\n\t\tconst lowerFileName = fileName.toLowerCase();\n\t\tconst lowerQuery = query.toLowerCase();\n\n\t\tlet score = 0;\n\n\t\t// Exact filename match (highest)\n\t\tif (lowerFileName === lowerQuery) score = 100;\n\t\t// Filename starts with query\n\t\telse if (lowerFileName.startsWith(lowerQuery)) score = 80;\n\t\t// Substring match in filename\n\t\telse if (lowerFileName.includes(lowerQuery)) score = 50;\n\t\t// Substring match in full path\n\t\telse if (filePath.toLowerCase().includes(lowerQuery)) score = 30;\n\n\t\t// Directories get a bonus to appear first\n\t\tif (isDirectory && score > 0) score += 10;\n\n\t\treturn score;\n\t}\n\n\t// Fuzzy file search using pure Node.js directory walking (respects .gitignore)\n\tprivate getFuzzyFileSuggestions(query: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tconst entries = walkDirectory(this.basePath, query, 100);\n\n\t\t\t// Score entries\n\t\t\tconst scoredEntries = entries\n\t\t\t\t.map((entry) => ({\n\t\t\t\t\t...entry,\n\t\t\t\t\tscore: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,\n\t\t\t\t}))\n\t\t\t\t.filter((entry) => entry.score > 0);\n\n\t\t\t// Sort by score (descending) and take top 20\n\t\t\tscoredEntries.sort((a, b) => b.score - a.score);\n\t\t\tconst topEntries = scoredEntries.slice(0, 20);\n\n\t\t\t// Build suggestions\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\t\t\tfor (const { path: entryPath, isDirectory } of topEntries) {\n\t\t\t\tconst entryName = basename(entryPath.endsWith(\"/\") ? entryPath.slice(0, -1) : entryPath);\n\t\t\t\tconst normalizedPath = entryPath.endsWith(\"/\") ? entryPath.slice(0, -1) : entryPath;\n\t\t\t\tconst valuePath = isDirectory ? normalizedPath + \"/\" : normalizedPath;\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: \"@\" + valuePath,\n\t\t\t\t\tlabel: entryName + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t\tdescription: normalizedPath,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn suggestions;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Force file completion (called on Tab key) - always returns suggestions\n\tgetForceFileSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Force extract path prefix - this will always return something\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, true);\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Check if we should trigger file completion (called on Tab key)\n\tshouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"autocomplete.js","sourceRoot":"","sources":["../src/autocomplete.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { readdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join } from \"path\";\n\n// Use fd to walk directory tree (fast, respects .gitignore)\nfunction walkDirectoryWithFd(\n\tbaseDir: string,\n\tfdPath: string,\n\tquery: string,\n\tmaxResults: number,\n): Array<{ path: string; isDirectory: boolean }> {\n\tconst args = [\"--base-directory\", baseDir, \"--max-results\", String(maxResults), \"--type\", \"f\", \"--type\", \"d\"];\n\n\t// Add query as pattern if provided\n\tif (query) {\n\t\targs.push(query);\n\t}\n\n\tconst result = spawnSync(fdPath, args, {\n\t\tencoding: \"utf-8\",\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\tmaxBuffer: 10 * 1024 * 1024,\n\t});\n\n\tif (result.status !== 0 || !result.stdout) {\n\t\treturn [];\n\t}\n\n\tconst lines = result.stdout.trim().split(\"\\n\").filter(Boolean);\n\tconst results: Array<{ path: string; isDirectory: boolean }> = [];\n\n\tfor (const line of lines) {\n\t\t// fd outputs directories with trailing /\n\t\tconst isDirectory = line.endsWith(\"/\");\n\t\tresults.push({\n\t\t\tpath: line,\n\t\t\tisDirectory,\n\t\t});\n\t}\n\n\treturn results;\n}\n\nexport interface AutocompleteItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SlashCommand {\n\tname: string;\n\tdescription?: string;\n\t// Function to get argument completions for this command\n\t// Returns null if no argument completion is available\n\tgetArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;\n}\n\nexport interface AutocompleteProvider {\n\t// Get autocomplete suggestions for current text/cursor position\n\t// Returns null if no suggestions available\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): {\n\t\titems: AutocompleteItem[];\n\t\tprefix: string; // What we're matching against (e.g., \"/\" or \"src/\")\n\t} | null;\n\n\t// Apply the selected item\n\t// Returns the new text and cursor position\n\tapplyCompletion(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t\titem: AutocompleteItem,\n\t\tprefix: string,\n\t): {\n\t\tlines: string[];\n\t\tcursorLine: number;\n\t\tcursorCol: number;\n\t};\n}\n\n// Combined provider that handles both slash commands and file paths\nexport class CombinedAutocompleteProvider implements AutocompleteProvider {\n\tprivate commands: (SlashCommand | AutocompleteItem)[];\n\tprivate basePath: string;\n\tprivate fdPath: string | null;\n\n\tconstructor(\n\t\tcommands: (SlashCommand | AutocompleteItem)[] = [],\n\t\tbasePath: string = process.cwd(),\n\t\tfdPath: string | null = null,\n\t) {\n\t\tthis.commands = commands;\n\t\tthis.basePath = basePath;\n\t\tthis.fdPath = fdPath;\n\t}\n\n\tgetSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Check for @ file reference (fuzzy search) - must be after a space or at start\n\t\tconst atMatch = textBeforeCursor.match(/(?:^|[\\s])(@[^\\s]*)$/);\n\t\tif (atMatch) {\n\t\t\tconst prefix = atMatch[1] ?? \"@\"; // The @... part\n\t\t\tconst query = prefix.slice(1); // Remove the @\n\t\t\tconst suggestions = this.getFuzzyFileSuggestions(query);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: prefix,\n\t\t\t};\n\t\t}\n\n\t\t// Check for slash commands\n\t\tif (textBeforeCursor.startsWith(\"/\")) {\n\t\t\tconst spaceIndex = textBeforeCursor.indexOf(\" \");\n\n\t\t\tif (spaceIndex === -1) {\n\t\t\t\t// No space yet - complete command names\n\t\t\t\tconst prefix = textBeforeCursor.slice(1); // Remove the \"/\"\n\t\t\t\tconst filtered = this.commands\n\t\t\t\t\t.filter((cmd) => {\n\t\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem\n\t\t\t\t\t\treturn name?.toLowerCase().startsWith(prefix.toLowerCase());\n\t\t\t\t\t})\n\t\t\t\t\t.map((cmd) => ({\n\t\t\t\t\t\tvalue: \"name\" in cmd ? cmd.name : cmd.value,\n\t\t\t\t\t\tlabel: \"name\" in cmd ? cmd.name : cmd.label,\n\t\t\t\t\t\t...(cmd.description && { description: cmd.description }),\n\t\t\t\t\t}));\n\n\t\t\t\tif (filtered.length === 0) return null;\n\n\t\t\t\treturn {\n\t\t\t\t\titems: filtered,\n\t\t\t\t\tprefix: textBeforeCursor,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// Space found - complete command arguments\n\t\t\t\tconst commandName = textBeforeCursor.slice(1, spaceIndex); // Command without \"/\"\n\t\t\t\tconst argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space\n\n\t\t\t\tconst command = this.commands.find((cmd) => {\n\t\t\t\t\tconst name = \"name\" in cmd ? cmd.name : cmd.value;\n\t\t\t\t\treturn name === commandName;\n\t\t\t\t});\n\t\t\t\tif (!command || !(\"getArgumentCompletions\" in command) || !command.getArgumentCompletions) {\n\t\t\t\t\treturn null; // No argument completion for this command\n\t\t\t\t}\n\n\t\t\t\tconst argumentSuggestions = command.getArgumentCompletions(argumentText);\n\t\t\t\tif (!argumentSuggestions || argumentSuggestions.length === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\titems: argumentSuggestions,\n\t\t\t\t\tprefix: argumentText,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Check for file paths - triggered by Tab or if we detect a path pattern\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, false);\n\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\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// Simple approach: find the last whitespace/delimiter and extract the word after it\n\t\t// This avoids catastrophic backtracking from nested quantifiers\n\t\tconst lastDelimiterIndex = Math.max(\n\t\t\ttext.lastIndexOf(\" \"),\n\t\t\ttext.lastIndexOf(\"\\t\"),\n\t\t\ttext.lastIndexOf('\"'),\n\t\t\ttext.lastIndexOf(\"'\"),\n\t\t\ttext.lastIndexOf(\"=\"),\n\t\t);\n\n\t\tconst pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);\n\n\t\t// For forced extraction (Tab key), always return something\n\t\tif (forceExtract) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// For natural triggers, return if it looks like a path, ends with /, starts with ~/, .\n\t\t// Only return empty string if the text looks like it's starting a path context\n\t\tif (pathPrefix.includes(\"/\") || pathPrefix.startsWith(\".\") || pathPrefix.startsWith(\"~/\")) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\t// Return empty string only if we're at the beginning of the line or after a space\n\t\t// (not after quotes or other delimiters that don't suggest file paths)\n\t\tif (pathPrefix === \"\" && (text === \"\" || text.endsWith(\" \"))) {\n\t\t\treturn pathPrefix;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Expand home directory (~/) to actual home path\n\tprivate expandHomePath(path: string): string {\n\t\tif (path.startsWith(\"~/\")) {\n\t\t\tconst expandedPath = join(homedir(), path.slice(2));\n\t\t\t// Preserve trailing slash if original path had one\n\t\t\treturn path.endsWith(\"/\") && !expandedPath.endsWith(\"/\") ? expandedPath + \"/\" : expandedPath;\n\t\t} else if (path === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\treturn path;\n\t}\n\n\t// Get file/directory suggestions for a given path prefix\n\tprivate getFileSuggestions(prefix: string): AutocompleteItem[] {\n\t\ttry {\n\t\t\tlet searchDir: string;\n\t\t\tlet searchPrefix: string;\n\t\t\tlet expandedPrefix = prefix;\n\t\t\tlet isAtPrefix = false;\n\n\t\t\t// Handle @ file attachment prefix\n\t\t\tif (prefix.startsWith(\"@\")) {\n\t\t\t\tisAtPrefix = true;\n\t\t\t\texpandedPrefix = prefix.slice(1); // Remove the @\n\t\t\t}\n\n\t\t\t// Handle home directory expansion\n\t\t\tif (expandedPrefix.startsWith(\"~\")) {\n\t\t\t\texpandedPrefix = this.expandHomePath(expandedPrefix);\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\texpandedPrefix === \"\" ||\n\t\t\t\texpandedPrefix === \"./\" ||\n\t\t\t\texpandedPrefix === \"../\" ||\n\t\t\t\texpandedPrefix === \"~\" ||\n\t\t\t\texpandedPrefix === \"~/\" ||\n\t\t\t\texpandedPrefix === \"/\" ||\n\t\t\t\tprefix === \"@\"\n\t\t\t) {\n\t\t\t\t// Complete from specified position\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix === \"/\") {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else if (expandedPrefix.endsWith(\"/\")) {\n\t\t\t\t// If prefix ends with /, show contents of that directory\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = expandedPrefix;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, expandedPrefix);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = \"\";\n\t\t\t} else {\n\t\t\t\t// Split into directory and file prefix\n\t\t\t\tconst dir = dirname(expandedPrefix);\n\t\t\t\tconst file = basename(expandedPrefix);\n\t\t\t\tif (prefix.startsWith(\"~\") || expandedPrefix.startsWith(\"/\")) {\n\t\t\t\t\tsearchDir = dir;\n\t\t\t\t} else {\n\t\t\t\t\tsearchDir = join(this.basePath, dir);\n\t\t\t\t}\n\t\t\t\tsearchPrefix = file;\n\t\t\t}\n\n\t\t\tconst entries = readdirSync(searchDir, { withFileTypes: true });\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\n\t\t\tfor (const entry of entries) {\n\t\t\t\tif (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst isDirectory = entry.isDirectory();\n\n\t\t\t\tlet relativePath: string;\n\t\t\t\tconst name = entry.name;\n\n\t\t\t\t// Handle @ prefix path construction\n\t\t\t\tif (isAtPrefix) {\n\t\t\t\t\tconst pathWithoutAt = expandedPrefix;\n\t\t\t\t\tif (pathWithoutAt.endsWith(\"/\")) {\n\t\t\t\t\t\trelativePath = \"@\" + pathWithoutAt + name;\n\t\t\t\t\t} else if (pathWithoutAt.includes(\"/\")) {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~/\")) {\n\t\t\t\t\t\t\tconst homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/\n\t\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\t\trelativePath = \"@~/\" + (dir === \".\" ? name : join(dir, name));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + join(dirname(pathWithoutAt), name);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (pathWithoutAt.startsWith(\"~\")) {\n\t\t\t\t\t\t\trelativePath = \"@~/\" + name;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = \"@\" + name;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (prefix.endsWith(\"/\")) {\n\t\t\t\t\t// If prefix ends with /, append entry to the prefix\n\t\t\t\t\trelativePath = prefix + name;\n\t\t\t\t} else if (prefix.includes(\"/\")) {\n\t\t\t\t\t// Preserve ~/ format for home directory paths\n\t\t\t\t\tif (prefix.startsWith(\"~/\")) {\n\t\t\t\t\t\tconst homeRelativeDir = prefix.slice(2); // Remove ~/\n\t\t\t\t\t\tconst dir = dirname(homeRelativeDir);\n\t\t\t\t\t\trelativePath = \"~/\" + (dir === \".\" ? name : join(dir, name));\n\t\t\t\t\t} else if (prefix.startsWith(\"/\")) {\n\t\t\t\t\t\t// Absolute path - construct properly\n\t\t\t\t\t\tconst dir = dirname(prefix);\n\t\t\t\t\t\tif (dir === \"/\") {\n\t\t\t\t\t\t\trelativePath = \"/\" + name;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = dir + \"/\" + name;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = join(dirname(prefix), name);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For standalone entries, preserve ~/ if original prefix was ~/\n\t\t\t\t\tif (prefix.startsWith(\"~\")) {\n\t\t\t\t\t\trelativePath = \"~/\" + name;\n\t\t\t\t\t} else {\n\t\t\t\t\t\trelativePath = name;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: isDirectory ? relativePath + \"/\" : relativePath,\n\t\t\t\t\tlabel: name,\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// Score an entry against the query (higher = better match)\n\t// isDirectory adds bonus to prioritize folders\n\tprivate scoreEntry(filePath: string, query: string, isDirectory: boolean): number {\n\t\tconst fileName = basename(filePath);\n\t\tconst lowerFileName = fileName.toLowerCase();\n\t\tconst lowerQuery = query.toLowerCase();\n\n\t\tlet score = 0;\n\n\t\t// Exact filename match (highest)\n\t\tif (lowerFileName === lowerQuery) score = 100;\n\t\t// Filename starts with query\n\t\telse if (lowerFileName.startsWith(lowerQuery)) score = 80;\n\t\t// Substring match in filename\n\t\telse if (lowerFileName.includes(lowerQuery)) score = 50;\n\t\t// Substring match in full path\n\t\telse if (filePath.toLowerCase().includes(lowerQuery)) score = 30;\n\n\t\t// Directories get a bonus to appear first\n\t\tif (isDirectory && score > 0) score += 10;\n\n\t\treturn score;\n\t}\n\n\t// Fuzzy file search using fd (fast, respects .gitignore)\n\tprivate getFuzzyFileSuggestions(query: string): AutocompleteItem[] {\n\t\tif (!this.fdPath) {\n\t\t\t// fd not available, return empty results\n\t\t\treturn [];\n\t\t}\n\n\t\ttry {\n\t\t\tconst entries = walkDirectoryWithFd(this.basePath, this.fdPath, query, 100);\n\n\t\t\t// Score entries\n\t\t\tconst scoredEntries = entries\n\t\t\t\t.map((entry) => ({\n\t\t\t\t\t...entry,\n\t\t\t\t\tscore: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,\n\t\t\t\t}))\n\t\t\t\t.filter((entry) => entry.score > 0);\n\n\t\t\t// Sort by score (descending) and take top 20\n\t\t\tscoredEntries.sort((a, b) => b.score - a.score);\n\t\t\tconst topEntries = scoredEntries.slice(0, 20);\n\n\t\t\t// Build suggestions\n\t\t\tconst suggestions: AutocompleteItem[] = [];\n\t\t\tfor (const { path: entryPath, isDirectory } of topEntries) {\n\t\t\t\t// fd already includes trailing / for directories\n\t\t\t\tconst pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;\n\t\t\t\tconst entryName = basename(pathWithoutSlash);\n\n\t\t\t\tsuggestions.push({\n\t\t\t\t\tvalue: \"@\" + entryPath,\n\t\t\t\t\tlabel: entryName + (isDirectory ? \"/\" : \"\"),\n\t\t\t\t\tdescription: pathWithoutSlash,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn suggestions;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// Force file completion (called on Tab key) - always returns suggestions\n\tgetForceFileSuggestions(\n\t\tlines: string[],\n\t\tcursorLine: number,\n\t\tcursorCol: number,\n\t): { items: AutocompleteItem[]; prefix: string } | null {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Force extract path prefix - this will always return something\n\t\tconst pathMatch = this.extractPathPrefix(textBeforeCursor, true);\n\t\tif (pathMatch !== null) {\n\t\t\tconst suggestions = this.getFileSuggestions(pathMatch);\n\t\t\tif (suggestions.length === 0) return null;\n\n\t\t\treturn {\n\t\t\t\titems: suggestions,\n\t\t\t\tprefix: pathMatch,\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// Check if we should trigger file completion (called on Tab key)\n\tshouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {\n\t\tconst currentLine = lines[cursorLine] || \"\";\n\t\tconst textBeforeCursor = currentLine.slice(0, cursorCol);\n\n\t\t// Don't trigger if we're typing a slash command at the start of the line\n\t\tif (textBeforeCursor.trim().startsWith(\"/\") && !textBeforeCursor.trim().includes(\" \")) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-list.d.ts","sourceRoot":"","sources":["../../src/components/select-list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC/B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC;AAED,qBAAa,UAAW,YAAW,SAAS;IAC3C,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,KAAK,CAAkB;IAExB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IAEtD,YAAY,KAAK,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAK1E;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAI9B;IAED,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpC;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"select-list.d.ts","sourceRoot":"","sources":["../../src/components/select-list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC/B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC;AAED,qBAAa,UAAW,YAAW,SAAS;IAC3C,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,KAAK,CAAkB;IAExB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IAEtD,YAAY,KAAK,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAK1E;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAI9B;IAED,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpC;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgG9B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAwBjC;IAED,OAAO,CAAC,qBAAqB;IAO7B,eAAe,IAAI,UAAU,GAAG,IAAI,CAGnC;CACD","sourcesContent":["import type { Component } from \"../tui.js\";\n\nexport interface SelectItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SelectListTheme {\n\tselectedPrefix: (text: string) => string;\n\tselectedText: (text: string) => string;\n\tdescription: (text: string) => string;\n\tscrollInfo: (text: string) => string;\n\tnoMatch: (text: string) => string;\n}\n\nexport class SelectList implements Component {\n\tprivate items: SelectItem[] = [];\n\tprivate filteredItems: SelectItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate maxVisible: number = 5;\n\tprivate theme: SelectListTheme;\n\n\tpublic onSelect?: (item: SelectItem) => void;\n\tpublic onCancel?: () => void;\n\tpublic onSelectionChange?: (item: SelectItem) => void;\n\n\tconstructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme) {\n\t\tthis.items = items;\n\t\tthis.filteredItems = items;\n\t\tthis.maxVisible = maxVisible;\n\t\tthis.theme = theme;\n\t}\n\n\tsetFilter(filter: string): void {\n\t\tthis.filteredItems = this.items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase()));\n\t\t// Reset selection when filter changes\n\t\tthis.selectedIndex = 0;\n\t}\n\n\tsetSelectedIndex(index: number): void {\n\t\tthis.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// If no items match filter, show message\n\t\tif (this.filteredItems.length === 0) {\n\t\t\tlines.push(this.theme.noMatch(\" No matching commands\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);\n\n\t\t// Render visible items\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredItems[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\t// Use arrow indicator for selection - entire line uses selectedText color\n\t\t\t\tconst prefixWidth = 2; // \"→ \" is 2 characters visually\n\t\t\t\tconst displayValue = item.label || item.value;\n\n\t\t\t\tif (item.description && width > 40) {\n\t\t\t\t\t// Calculate how much space we have for value + description\n\t\t\t\t\tconst maxValueLength = Math.min(displayValue.length, 30);\n\t\t\t\t\tconst truncatedValue = displayValue.substring(0, maxValueLength);\n\t\t\t\t\tconst spacing = \" \".repeat(Math.max(1, 32 - truncatedValue.length));\n\n\t\t\t\t\t// Calculate remaining space for description using visible widths\n\t\t\t\t\tconst descriptionStart = prefixWidth + truncatedValue.length + spacing.length;\n\t\t\t\t\tconst remainingWidth = width - descriptionStart - 2; // -2 for safety\n\n\t\t\t\t\tif (remainingWidth > 10) {\n\t\t\t\t\t\tconst truncatedDesc = item.description.substring(0, remainingWidth);\n\t\t\t\t\t\t// Apply selectedText to entire line content\n\t\t\t\t\t\tline = this.theme.selectedText(\"→ \" + truncatedValue + spacing + truncatedDesc);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not enough space for description\n\t\t\t\t\t\tconst maxWidth = width - prefixWidth - 2;\n\t\t\t\t\t\tline = this.theme.selectedText(\"→ \" + displayValue.substring(0, maxWidth));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No description or not enough width\n\t\t\t\t\tconst maxWidth = width - prefixWidth - 2;\n\t\t\t\t\tline = this.theme.selectedText(\"→ \" + displayValue.substring(0, maxWidth));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst displayValue = item.label || item.value;\n\t\t\t\tconst prefix = \" \";\n\n\t\t\t\tif (item.description && width > 40) {\n\t\t\t\t\t// Calculate how much space we have for value + description\n\t\t\t\t\tconst maxValueLength = Math.min(displayValue.length, 30);\n\t\t\t\t\tconst truncatedValue = displayValue.substring(0, maxValueLength);\n\t\t\t\t\tconst spacing = \" \".repeat(Math.max(1, 32 - truncatedValue.length));\n\n\t\t\t\t\t// Calculate remaining space for description\n\t\t\t\t\tconst descriptionStart = prefix.length + truncatedValue.length + spacing.length;\n\t\t\t\t\tconst remainingWidth = width - descriptionStart - 2; // -2 for safety\n\n\t\t\t\t\tif (remainingWidth > 10) {\n\t\t\t\t\t\tconst truncatedDesc = item.description.substring(0, remainingWidth);\n\t\t\t\t\t\tconst descText = this.theme.description(spacing + truncatedDesc);\n\t\t\t\t\t\tline = prefix + truncatedValue + descText;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not enough space for description\n\t\t\t\t\t\tconst maxWidth = width - prefix.length - 2;\n\t\t\t\t\t\tline = prefix + displayValue.substring(0, maxWidth);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No description or not enough width\n\t\t\t\t\tconst maxWidth = width - prefix.length - 2;\n\t\t\t\t\tline = prefix + displayValue.substring(0, maxWidth);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlines.push(line);\n\t\t}\n\n\t\t// Add scroll indicators if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredItems.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`;\n\t\t\t// Truncate if too long for terminal\n\t\t\tconst maxWidth = width - 2;\n\t\t\tconst truncated = scrollText.substring(0, maxWidth);\n\t\t\tlines.push(this.theme.scrollInfo(truncated));\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.notifySelectionChange();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredItems.length - 1, this.selectedIndex + 1);\n\t\t\tthis.notifySelectionChange();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedItem = this.filteredItems[this.selectedIndex];\n\t\t\tif (selectedItem && this.onSelect) {\n\t\t\t\tthis.onSelect(selectedItem);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (keyData === \"\\x1b\" || keyData === \"\\x03\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate notifySelectionChange(): void {\n\t\tconst selectedItem = this.filteredItems[this.selectedIndex];\n\t\tif (selectedItem && this.onSelectionChange) {\n\t\t\tthis.onSelectionChange(selectedItem);\n\t\t}\n\t}\n\n\tgetSelectedItem(): SelectItem | null {\n\t\tconst item = this.filteredItems[this.selectedIndex];\n\t\treturn item || null;\n\t}\n}\n"]}
|
|
@@ -42,8 +42,7 @@ export class SelectList {
|
|
|
42
42
|
const isSelected = i === this.selectedIndex;
|
|
43
43
|
let line = "";
|
|
44
44
|
if (isSelected) {
|
|
45
|
-
// Use arrow indicator for selection
|
|
46
|
-
const prefix = this.theme.selectedPrefix("→ ");
|
|
45
|
+
// Use arrow indicator for selection - entire line uses selectedText color
|
|
47
46
|
const prefixWidth = 2; // "→ " is 2 characters visually
|
|
48
47
|
const displayValue = item.label || item.value;
|
|
49
48
|
if (item.description && width > 40) {
|
|
@@ -56,22 +55,19 @@ export class SelectList {
|
|
|
56
55
|
const remainingWidth = width - descriptionStart - 2; // -2 for safety
|
|
57
56
|
if (remainingWidth > 10) {
|
|
58
57
|
const truncatedDesc = item.description.substring(0, remainingWidth);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
line = prefix + selectedText + descText;
|
|
58
|
+
// Apply selectedText to entire line content
|
|
59
|
+
line = this.theme.selectedText("→ " + truncatedValue + spacing + truncatedDesc);
|
|
62
60
|
}
|
|
63
61
|
else {
|
|
64
62
|
// Not enough space for description
|
|
65
63
|
const maxWidth = width - prefixWidth - 2;
|
|
66
|
-
|
|
67
|
-
line = prefix + selectedText;
|
|
64
|
+
line = this.theme.selectedText("→ " + displayValue.substring(0, maxWidth));
|
|
68
65
|
}
|
|
69
66
|
}
|
|
70
67
|
else {
|
|
71
68
|
// No description or not enough width
|
|
72
69
|
const maxWidth = width - prefixWidth - 2;
|
|
73
|
-
|
|
74
|
-
line = prefix + selectedText;
|
|
70
|
+
line = this.theme.selectedText("→ " + displayValue.substring(0, maxWidth));
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-list.js","sourceRoot":"","sources":["../../src/components/select-list.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\n\nexport interface SelectItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SelectListTheme {\n\tselectedPrefix: (text: string) => string;\n\tselectedText: (text: string) => string;\n\tdescription: (text: string) => string;\n\tscrollInfo: (text: string) => string;\n\tnoMatch: (text: string) => string;\n}\n\nexport class SelectList implements Component {\n\tprivate items: SelectItem[] = [];\n\tprivate filteredItems: SelectItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate maxVisible: number = 5;\n\tprivate theme: SelectListTheme;\n\n\tpublic onSelect?: (item: SelectItem) => void;\n\tpublic onCancel?: () => void;\n\tpublic onSelectionChange?: (item: SelectItem) => void;\n\n\tconstructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme) {\n\t\tthis.items = items;\n\t\tthis.filteredItems = items;\n\t\tthis.maxVisible = maxVisible;\n\t\tthis.theme = theme;\n\t}\n\n\tsetFilter(filter: string): void {\n\t\tthis.filteredItems = this.items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase()));\n\t\t// Reset selection when filter changes\n\t\tthis.selectedIndex = 0;\n\t}\n\n\tsetSelectedIndex(index: number): void {\n\t\tthis.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// If no items match filter, show message\n\t\tif (this.filteredItems.length === 0) {\n\t\t\tlines.push(this.theme.noMatch(\" No matching commands\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);\n\n\t\t// Render visible items\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredItems[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\t// Use arrow indicator for selection\n\t\t\t\tconst prefix = this.theme.selectedPrefix(\"→ \");\n\t\t\t\tconst prefixWidth = 2; // \"→ \" is 2 characters visually\n\t\t\t\tconst displayValue = item.label || item.value;\n\n\t\t\t\tif (item.description && width > 40) {\n\t\t\t\t\t// Calculate how much space we have for value + description\n\t\t\t\t\tconst maxValueLength = Math.min(displayValue.length, 30);\n\t\t\t\t\tconst truncatedValue = displayValue.substring(0, maxValueLength);\n\t\t\t\t\tconst spacing = \" \".repeat(Math.max(1, 32 - truncatedValue.length));\n\n\t\t\t\t\t// Calculate remaining space for description using visible widths\n\t\t\t\t\tconst descriptionStart = prefixWidth + truncatedValue.length + spacing.length;\n\t\t\t\t\tconst remainingWidth = width - descriptionStart - 2; // -2 for safety\n\n\t\t\t\t\tif (remainingWidth > 10) {\n\t\t\t\t\t\tconst truncatedDesc = item.description.substring(0, remainingWidth);\n\t\t\t\t\t\tconst selectedText = this.theme.selectedText(truncatedValue);\n\t\t\t\t\t\tconst descText = this.theme.description(spacing + truncatedDesc);\n\t\t\t\t\t\tline = prefix + selectedText + descText;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not enough space for description\n\t\t\t\t\t\tconst maxWidth = width - prefixWidth - 2;\n\t\t\t\t\t\tconst selectedText = this.theme.selectedText(displayValue.substring(0, maxWidth));\n\t\t\t\t\t\tline = prefix + selectedText;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No description or not enough width\n\t\t\t\t\tconst maxWidth = width - prefixWidth - 2;\n\t\t\t\t\tconst selectedText = this.theme.selectedText(displayValue.substring(0, maxWidth));\n\t\t\t\t\tline = prefix + selectedText;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst displayValue = item.label || item.value;\n\t\t\t\tconst prefix = \" \";\n\n\t\t\t\tif (item.description && width > 40) {\n\t\t\t\t\t// Calculate how much space we have for value + description\n\t\t\t\t\tconst maxValueLength = Math.min(displayValue.length, 30);\n\t\t\t\t\tconst truncatedValue = displayValue.substring(0, maxValueLength);\n\t\t\t\t\tconst spacing = \" \".repeat(Math.max(1, 32 - truncatedValue.length));\n\n\t\t\t\t\t// Calculate remaining space for description\n\t\t\t\t\tconst descriptionStart = prefix.length + truncatedValue.length + spacing.length;\n\t\t\t\t\tconst remainingWidth = width - descriptionStart - 2; // -2 for safety\n\n\t\t\t\t\tif (remainingWidth > 10) {\n\t\t\t\t\t\tconst truncatedDesc = item.description.substring(0, remainingWidth);\n\t\t\t\t\t\tconst descText = this.theme.description(spacing + truncatedDesc);\n\t\t\t\t\t\tline = prefix + truncatedValue + descText;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not enough space for description\n\t\t\t\t\t\tconst maxWidth = width - prefix.length - 2;\n\t\t\t\t\t\tline = prefix + displayValue.substring(0, maxWidth);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No description or not enough width\n\t\t\t\t\tconst maxWidth = width - prefix.length - 2;\n\t\t\t\t\tline = prefix + displayValue.substring(0, maxWidth);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlines.push(line);\n\t\t}\n\n\t\t// Add scroll indicators if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredItems.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`;\n\t\t\t// Truncate if too long for terminal\n\t\t\tconst maxWidth = width - 2;\n\t\t\tconst truncated = scrollText.substring(0, maxWidth);\n\t\t\tlines.push(this.theme.scrollInfo(truncated));\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.notifySelectionChange();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredItems.length - 1, this.selectedIndex + 1);\n\t\t\tthis.notifySelectionChange();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedItem = this.filteredItems[this.selectedIndex];\n\t\t\tif (selectedItem && this.onSelect) {\n\t\t\t\tthis.onSelect(selectedItem);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (keyData === \"\\x1b\" || keyData === \"\\x03\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate notifySelectionChange(): void {\n\t\tconst selectedItem = this.filteredItems[this.selectedIndex];\n\t\tif (selectedItem && this.onSelectionChange) {\n\t\t\tthis.onSelectionChange(selectedItem);\n\t\t}\n\t}\n\n\tgetSelectedItem(): SelectItem | null {\n\t\tconst item = this.filteredItems[this.selectedIndex];\n\t\treturn item || null;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"select-list.js","sourceRoot":"","sources":["../../src/components/select-list.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\n\nexport interface SelectItem {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface SelectListTheme {\n\tselectedPrefix: (text: string) => string;\n\tselectedText: (text: string) => string;\n\tdescription: (text: string) => string;\n\tscrollInfo: (text: string) => string;\n\tnoMatch: (text: string) => string;\n}\n\nexport class SelectList implements Component {\n\tprivate items: SelectItem[] = [];\n\tprivate filteredItems: SelectItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate maxVisible: number = 5;\n\tprivate theme: SelectListTheme;\n\n\tpublic onSelect?: (item: SelectItem) => void;\n\tpublic onCancel?: () => void;\n\tpublic onSelectionChange?: (item: SelectItem) => void;\n\n\tconstructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme) {\n\t\tthis.items = items;\n\t\tthis.filteredItems = items;\n\t\tthis.maxVisible = maxVisible;\n\t\tthis.theme = theme;\n\t}\n\n\tsetFilter(filter: string): void {\n\t\tthis.filteredItems = this.items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase()));\n\t\t// Reset selection when filter changes\n\t\tthis.selectedIndex = 0;\n\t}\n\n\tsetSelectedIndex(index: number): void {\n\t\tthis.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// If no items match filter, show message\n\t\tif (this.filteredItems.length === 0) {\n\t\t\tlines.push(this.theme.noMatch(\" No matching commands\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);\n\n\t\t// Render visible items\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredItems[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\t// Use arrow indicator for selection - entire line uses selectedText color\n\t\t\t\tconst prefixWidth = 2; // \"→ \" is 2 characters visually\n\t\t\t\tconst displayValue = item.label || item.value;\n\n\t\t\t\tif (item.description && width > 40) {\n\t\t\t\t\t// Calculate how much space we have for value + description\n\t\t\t\t\tconst maxValueLength = Math.min(displayValue.length, 30);\n\t\t\t\t\tconst truncatedValue = displayValue.substring(0, maxValueLength);\n\t\t\t\t\tconst spacing = \" \".repeat(Math.max(1, 32 - truncatedValue.length));\n\n\t\t\t\t\t// Calculate remaining space for description using visible widths\n\t\t\t\t\tconst descriptionStart = prefixWidth + truncatedValue.length + spacing.length;\n\t\t\t\t\tconst remainingWidth = width - descriptionStart - 2; // -2 for safety\n\n\t\t\t\t\tif (remainingWidth > 10) {\n\t\t\t\t\t\tconst truncatedDesc = item.description.substring(0, remainingWidth);\n\t\t\t\t\t\t// Apply selectedText to entire line content\n\t\t\t\t\t\tline = this.theme.selectedText(\"→ \" + truncatedValue + spacing + truncatedDesc);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not enough space for description\n\t\t\t\t\t\tconst maxWidth = width - prefixWidth - 2;\n\t\t\t\t\t\tline = this.theme.selectedText(\"→ \" + displayValue.substring(0, maxWidth));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No description or not enough width\n\t\t\t\t\tconst maxWidth = width - prefixWidth - 2;\n\t\t\t\t\tline = this.theme.selectedText(\"→ \" + displayValue.substring(0, maxWidth));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst displayValue = item.label || item.value;\n\t\t\t\tconst prefix = \" \";\n\n\t\t\t\tif (item.description && width > 40) {\n\t\t\t\t\t// Calculate how much space we have for value + description\n\t\t\t\t\tconst maxValueLength = Math.min(displayValue.length, 30);\n\t\t\t\t\tconst truncatedValue = displayValue.substring(0, maxValueLength);\n\t\t\t\t\tconst spacing = \" \".repeat(Math.max(1, 32 - truncatedValue.length));\n\n\t\t\t\t\t// Calculate remaining space for description\n\t\t\t\t\tconst descriptionStart = prefix.length + truncatedValue.length + spacing.length;\n\t\t\t\t\tconst remainingWidth = width - descriptionStart - 2; // -2 for safety\n\n\t\t\t\t\tif (remainingWidth > 10) {\n\t\t\t\t\t\tconst truncatedDesc = item.description.substring(0, remainingWidth);\n\t\t\t\t\t\tconst descText = this.theme.description(spacing + truncatedDesc);\n\t\t\t\t\t\tline = prefix + truncatedValue + descText;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not enough space for description\n\t\t\t\t\t\tconst maxWidth = width - prefix.length - 2;\n\t\t\t\t\t\tline = prefix + displayValue.substring(0, maxWidth);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No description or not enough width\n\t\t\t\t\tconst maxWidth = width - prefix.length - 2;\n\t\t\t\t\tline = prefix + displayValue.substring(0, maxWidth);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlines.push(line);\n\t\t}\n\n\t\t// Add scroll indicators if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredItems.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`;\n\t\t\t// Truncate if too long for terminal\n\t\t\tconst maxWidth = width - 2;\n\t\t\tconst truncated = scrollText.substring(0, maxWidth);\n\t\t\tlines.push(this.theme.scrollInfo(truncated));\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.notifySelectionChange();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.filteredItems.length - 1, this.selectedIndex + 1);\n\t\t\tthis.notifySelectionChange();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedItem = this.filteredItems[this.selectedIndex];\n\t\t\tif (selectedItem && this.onSelect) {\n\t\t\t\tthis.onSelect(selectedItem);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (keyData === \"\\x1b\" || keyData === \"\\x03\") {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate notifySelectionChange(): void {\n\t\tconst selectedItem = this.filteredItems[this.selectedIndex];\n\t\tif (selectedItem && this.onSelectionChange) {\n\t\t\tthis.onSelectionChange(selectedItem);\n\t\t}\n\t}\n\n\tgetSelectedItem(): SelectItem | null {\n\t\tconst item = this.filteredItems[this.selectedIndex];\n\t\treturn item || null;\n\t}\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"chalk": "^5.5.0",
|
|
42
42
|
"marked": "^15.0.12",
|
|
43
43
|
"mime-types": "^3.0.1",
|
|
44
|
-
"minimatch": "^10.1.1",
|
|
45
44
|
"string-width": "^8.1.0"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|