feat(grep): added grep CLI, diff highlighting, FileReader abstraction, and markdown improvements

- Added grep CLI subcommand with support for pattern matching, file globbing, context lines, and multiple output modes (content, files-with-matches, count).
- Added diff syntax highlighting support with distinct colors for inserted and deleted lines in code output.
- Implemented FileReader abstraction for cross-platform file reading with mmap support and 4MB size limit.
- Refactored worker pool initialization to support both compiled and development build environments using resolveWorkerSpecifier utility.
- Enhanced markdown rendering with improved inline style context handling and blockquote formatting with text wrapping.
- Replaced external strip-ansi dependency with Bun's built-in stripANSI method across all packages.
This commit is contained in:
can1357
2026-01-31 01:27:36 +01:00
parent d1a18a29e5
commit a07f38b16e
25 changed files with 441 additions and 73 deletions
+152
View File
@@ -0,0 +1,152 @@
/**
* Grep CLI command handlers.
*
* Handles `omp grep` subcommand for testing grep tool on Windows.
*/
import * as path from "node:path";
import { grep } from "@oh-my-pi/pi-natives";
import chalk from "chalk";
import { APP_NAME } from "../config";
export interface GrepCommandArgs {
pattern: string;
path: string;
glob?: string;
limit: number;
context: number;
mode: "content" | "filesWithMatches" | "count";
}
/**
* Parse grep subcommand arguments.
* Returns undefined if not a grep command.
*/
export function parseGrepArgs(args: string[]): GrepCommandArgs | undefined {
if (args.length === 0 || args[0] !== "grep") {
return undefined;
}
const result: GrepCommandArgs = {
pattern: "",
path: ".",
limit: 20,
context: 2,
mode: "content",
};
const positional: string[] = [];
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg === "--glob" || arg === "-g") {
result.glob = args[++i];
} else if (arg === "--limit" || arg === "-l") {
result.limit = parseInt(args[++i], 10);
} else if (arg === "--context" || arg === "-C") {
result.context = parseInt(args[++i], 10);
} else if (arg === "--files" || arg === "-f") {
result.mode = "filesWithMatches";
} else if (arg === "--count" || arg === "-c") {
result.mode = "count";
} else if (!arg.startsWith("-")) {
positional.push(arg);
}
}
if (positional.length >= 1) {
result.pattern = positional[0];
}
if (positional.length >= 2) {
result.path = positional[1];
}
return result;
}
export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
if (!cmd.pattern) {
console.error(chalk.red("Error: Pattern is required"));
process.exit(1);
}
const searchPath = path.resolve(cmd.path);
console.log(chalk.dim(`Searching in: ${searchPath}`));
console.log(chalk.dim(`Pattern: ${cmd.pattern}`));
console.log(chalk.dim(`Mode: ${cmd.mode}, Limit: ${cmd.limit}, Context: ${cmd.context}`));
console.log("");
try {
const result = await grep({
pattern: cmd.pattern,
path: searchPath,
glob: cmd.glob,
mode: cmd.mode,
maxCount: cmd.limit,
context: cmd.mode === "content" ? cmd.context : undefined,
hidden: true,
});
console.log(chalk.green(`Total matches: ${result.totalMatches}`));
console.log(chalk.green(`Files with matches: ${result.filesWithMatches}`));
console.log(chalk.green(`Files searched: ${result.filesSearched}`));
if (result.limitReached) {
console.log(chalk.yellow(`Limit reached: true`));
}
console.log("");
for (const match of result.matches) {
const displayPath = match.path.replace(/\\/g, "/");
if (cmd.mode === "content") {
if (match.contextBefore) {
for (const ctx of match.contextBefore) {
console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
}
}
console.log(`${chalk.cyan(displayPath)}:${chalk.yellow(String(match.lineNumber))}: ${match.line}`);
if (match.contextAfter) {
for (const ctx of match.contextAfter) {
console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
}
}
console.log("");
} else if (cmd.mode === "count") {
console.log(`${chalk.cyan(displayPath)}: ${match.matchCount ?? 0} matches`);
} else {
console.log(chalk.cyan(displayPath));
}
}
} catch (err) {
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
process.exit(1);
}
}
export function printGrepHelp(): void {
console.log(`${chalk.bold(`${APP_NAME} grep`)} - Test grep tool
${chalk.bold("Usage:")}
${APP_NAME} grep <pattern> [path] [options]
${chalk.bold("Arguments:")}
pattern Regex pattern to search for
path Directory or file to search (default: .)
${chalk.bold("Options:")}
-g, --glob <pattern> Filter files by glob pattern
-l, --limit <n> Max matches (default: 20)
-C, --context <n> Context lines (default: 2)
-f, --files Output file names only
-c, --count Output match counts per file
-h, --help Show this help
${chalk.bold("Environment:")}
OMP_GREP_WORKERS=0 Disable worker pool (use single-threaded mode)
${chalk.bold("Examples:")}
${APP_NAME} grep "import" src/
${APP_NAME} grep "TODO" . --glob "*.ts"
${APP_NAME} grep "function" --files
`);
}