Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 105 additions & 32 deletions extensions/cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion extensions/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"dependencies": {
"@sentry/profiling-node": "^9.43.0",
"fdir": "^6.4.2",
"find-up": "^8.0.0",
"fzf": "^0.5.2",
"js-yaml": "^4.1.1"
},
Expand Down Expand Up @@ -87,6 +88,7 @@
"@vitest/ui": "^3.2.4",
"@workos-inc/node": "^7.45.0",
"chalk": "^5.4.1",
"clipboardy": "^4.0.0",
"commander": "^14.0.0",
"conventional-changelog-conventionalcommits": "^9.1.0",
"core": "file:../../core",
Expand All @@ -100,7 +102,6 @@
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-unused-imports": "^4.1.4",
"execa": "^9.6.0",
"clipboardy": "^4.0.0",
"express": "^5.1.0",
"glob": "^11.0.3",
"gpt-tokenizer": "^3.0.1",
Expand Down
105 changes: 87 additions & 18 deletions extensions/cli/src/tools/searchCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,28 @@ import * as fs from "fs";
import * as util from "util";

import { ContinueError, ContinueErrorReason } from "core/util/errors.js";
import { findUp } from "find-up";

import { Tool } from "./types.js";

const execPromise = util.promisify(child_process.exec);

// Default maximum number of results to display
const DEFAULT_MAX_RESULTS = 100;
const MAX_LINE_LENGTH = 1000;
async function getGitignorePatterns() {
const gitIgnorePath = await findUp(".gitignore");
if (!gitIgnorePath) return [];
const content = fs.readFileSync(gitIgnorePath, "utf-8");
const ignorePatterns = [];
for (let line of content.trim().split("\n")) {
line = line.trim();
if (line.startsWith("#") || line === "") continue; // ignore comments and empty line
if (line.startsWith("!")) continue; // ignore negated ignores
ignorePatterns.push(line);
}
return ignorePatterns;
}

export async function checkIfRipgrepIsInstalled(): Promise<boolean> {
// procedure 1: search with ripgrep
async function checkIfRipgrepIsInstalled(): Promise<boolean> {
try {
await execPromise("rg --version");
return true;
Expand All @@ -21,6 +33,57 @@ export async function checkIfRipgrepIsInstalled(): Promise<boolean> {
}
}

async function searchWithRipgrep(
pattern: string,
searchPath: string,
filePattern?: string,
) {
let command = `rg --line-number --with-filename --color never "${pattern}"`;

if (filePattern) {
command += ` -g "${filePattern}"`;
}

const ignorePatterns = await getGitignorePatterns();
for (const ignorePattern of ignorePatterns) {
command += ` -g "!${ignorePattern}"`;
}

command += ` "${searchPath}"`;
const { stdout, stderr } = await execPromise(command);
return { stdout, stderr };
}

// procedure 2: search with grep on unix or findstr on windows
async function searchWithGrepOrFindstr(
pattern: string,
searchPath: string,
filePattern?: string,
) {
const isWindows = process.platform === "win32";
const ignorePatterns = await getGitignorePatterns();
let command: string;
if (isWindows) {
const fileSpec = filePattern ? filePattern : "*";
command = `findstr /S /N /P /R "${pattern}" "${fileSpec}"`; // findstr does not support ignoring patterns
} else {
let excludeArgs = "";
for (const ignorePattern of ignorePatterns) {
excludeArgs += ` --exclude="${ignorePattern}" --exclude-dir="${ignorePattern}"`; // use both exclude and exclude-dir because ignorePattern can be a file or directory
}
if (filePattern) {
command = `find . -type f -path "${filePattern}" -print0 | xargs -0 grep -nH -I${excludeArgs} "${pattern}"`;
} else {
command = `grep -R -n -H -I${excludeArgs} "${pattern}" .`;
}
}
return await execPromise(command, { cwd: searchPath });
}

// Default maximum number of results to display
const DEFAULT_MAX_RESULTS = 100;
const MAX_LINE_LENGTH = 1000;

export const searchCodeTool: Tool = {
name: "Search",
displayName: "Search",
Expand Down Expand Up @@ -73,15 +136,26 @@ export const searchCodeTool: Tool = {
);
}

let command = `rg --line-number --with-filename --color never "${args.pattern}"`;

if (args.file_pattern) {
command += ` -g "${args.file_pattern}"`;
}

command += ` "${searchPath}"`;
let stdout = "",
stderr = "";
try {
const { stdout, stderr } = await execPromise(command);
if (await checkIfRipgrepIsInstalled()) {
const results = await searchWithRipgrep(
args.pattern,
searchPath,
args.file_pattern,
);
stdout = results.stdout;
stderr = results.stderr;
} else {
const results = await searchWithGrepOrFindstr(
args.pattern,
searchPath,
args.file_pattern,
);
stdout = results.stdout;
stderr = results.stderr;
}

if (stderr) {
return `Warning during search: ${stderr}\n\n${stdout}`;
Expand Down Expand Up @@ -121,13 +195,8 @@ export const searchCodeTool: Tool = {
args.file_pattern ? ` in files matching "${args.file_pattern}"` : ""
}.`;
}
if (error instanceof Error) {
if (error.message.includes("command not found")) {
throw new Error(`ripgrep is not installed.`);
}
}
throw new Error(
`Error executing ripgrep: ${
`Error executing search: ${
error instanceof Error ? error.message : String(error)
}`,
);
Expand Down
Loading