Skip to content

Commit 9a333f5

Browse files
uinstinctRomneyDa
andauthored
feat(cli): add grep/find/findstr as alternative search strategy (#8616)
* feat(cli): add grep/find/findstr as alternative search strategy * use -path instead of -name * add dependent packages * fix export * include negated patterns * Revert "include negated patterns" This reverts commit 53e16aa. --------- Co-authored-by: Dallin Romney <dallinromney@gmail.com>
1 parent fc5606c commit 9a333f5

File tree

3 files changed

+193
-50
lines changed

3 files changed

+193
-50
lines changed

extensions/cli/package-lock.json

Lines changed: 105 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"dependencies": {
4747
"@sentry/profiling-node": "^9.43.0",
4848
"fdir": "^6.4.2",
49+
"find-up": "^8.0.0",
4950
"fzf": "^0.5.2",
5051
"js-yaml": "^4.1.1"
5152
},
@@ -87,6 +88,7 @@
8788
"@vitest/ui": "^3.2.4",
8889
"@workos-inc/node": "^7.45.0",
8990
"chalk": "^5.4.1",
91+
"clipboardy": "^4.0.0",
9092
"commander": "^14.0.0",
9193
"conventional-changelog-conventionalcommits": "^9.1.0",
9294
"core": "file:../../core",
@@ -100,7 +102,6 @@
100102
"eslint-plugin-import": "^2.32.0",
101103
"eslint-plugin-unused-imports": "^4.1.4",
102104
"execa": "^9.6.0",
103-
"clipboardy": "^4.0.0",
104105
"express": "^5.1.0",
105106
"glob": "^11.0.3",
106107
"gpt-tokenizer": "^3.0.1",

extensions/cli/src/tools/searchCode.ts

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,27 @@ import * as fs from "fs";
33
import * as util from "util";
44

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

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

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

11-
// Default maximum number of results to display
12-
const DEFAULT_MAX_RESULTS = 100;
13-
const MAX_LINE_LENGTH = 1000;
12+
async function getGitignorePatterns() {
13+
const gitIgnorePath = await findUp(".gitignore");
14+
if (!gitIgnorePath) return [];
15+
const content = fs.readFileSync(gitIgnorePath, "utf-8");
16+
const ignorePatterns = [];
17+
for (let line of content.trim().split("\n")) {
18+
line = line.trim();
19+
if (line.startsWith("#") || line === "") continue; // ignore comments and empty line
20+
if (line.startsWith("!")) continue; // ignore negated ignores
21+
ignorePatterns.push(line);
22+
}
23+
return ignorePatterns;
24+
}
1425

26+
// procedure 1: search with ripgrep
1527
export async function checkIfRipgrepIsInstalled(): Promise<boolean> {
1628
try {
1729
await execPromise("rg --version");
@@ -21,6 +33,57 @@ export async function checkIfRipgrepIsInstalled(): Promise<boolean> {
2133
}
2234
}
2335

36+
async function searchWithRipgrep(
37+
pattern: string,
38+
searchPath: string,
39+
filePattern?: string,
40+
) {
41+
let command = `rg --line-number --with-filename --color never "${pattern}"`;
42+
43+
if (filePattern) {
44+
command += ` -g "${filePattern}"`;
45+
}
46+
47+
const ignorePatterns = await getGitignorePatterns();
48+
for (const ignorePattern of ignorePatterns) {
49+
command += ` -g "!${ignorePattern}"`;
50+
}
51+
52+
command += ` "${searchPath}"`;
53+
const { stdout, stderr } = await execPromise(command);
54+
return { stdout, stderr };
55+
}
56+
57+
// procedure 2: search with grep on unix or findstr on windows
58+
async function searchWithGrepOrFindstr(
59+
pattern: string,
60+
searchPath: string,
61+
filePattern?: string,
62+
) {
63+
const isWindows = process.platform === "win32";
64+
const ignorePatterns = await getGitignorePatterns();
65+
let command: string;
66+
if (isWindows) {
67+
const fileSpec = filePattern ? filePattern : "*";
68+
command = `findstr /S /N /P /R "${pattern}" "${fileSpec}"`; // findstr does not support ignoring patterns
69+
} else {
70+
let excludeArgs = "";
71+
for (const ignorePattern of ignorePatterns) {
72+
excludeArgs += ` --exclude="${ignorePattern}" --exclude-dir="${ignorePattern}"`; // use both exclude and exclude-dir because ignorePattern can be a file or directory
73+
}
74+
if (filePattern) {
75+
command = `find . -type f -path "${filePattern}" -print0 | xargs -0 grep -nH -I${excludeArgs} "${pattern}"`;
76+
} else {
77+
command = `grep -R -n -H -I${excludeArgs} "${pattern}" .`;
78+
}
79+
}
80+
return await execPromise(command, { cwd: searchPath });
81+
}
82+
83+
// Default maximum number of results to display
84+
const DEFAULT_MAX_RESULTS = 100;
85+
const MAX_LINE_LENGTH = 1000;
86+
2487
export const searchCodeTool: Tool = {
2588
name: "Search",
2689
displayName: "Search",
@@ -73,15 +136,26 @@ export const searchCodeTool: Tool = {
73136
);
74137
}
75138

76-
let command = `rg --line-number --with-filename --color never "${args.pattern}"`;
77-
78-
if (args.file_pattern) {
79-
command += ` -g "${args.file_pattern}"`;
80-
}
81-
82-
command += ` "${searchPath}"`;
139+
let stdout = "",
140+
stderr = "";
83141
try {
84-
const { stdout, stderr } = await execPromise(command);
142+
if (await checkIfRipgrepIsInstalled()) {
143+
const results = await searchWithRipgrep(
144+
args.pattern,
145+
searchPath,
146+
args.file_pattern,
147+
);
148+
stdout = results.stdout;
149+
stderr = results.stderr;
150+
} else {
151+
const results = await searchWithGrepOrFindstr(
152+
args.pattern,
153+
searchPath,
154+
args.file_pattern,
155+
);
156+
stdout = results.stdout;
157+
stderr = results.stderr;
158+
}
85159

86160
if (stderr) {
87161
return `Warning during search: ${stderr}\n\n${stdout}`;
@@ -121,13 +195,8 @@ export const searchCodeTool: Tool = {
121195
args.file_pattern ? ` in files matching "${args.file_pattern}"` : ""
122196
}.`;
123197
}
124-
if (error instanceof Error) {
125-
if (error.message.includes("command not found")) {
126-
throw new Error(`ripgrep is not installed.`);
127-
}
128-
}
129198
throw new Error(
130-
`Error executing ripgrep: ${
199+
`Error executing search: ${
131200
error instanceof Error ? error.message : String(error)
132201
}`,
133202
);

0 commit comments

Comments
 (0)