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:
@@ -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
|
||||
`);
|
||||
}
|
||||
Reference in New Issue
Block a user