feat: add global workspace search with regex and file filtering
This commit is contained in:
@@ -1,8 +1,32 @@
|
||||
import fs from "fs/promises";
|
||||
import { createReadStream } from "fs";
|
||||
import path from "path";
|
||||
import readline from "readline";
|
||||
|
||||
const WORKSPACE_PATH = process.env.WORKSPACE_PATH || "/workspace";
|
||||
|
||||
export interface SearchMatch {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
text: string;
|
||||
matchLength: number;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
matches: SearchMatch[];
|
||||
totalMatches: number;
|
||||
filesSearched: number;
|
||||
}
|
||||
|
||||
interface SearchOptions {
|
||||
regex?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
includePattern?: string;
|
||||
excludePattern?: string;
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
export interface FileNode {
|
||||
name: string;
|
||||
path: string;
|
||||
@@ -74,6 +98,111 @@ export async function deleteFile(
|
||||
await fs.unlink(fullPath);
|
||||
}
|
||||
|
||||
const BINARY_EXTENSIONS = new Set([
|
||||
".mod", ".hak", ".bic", ".tlk", ".png", ".jpg", ".tga",
|
||||
".wav", ".bmu", ".ncs", ".gic",
|
||||
]);
|
||||
|
||||
function matchesExtPattern(filePath: string, pattern: string): boolean {
|
||||
if (pattern.startsWith("*.")) {
|
||||
return filePath.endsWith(pattern.slice(1));
|
||||
}
|
||||
return filePath.endsWith(pattern);
|
||||
}
|
||||
|
||||
async function walkFiles(dir: string, relativeTo: string): Promise<string[]> {
|
||||
const results: string[] = [];
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (IGNORED_DIRS.has(entry.name)) continue;
|
||||
results.push(...(await walkFiles(fullPath, relativeTo)));
|
||||
} else {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!BINARY_EXTENSIONS.has(ext)) {
|
||||
results.push(path.relative(relativeTo, fullPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function searchFiles(
|
||||
repo: string,
|
||||
query: string,
|
||||
options: SearchOptions = {},
|
||||
): Promise<SearchResult> {
|
||||
const maxResults = options.maxResults ?? 1000;
|
||||
const repoRoot = resolveRepoPath(repo, "");
|
||||
const files = await walkFiles(repoRoot, repoRoot);
|
||||
|
||||
const matches: SearchMatch[] = [];
|
||||
let filesSearched = 0;
|
||||
|
||||
let pattern: RegExp | null = null;
|
||||
if (options.regex) {
|
||||
pattern = new RegExp(query, options.caseSensitive ? "g" : "gi");
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
if (matches.length >= maxResults) break;
|
||||
|
||||
if (options.includePattern && !matchesExtPattern(file, options.includePattern)) continue;
|
||||
if (options.excludePattern && matchesExtPattern(file, options.excludePattern)) continue;
|
||||
|
||||
const fullPath = path.join(repoRoot, file);
|
||||
filesSearched++;
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: createReadStream(fullPath, { encoding: "utf-8" }),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let lineNum = 0;
|
||||
for await (const line of rl) {
|
||||
lineNum++;
|
||||
if (matches.length >= maxResults) {
|
||||
rl.close();
|
||||
break;
|
||||
}
|
||||
|
||||
if (pattern) {
|
||||
pattern.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = pattern.exec(line)) !== null) {
|
||||
matches.push({
|
||||
file,
|
||||
line: lineNum,
|
||||
column: m.index,
|
||||
text: line,
|
||||
matchLength: m[0].length,
|
||||
});
|
||||
if (matches.length >= maxResults) break;
|
||||
}
|
||||
} else {
|
||||
const haystack = options.caseSensitive ? line : line.toLowerCase();
|
||||
const needle = options.caseSensitive ? query : query.toLowerCase();
|
||||
let startIdx = 0;
|
||||
let idx: number;
|
||||
while ((idx = haystack.indexOf(needle, startIdx)) !== -1) {
|
||||
matches.push({
|
||||
file,
|
||||
line: lineNum,
|
||||
column: idx,
|
||||
text: line,
|
||||
matchLength: needle.length,
|
||||
});
|
||||
startIdx = idx + 1;
|
||||
if (matches.length >= maxResults) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { matches, totalMatches: matches.length, filesSearched };
|
||||
}
|
||||
|
||||
function resolveRepoPath(repo: string, filePath: string): string {
|
||||
const repoRoot = path.resolve(path.join(WORKSPACE_PATH, "repos", repo));
|
||||
const resolved = path.resolve(repoRoot, filePath);
|
||||
|
||||
Reference in New Issue
Block a user