mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 23:01:32 +00:00
feat: fuzzy file search with @ prefix
- Type @ to fuzzy-search files/folders across project - Respects .gitignore and skips hidden files - Pure Node.js implementation using readdir with withFileTypes - No external dependencies (fd/find) required - Also optimized Tab completion to use withFileTypes instead of statSync Based on PR #60 by @fightbulc, reimplemented for performance.
This commit is contained in:
parent
4de46fbab3
commit
384e4a3a7d
5 changed files with 137 additions and 224 deletions
3
package-lock.json
generated
3
package-lock.json
generated
|
|
@ -2379,6 +2379,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "10.1.1",
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/brace-expansion": "^5.0.0"
|
"@isaacs/brace-expansion": "^5.0.0"
|
||||||
|
|
@ -3955,6 +3957,7 @@
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"marked": "^15.0.12",
|
"marked": "^15.0.12",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
|
"minimatch": "^10.1.1",
|
||||||
"string-width": "^8.1.0"
|
"string-width": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Fuzzy File Search (`@`)**: Type `@` followed by a search term to fuzzy-search files and folders across your project. Respects `.gitignore` and skips hidden files. Directories are prioritized in results. Based on [PR #60](https://github.com/badlogic/pi-mono/pull/60) by [@fightbulc](https://github.com/fightbulc), reimplemented with pure Node.js for fast, dependency-free searching.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **Emoji Text Wrapping Crash**: Fixed crash when rendering text containing emojis (e.g., 😂) followed by long content like URLs. The `breakLongWord` function in `pi-tui` was iterating over UTF-16 code units instead of grapheme clusters, causing emojis (which are surrogate pairs) to be miscounted during line wrapping. Now uses `Intl.Segmenter` to properly handle multi-codepoint characters.
|
- **Emoji Text Wrapping Crash**: Fixed crash when rendering text containing emojis (e.g., 😂) followed by long content like URLs. The `breakLongWord` function in `pi-tui` was iterating over UTF-16 code units instead of grapheme clusters, causing emojis (which are surrogate pairs) to be miscounted during line wrapping. Now uses `Intl.Segmenter` to properly handle multi-codepoint characters.
|
||||||
|
|
|
||||||
|
|
@ -469,9 +469,8 @@ Type **`@`** to fuzzy-search for files and folders in your project:
|
||||||
- Directories are prioritized and shown with trailing `/`
|
- Directories are prioritized and shown with trailing `/`
|
||||||
- Autocomplete triggers immediately when you type `@`
|
- Autocomplete triggers immediately when you type `@`
|
||||||
- Use **Up/Down arrows** to navigate, **Tab**/**Enter** to select
|
- Use **Up/Down arrows** to navigate, **Tab**/**Enter** to select
|
||||||
- Only shows attachable files (text, code, images) and directories
|
|
||||||
|
|
||||||
Uses `fdfind`/`fd` for fast searching if available, falls back to `find` on all Unix systems.
|
Respects `.gitignore` files and skips hidden files/directories.
|
||||||
|
|
||||||
### Path Completion
|
### Path Completion
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"marked": "^15.0.12",
|
"marked": "^15.0.12",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
|
"minimatch": "^10.1.1",
|
||||||
"string-width": "^8.1.0"
|
"string-width": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,89 +1,111 @@
|
||||||
import { execSync } from "child_process";
|
import { type Dirent, readdirSync, readFileSync } from "fs";
|
||||||
import { readdirSync, statSync } from "fs";
|
import { minimatch } from "minimatch";
|
||||||
import mimeTypes from "mime-types";
|
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { basename, dirname, extname, join } from "path";
|
import { basename, dirname, join, relative } from "path";
|
||||||
|
|
||||||
function isAttachableFile(filePath: string): boolean {
|
// Parse gitignore-style file into patterns
|
||||||
const mimeType = mimeTypes.lookup(filePath);
|
function parseIgnoreFile(filePath: string): string[] {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(filePath, "utf-8");
|
||||||
|
return content
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line && !line.startsWith("#"));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check file extension for common text files that might be misidentified
|
// Check if a path matches gitignore patterns
|
||||||
const textExtensions = [
|
function isIgnored(filePath: string, patterns: string[]): boolean {
|
||||||
".txt",
|
const pathWithoutSlash = filePath.endsWith("/") ? filePath.slice(0, -1) : filePath;
|
||||||
".md",
|
const isDir = filePath.endsWith("/");
|
||||||
".markdown",
|
|
||||||
".js",
|
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".jsx",
|
|
||||||
".py",
|
|
||||||
".java",
|
|
||||||
".c",
|
|
||||||
".cpp",
|
|
||||||
".h",
|
|
||||||
".hpp",
|
|
||||||
".cs",
|
|
||||||
".php",
|
|
||||||
".rb",
|
|
||||||
".go",
|
|
||||||
".rs",
|
|
||||||
".swift",
|
|
||||||
".kt",
|
|
||||||
".scala",
|
|
||||||
".sh",
|
|
||||||
".bash",
|
|
||||||
".zsh",
|
|
||||||
".fish",
|
|
||||||
".html",
|
|
||||||
".htm",
|
|
||||||
".css",
|
|
||||||
".scss",
|
|
||||||
".sass",
|
|
||||||
".less",
|
|
||||||
".xml",
|
|
||||||
".json",
|
|
||||||
".yaml",
|
|
||||||
".yml",
|
|
||||||
".toml",
|
|
||||||
".ini",
|
|
||||||
".cfg",
|
|
||||||
".conf",
|
|
||||||
".log",
|
|
||||||
".sql",
|
|
||||||
".r",
|
|
||||||
".R",
|
|
||||||
".m",
|
|
||||||
".pl",
|
|
||||||
".lua",
|
|
||||||
".vim",
|
|
||||||
".dockerfile",
|
|
||||||
".makefile",
|
|
||||||
".cmake",
|
|
||||||
".gradle",
|
|
||||||
".maven",
|
|
||||||
".properties",
|
|
||||||
".env",
|
|
||||||
];
|
|
||||||
|
|
||||||
const ext = extname(filePath).toLowerCase();
|
let ignored = false;
|
||||||
if (textExtensions.includes(ext)) return true;
|
|
||||||
|
|
||||||
if (!mimeType) return false;
|
for (const pattern of patterns) {
|
||||||
|
let p = pattern;
|
||||||
|
const negated = p.startsWith("!");
|
||||||
|
if (negated) p = p.slice(1);
|
||||||
|
|
||||||
if (mimeType.startsWith("image/")) return true;
|
// Directory-only pattern
|
||||||
if (mimeType.startsWith("text/")) return true;
|
const dirOnly = p.endsWith("/");
|
||||||
|
if (dirOnly) {
|
||||||
|
if (!isDir) continue;
|
||||||
|
p = p.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
// Special cases for common text files that might not be detected as text/
|
// Remove leading slash (means anchored to root)
|
||||||
const commonTextTypes = [
|
const anchored = p.startsWith("/");
|
||||||
"application/json",
|
if (anchored) p = p.slice(1);
|
||||||
"application/javascript",
|
|
||||||
"application/typescript",
|
|
||||||
"application/xml",
|
|
||||||
"application/yaml",
|
|
||||||
"application/x-yaml",
|
|
||||||
];
|
|
||||||
|
|
||||||
return commonTextTypes.includes(mimeType);
|
// Match - either at any level or anchored
|
||||||
|
const matchPattern = anchored ? p : "**/" + p;
|
||||||
|
const matches = minimatch(pathWithoutSlash, matchPattern, { dot: true });
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
ignored = !negated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk directory tree respecting .gitignore, similar to fd
|
||||||
|
function walkDirectory(
|
||||||
|
baseDir: string,
|
||||||
|
query: string,
|
||||||
|
maxResults: number,
|
||||||
|
): Array<{ path: string; isDirectory: boolean }> {
|
||||||
|
const results: Array<{ path: string; isDirectory: boolean }> = [];
|
||||||
|
const rootIgnorePatterns = parseIgnoreFile(join(baseDir, ".gitignore"));
|
||||||
|
|
||||||
|
function walk(currentDir: string, ignorePatterns: string[]): void {
|
||||||
|
if (results.length >= maxResults) return;
|
||||||
|
|
||||||
|
// Load local .gitignore if exists
|
||||||
|
const localPatterns = parseIgnoreFile(join(currentDir, ".gitignore"));
|
||||||
|
const combinedPatterns = [...ignorePatterns, ...localPatterns];
|
||||||
|
|
||||||
|
let entries: Dirent[];
|
||||||
|
try {
|
||||||
|
entries = readdirSync(currentDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return; // Can't read directory, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (results.length >= maxResults) return;
|
||||||
|
|
||||||
|
// Skip hidden files/dirs
|
||||||
|
if (entry.name.startsWith(".")) continue;
|
||||||
|
|
||||||
|
const fullPath = join(currentDir, entry.name);
|
||||||
|
const relativePath = relative(baseDir, fullPath);
|
||||||
|
|
||||||
|
// Check if ignored
|
||||||
|
const pathToCheck = entry.isDirectory() ? relativePath + "/" : relativePath;
|
||||||
|
if (isIgnored(pathToCheck, combinedPatterns)) continue;
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// Check if dir matches query
|
||||||
|
if (!query || entry.name.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
results.push({ path: relativePath + "/", isDirectory: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse
|
||||||
|
walk(fullPath, combinedPatterns);
|
||||||
|
} else {
|
||||||
|
// Check if file matches query
|
||||||
|
if (!query || entry.name.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
results.push({ path: relativePath, isDirectory: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(baseDir, rootIgnorePatterns);
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutocompleteItem {
|
export interface AutocompleteItem {
|
||||||
|
|
@ -131,7 +153,6 @@ export interface AutocompleteProvider {
|
||||||
export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
||||||
private commands: (SlashCommand | AutocompleteItem)[];
|
private commands: (SlashCommand | AutocompleteItem)[];
|
||||||
private basePath: string;
|
private basePath: string;
|
||||||
private fdCommand: string | null | undefined = undefined; // undefined = not checked yet
|
|
||||||
|
|
||||||
constructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd()) {
|
constructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd()) {
|
||||||
this.commands = commands;
|
this.commands = commands;
|
||||||
|
|
@ -398,82 +419,71 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
||||||
searchPrefix = file;
|
searchPrefix = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = readdirSync(searchDir);
|
const entries = readdirSync(searchDir, { withFileTypes: true });
|
||||||
const suggestions: AutocompleteItem[] = [];
|
const suggestions: AutocompleteItem[] = [];
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.toLowerCase().startsWith(searchPrefix.toLowerCase())) {
|
if (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = join(searchDir, entry);
|
const isDirectory = entry.isDirectory();
|
||||||
let isDirectory: boolean;
|
|
||||||
try {
|
|
||||||
isDirectory = statSync(fullPath).isDirectory();
|
|
||||||
} catch (e) {
|
|
||||||
// Skip files we can't stat (permission issues, broken symlinks, etc.)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For @ prefix, filter to only show directories and attachable files
|
|
||||||
if (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let relativePath: string;
|
let relativePath: string;
|
||||||
|
const name = entry.name;
|
||||||
|
|
||||||
// Handle @ prefix path construction
|
// Handle @ prefix path construction
|
||||||
if (isAtPrefix) {
|
if (isAtPrefix) {
|
||||||
const pathWithoutAt = expandedPrefix;
|
const pathWithoutAt = expandedPrefix;
|
||||||
if (pathWithoutAt.endsWith("/")) {
|
if (pathWithoutAt.endsWith("/")) {
|
||||||
relativePath = "@" + pathWithoutAt + entry;
|
relativePath = "@" + pathWithoutAt + name;
|
||||||
} else if (pathWithoutAt.includes("/")) {
|
} else if (pathWithoutAt.includes("/")) {
|
||||||
if (pathWithoutAt.startsWith("~/")) {
|
if (pathWithoutAt.startsWith("~/")) {
|
||||||
const homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/
|
const homeRelativeDir = pathWithoutAt.slice(2); // Remove ~/
|
||||||
const dir = dirname(homeRelativeDir);
|
const dir = dirname(homeRelativeDir);
|
||||||
relativePath = "@~/" + (dir === "." ? entry : join(dir, entry));
|
relativePath = "@~/" + (dir === "." ? name : join(dir, name));
|
||||||
} else {
|
} else {
|
||||||
relativePath = "@" + join(dirname(pathWithoutAt), entry);
|
relativePath = "@" + join(dirname(pathWithoutAt), name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (pathWithoutAt.startsWith("~")) {
|
if (pathWithoutAt.startsWith("~")) {
|
||||||
relativePath = "@~/" + entry;
|
relativePath = "@~/" + name;
|
||||||
} else {
|
} else {
|
||||||
relativePath = "@" + entry;
|
relativePath = "@" + name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (prefix.endsWith("/")) {
|
} else if (prefix.endsWith("/")) {
|
||||||
// If prefix ends with /, append entry to the prefix
|
// If prefix ends with /, append entry to the prefix
|
||||||
relativePath = prefix + entry;
|
relativePath = prefix + name;
|
||||||
} else if (prefix.includes("/")) {
|
} else if (prefix.includes("/")) {
|
||||||
// Preserve ~/ format for home directory paths
|
// Preserve ~/ format for home directory paths
|
||||||
if (prefix.startsWith("~/")) {
|
if (prefix.startsWith("~/")) {
|
||||||
const homeRelativeDir = prefix.slice(2); // Remove ~/
|
const homeRelativeDir = prefix.slice(2); // Remove ~/
|
||||||
const dir = dirname(homeRelativeDir);
|
const dir = dirname(homeRelativeDir);
|
||||||
relativePath = "~/" + (dir === "." ? entry : join(dir, entry));
|
relativePath = "~/" + (dir === "." ? name : join(dir, name));
|
||||||
} else if (prefix.startsWith("/")) {
|
} else if (prefix.startsWith("/")) {
|
||||||
// Absolute path - construct properly
|
// Absolute path - construct properly
|
||||||
const dir = dirname(prefix);
|
const dir = dirname(prefix);
|
||||||
if (dir === "/") {
|
if (dir === "/") {
|
||||||
relativePath = "/" + entry;
|
relativePath = "/" + name;
|
||||||
} else {
|
} else {
|
||||||
relativePath = dir + "/" + entry;
|
relativePath = dir + "/" + name;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
relativePath = join(dirname(prefix), entry);
|
relativePath = join(dirname(prefix), name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For standalone entries, preserve ~/ if original prefix was ~/
|
// For standalone entries, preserve ~/ if original prefix was ~/
|
||||||
if (prefix.startsWith("~")) {
|
if (prefix.startsWith("~")) {
|
||||||
relativePath = "~/" + entry;
|
relativePath = "~/" + name;
|
||||||
} else {
|
} else {
|
||||||
relativePath = entry;
|
relativePath = name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suggestions.push({
|
suggestions.push({
|
||||||
value: isDirectory ? relativePath + "/" : relativePath,
|
value: isDirectory ? relativePath + "/" : relativePath,
|
||||||
label: entry,
|
label: name,
|
||||||
description: isDirectory ? "directory" : "file",
|
description: isDirectory ? "directory" : "file",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -518,98 +528,18 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fuzzy file search using fdfind, fd, or find (fallback)
|
// Fuzzy file search using pure Node.js directory walking (respects .gitignore)
|
||||||
private getFuzzyFileSuggestions(query: string): AutocompleteItem[] {
|
private getFuzzyFileSuggestions(query: string): AutocompleteItem[] {
|
||||||
try {
|
try {
|
||||||
let result: string;
|
const entries = walkDirectory(this.basePath, query, 100);
|
||||||
const fdCommand = this.getFdCommand();
|
|
||||||
|
|
||||||
if (fdCommand) {
|
// Score entries
|
||||||
const args = ["--max-results", "100"];
|
const scoredEntries = entries
|
||||||
|
.map((entry) => ({
|
||||||
if (query) {
|
...entry,
|
||||||
args.push(query);
|
score: query ? this.scoreEntry(entry.path, query, entry.isDirectory) : 1,
|
||||||
}
|
}))
|
||||||
|
.filter((entry) => entry.score > 0);
|
||||||
result = execSync(`${fdCommand} ${args.join(" ")}`, {
|
|
||||||
cwd: this.basePath,
|
|
||||||
encoding: "utf-8",
|
|
||||||
timeout: 2000,
|
|
||||||
maxBuffer: 1024 * 1024,
|
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback to find
|
|
||||||
const pattern = query ? `*${query}*` : "*";
|
|
||||||
|
|
||||||
const cmd = [
|
|
||||||
"find",
|
|
||||||
".",
|
|
||||||
"-iname",
|
|
||||||
`'${pattern}'`,
|
|
||||||
"!",
|
|
||||||
"-path",
|
|
||||||
"'*/.git/*'",
|
|
||||||
"!",
|
|
||||||
"-path",
|
|
||||||
"'*/node_modules/*'",
|
|
||||||
"!",
|
|
||||||
"-path",
|
|
||||||
"'*/__pycache__/*'",
|
|
||||||
"!",
|
|
||||||
"-path",
|
|
||||||
"'*/.venv/*'",
|
|
||||||
"!",
|
|
||||||
"-path",
|
|
||||||
"'*/dist/*'",
|
|
||||||
"!",
|
|
||||||
"-path",
|
|
||||||
"'*/build/*'",
|
|
||||||
"2>/dev/null",
|
|
||||||
"|",
|
|
||||||
"head",
|
|
||||||
"-100",
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
result = execSync(cmd, {
|
|
||||||
cwd: this.basePath,
|
|
||||||
encoding: "utf-8",
|
|
||||||
timeout: 3000,
|
|
||||||
maxBuffer: 1024 * 1024,
|
|
||||||
shell: "/bin/bash",
|
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = result
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.filter((f) => f.length > 0)
|
|
||||||
.map((f) => (f.startsWith("./") ? f.slice(2) : f));
|
|
||||||
|
|
||||||
// Score and filter entries (files and directories)
|
|
||||||
const scoredEntries: { path: string; score: number; isDirectory: boolean }[] = [];
|
|
||||||
|
|
||||||
for (const entryPath of entries) {
|
|
||||||
const fullPath = join(this.basePath, entryPath);
|
|
||||||
|
|
||||||
let isDirectory: boolean;
|
|
||||||
try {
|
|
||||||
isDirectory = statSync(fullPath).isDirectory();
|
|
||||||
} catch {
|
|
||||||
continue; // Skip if we can't stat
|
|
||||||
}
|
|
||||||
|
|
||||||
// For files, check if attachable
|
|
||||||
if (!isDirectory && !isAttachableFile(fullPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const score = query ? this.scoreEntry(entryPath, query, isDirectory) : 1;
|
|
||||||
if (score > 0) {
|
|
||||||
scoredEntries.push({ path: entryPath, score, isDirectory });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by score (descending) and take top 20
|
// Sort by score (descending) and take top 20
|
||||||
scoredEntries.sort((a, b) => b.score - a.score);
|
scoredEntries.sort((a, b) => b.score - a.score);
|
||||||
|
|
@ -618,8 +548,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
||||||
// Build suggestions
|
// Build suggestions
|
||||||
const suggestions: AutocompleteItem[] = [];
|
const suggestions: AutocompleteItem[] = [];
|
||||||
for (const { path: entryPath, isDirectory } of topEntries) {
|
for (const { path: entryPath, isDirectory } of topEntries) {
|
||||||
const entryName = basename(entryPath);
|
const entryName = basename(entryPath.endsWith("/") ? entryPath.slice(0, -1) : entryPath);
|
||||||
// Normalize path - remove trailing slash if present, we'll add it back for dirs
|
|
||||||
const normalizedPath = entryPath.endsWith("/") ? entryPath.slice(0, -1) : entryPath;
|
const normalizedPath = entryPath.endsWith("/") ? entryPath.slice(0, -1) : entryPath;
|
||||||
const valuePath = isDirectory ? normalizedPath + "/" : normalizedPath;
|
const valuePath = isDirectory ? normalizedPath + "/" : normalizedPath;
|
||||||
|
|
||||||
|
|
@ -631,31 +560,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
return suggestions;
|
return suggestions;
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check which fd command is available (fdfind on Debian/Ubuntu, fd elsewhere)
|
|
||||||
// Result is cached after first check
|
|
||||||
private getFdCommand(): string | null {
|
|
||||||
if (this.fdCommand !== undefined) {
|
|
||||||
return this.fdCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
execSync("fdfind --version", { encoding: "utf-8", timeout: 1000, stdio: "pipe" });
|
|
||||||
this.fdCommand = "fdfind";
|
|
||||||
return this.fdCommand;
|
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
return [];
|
||||||
execSync("fd --version", { encoding: "utf-8", timeout: 1000, stdio: "pipe" });
|
|
||||||
this.fdCommand = "fd";
|
|
||||||
return this.fdCommand;
|
|
||||||
} catch {
|
|
||||||
this.fdCommand = null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue