-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathindex.ts
More file actions
304 lines (264 loc) · 9.98 KB
/
Copy pathindex.ts
File metadata and controls
304 lines (264 loc) · 9.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import * as childProcess from "child_process"
import * as path from "path"
import * as readline from "readline"
import * as vscode from "vscode"
import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
import { fileExistsAtPath } from "../../utils/fs"
/*
This file provides functionality to perform regex searches on files using ripgrep.
Inspired by: https://github.com/DiscreteTom/vscode-ripgrep-utils
Key components:
1. getBinPath: Locates the ripgrep binary inside the VS Code installation.
2. execRipgrep: Executes the ripgrep command and returns the output.
3. regexSearchFiles: The main function that performs regex searches on files.
- Parameters:
* cwd: The current working directory (for relative path calculation)
* directoryPath: The directory to search in
* regex: The regular expression to search for (Rust regex syntax)
* filePattern: Optional glob pattern to filter files (default: '*')
- Returns: A formatted string containing search results with context
The search results include:
- Relative file paths
- 2 lines of context before and after each match
- Matches formatted with pipe characters for easy reading
Usage example:
const results = await regexSearchFiles('/path/to/cwd', '/path/to/search', 'TODO:', '*.ts');
rel/path/to/app.ts
│----
│function processData(data: any) {
│ // Some processing logic here
│ // TODO: Implement error handling
│ return processedData;
│}
│----
rel/path/to/helper.ts
│----
│ let result = 0;
│ for (let i = 0; i < input; i++) {
│ // TODO: Optimize this function for performance
│ result += Math.pow(i, 2);
│ }
│----
*/
const isWindows = process.platform.startsWith("win")
const binName = isWindows ? "rg.exe" : "rg"
// VS Code's @vscode/ripgrep-universal package (used by recent VS Code builds,
// including the Insiders staged-install layout) nests the binary under
// bin/<platform>-<arch>/ rather than directly in bin/.
const ripgrepUniversalBinDir = `bin/${process.platform}-${process.arch}`
// @vscode/ripgrep >=1.18 ships the binary in a platform-specific optional
// package (e.g. @vscode/ripgrep-win32-x64). Matches the wrapper's own arch
// selection: process.env.npm_config_arch || process.arch.
const platformPkgArch = process.env.npm_config_arch || process.arch
const ripgrepPlatformPkg = `@vscode/ripgrep-${process.platform}-${platformPkgArch}`
interface SearchFileResult {
file: string
searchResults: SearchResult[]
}
interface SearchResult {
lines: SearchLineResult[]
}
interface SearchLineResult {
line: number
text: string
isMatch: boolean
column?: number
}
// Constants
const MAX_RESULTS = 300
const MAX_LINE_LENGTH = 500
/**
* Truncates a line if it exceeds the maximum length
* @param line The line to truncate
* @param maxLength The maximum allowed length (defaults to MAX_LINE_LENGTH)
* @returns The truncated line, or the original line if it's shorter than maxLength
*/
export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string {
return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line
}
/**
* Returns the ordered list of absolute candidate paths where ripgrep may
* live under the given VS Code appRoot. Used by both getBinPath (first-match
* resolution) and the diagnostic command (existence report for all paths).
*/
export function ripgrepCandidatePaths(vscodeAppRoot: string): readonly string[] {
return [
path.join(vscodeAppRoot, "node_modules/@vscode/ripgrep/bin/", binName),
path.join(vscodeAppRoot, "node_modules/vscode-ripgrep/bin", binName),
path.join(vscodeAppRoot, "node_modules.asar.unpacked/vscode-ripgrep/bin/", binName),
path.join(vscodeAppRoot, "node_modules.asar.unpacked/@vscode/ripgrep/bin/", binName),
path.join(vscodeAppRoot, `node_modules/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`, binName),
path.join(
vscodeAppRoot,
`node_modules.asar.unpacked/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`,
binName,
),
// @vscode/ripgrep >=1.18 (VS Code 1.130+): binary lives in a platform-specific optional package.
path.join(vscodeAppRoot, `node_modules/${ripgrepPlatformPkg}/bin`, binName),
path.join(vscodeAppRoot, `node_modules.asar.unpacked/${ripgrepPlatformPkg}/bin`, binName),
]
}
/**
* Get the path to the ripgrep binary shipped inside the VS Code installation.
*
* Probes all known layouts: classic @vscode/ripgrep, @vscode/ripgrep-universal
* (VS Code Insiders staged-install), and the @vscode/ripgrep >=1.18
* platform-package layout used by VS Code 1.130+.
*
* Returns `undefined` when ripgrep cannot be located.
*/
export async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
for (const candidate of ripgrepCandidatePaths(vscodeAppRoot)) {
if (await fileExistsAtPath(candidate)) return candidate
}
return undefined
}
async function execRipgrep(bin: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
const rgProcess = childProcess.spawn(bin, args)
// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.
const rl = readline.createInterface({
input: rgProcess.stdout,
crlfDelay: Infinity, // treat \r\n as a single line break even if it's split across chunks. This ensures consistent behavior across different operating systems.
})
let output = ""
let lineCount = 0
const maxLines = MAX_RESULTS * 5 // limiting ripgrep output with max lines since there's no other way to limit results. it's okay that we're outputting as json, since we're parsing it line by line and ignore anything that's not part of a match. This assumes each result is at most 5 lines.
rl.on("line", (line) => {
if (lineCount < maxLines) {
output += line + "\n"
lineCount++
} else {
rl.close()
rgProcess.kill()
}
})
let errorOutput = ""
rgProcess.stderr.on("data", (data) => {
errorOutput += data.toString()
})
rl.on("close", () => {
if (errorOutput) {
reject(new Error(`ripgrep process error: ${errorOutput}`))
} else {
resolve(output)
}
})
rgProcess.on("error", (error) => {
reject(new Error(`ripgrep process error: ${error.message}`))
})
})
}
export async function regexSearchFiles(
cwd: string,
directoryPath: string,
regex: string,
filePattern?: string,
rooIgnoreController?: RooIgnoreController,
): Promise<string> {
const vscodeAppRoot = vscode.env.appRoot
const rgPath = await getBinPath(vscodeAppRoot)
if (!rgPath) {
throw new Error("Could not find ripgrep binary")
}
const args = ["--json", "-e", regex]
// Only add --glob if a specific file pattern is provided
// Using --glob "*" overrides .gitignore behavior, so we omit it when no pattern is specified
if (filePattern) {
args.push("--glob", filePattern)
}
args.push("--context", "1", "--no-messages", directoryPath)
let output: string
try {
output = await execRipgrep(rgPath, args)
} catch (error) {
console.error("Error executing ripgrep:", error)
return "No results found"
}
const results: SearchFileResult[] = []
let currentFile: SearchFileResult | null = null
output.split("\n").forEach((line) => {
if (line) {
try {
const parsed = JSON.parse(line)
if (parsed.type === "begin") {
currentFile = {
file: parsed.data.path.text.toString(),
searchResults: [],
}
} else if (parsed.type === "end") {
// Reset the current result when a new file is encountered
results.push(currentFile as SearchFileResult)
currentFile = null
} else if ((parsed.type === "match" || parsed.type === "context") && currentFile) {
const line = {
line: parsed.data.line_number,
text: truncateLine(parsed.data.lines.text),
isMatch: parsed.type === "match",
...(parsed.type === "match" && { column: parsed.data.absolute_offset }),
}
const lastResult = currentFile.searchResults[currentFile.searchResults.length - 1]
if (lastResult?.lines.length > 0) {
const lastLine = lastResult.lines[lastResult.lines.length - 1]
// If this line is contiguous with the last result, add to it
if (parsed.data.line_number <= lastLine.line + 1) {
lastResult.lines.push(line)
} else {
// Otherwise create a new result
currentFile.searchResults.push({
lines: [line],
})
}
} else {
// First line in file
currentFile.searchResults.push({
lines: [line],
})
}
}
} catch (error) {
console.error("Error parsing ripgrep output:", error)
}
}
})
// console.log(results)
// Filter results using RooIgnoreController if provided
const filteredResults = rooIgnoreController
? results.filter((result) => rooIgnoreController.validateAccess(result.file))
: results
return formatResults(filteredResults, cwd)
}
function formatResults(fileResults: SearchFileResult[], cwd: string): string {
const groupedResults: { [key: string]: SearchResult[] } = {}
const totalResults = fileResults.reduce((sum, file) => sum + file.searchResults.length, 0)
let output = ""
if (totalResults >= MAX_RESULTS) {
output += `Showing first ${MAX_RESULTS} of ${MAX_RESULTS}+ results. Use a more specific search if necessary.\n\n`
} else {
output += `Found ${totalResults === 1 ? "1 result" : `${totalResults.toLocaleString()} results`}.\n\n`
}
// Group results by file name
fileResults.slice(0, MAX_RESULTS).forEach((file) => {
const relativeFilePath = path.relative(cwd, file.file)
if (!groupedResults[relativeFilePath]) {
groupedResults[relativeFilePath] = []
groupedResults[relativeFilePath].push(...file.searchResults)
}
})
for (const [filePath, fileResults] of Object.entries(groupedResults)) {
output += `# ${filePath.toPosix()}\n`
fileResults.forEach((result) => {
// Only show results with at least one line
if (result.lines.length > 0) {
// Show all lines in the result
result.lines.forEach((line) => {
const lineNumber = String(line.line).padStart(3, " ")
output += `${lineNumber} | ${line.text.trimEnd()}\n`
})
output += "----\n"
}
})
output += "\n"
}
return output.trim()
}