Skip to content

Commit 051b67b

Browse files
committed
fix: resolve ripgrep from @vscode/ripgrep-universal and the system PATH
search_files and list_files threw "Could not find ripgrep binary" on VS Code Insiders, whose staged-install builds ship ripgrep as @vscode/ripgrep-universal with the binary nested under bin/<platform>-<arch>/ — a layout getBinPath did not recognize. getBinPath now also checks that layout, and falls back to ripgrep on the system PATH when no copy is found in the VS Code install (covering VS Code forks and headless/CLI hosts).
1 parent 166bc3f commit 051b67b

2 files changed

Lines changed: 120 additions & 4 deletions

File tree

src/services/ripgrep/__tests__/index.spec.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
// npx vitest run src/services/ripgrep/__tests__/index.spec.ts
22

3-
import { truncateLine } from "../index"
3+
import path from "path"
4+
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"
5+
6+
import { truncateLine, getBinPath } from "../index"
7+
import { fileExistsAtPath } from "../../../utils/fs"
8+
9+
vi.mock("../../../utils/fs", () => ({
10+
fileExistsAtPath: vi.fn(),
11+
}))
12+
13+
const mockFileExists = vi.mocked(fileExistsAtPath)
414

515
describe("Ripgrep line truncation", () => {
616
// The default MAX_LINE_LENGTH is 500 in the implementation
@@ -48,3 +58,61 @@ describe("Ripgrep line truncation", () => {
4858
expect(truncated).toContain("[truncated...]")
4959
})
5060
})
61+
62+
describe("getBinPath", () => {
63+
const appRoot = "/fake/vscode/appRoot"
64+
const binName = process.platform.startsWith("win") ? "rg.exe" : "rg"
65+
const platformDir = `${process.platform}-${process.arch}`
66+
const originalPath = process.env.PATH
67+
68+
beforeEach(() => {
69+
mockFileExists.mockReset()
70+
mockFileExists.mockResolvedValue(false)
71+
})
72+
73+
afterEach(() => {
74+
if (originalPath === undefined) {
75+
delete process.env.PATH
76+
} else {
77+
process.env.PATH = originalPath
78+
}
79+
})
80+
81+
it("resolves ripgrep from the classic @vscode/ripgrep layout", async () => {
82+
const rg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName)
83+
mockFileExists.mockImplementation(async (p: string) => p === rg)
84+
85+
expect(await getBinPath(appRoot)).toBe(rg)
86+
})
87+
88+
it("resolves ripgrep from the @vscode/ripgrep-universal layout (VS Code Insiders)", async () => {
89+
const rg = path.join(appRoot, "node_modules/@vscode/ripgrep-universal/bin", platformDir, binName)
90+
mockFileExists.mockImplementation(async (p: string) => p === rg)
91+
92+
expect(await getBinPath(appRoot)).toBe(rg)
93+
})
94+
95+
it("falls back to ripgrep on the system PATH when the VS Code copy is absent", async () => {
96+
process.env.PATH = ["/fake/empty", "/fake/tools"].join(path.delimiter)
97+
const rg = path.join("/fake/tools", binName)
98+
mockFileExists.mockImplementation(async (p: string) => p === rg)
99+
100+
expect(await getBinPath(appRoot)).toBe(rg)
101+
})
102+
103+
it("prefers the VS Code copy over the system PATH", async () => {
104+
process.env.PATH = "/fake/tools"
105+
const vscodeRg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName)
106+
const pathRg = path.join("/fake/tools", binName)
107+
mockFileExists.mockImplementation(async (p: string) => p === vscodeRg || p === pathRg)
108+
109+
expect(await getBinPath(appRoot)).toBe(vscodeRg)
110+
})
111+
112+
it("returns undefined when ripgrep cannot be found anywhere", async () => {
113+
process.env.PATH = "/fake/empty"
114+
mockFileExists.mockResolvedValue(false)
115+
116+
expect(await getBinPath(appRoot)).toBeUndefined()
117+
})
118+
})

src/services/ripgrep/index.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This file provides functionality to perform regex searches on files using ripgre
1111
Inspired by: https://github.com/DiscreteTom/vscode-ripgrep-utils
1212
1313
Key components:
14-
1. getBinPath: Locates the ripgrep binary within the VSCode installation.
14+
1. getBinPath: Locates the ripgrep binary (in the VS Code install, or on the system PATH).
1515
2. execRipgrep: Executes the ripgrep command and returns the output.
1616
3. regexSearchFiles: The main function that performs regex searches on files.
1717
- Parameters:
@@ -51,6 +51,11 @@ rel/path/to/helper.ts
5151
const isWindows = process.platform.startsWith("win")
5252
const binName = isWindows ? "rg.exe" : "rg"
5353

54+
// VS Code's @vscode/ripgrep-universal package (used by recent VS Code builds,
55+
// including the Insiders staged-install layout) nests the binary under
56+
// bin/<platform>-<arch>/ rather than directly in bin/.
57+
const ripgrepUniversalBinDir = `bin/${process.platform}-${process.arch}`
58+
5459
interface SearchFileResult {
5560
file: string
5661
searchResults: SearchResult[]
@@ -80,7 +85,47 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH):
8085
return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line
8186
}
8287
/**
83-
* Get the path to the ripgrep binary within the VSCode installation
88+
* Look up the ripgrep binary on the system PATH.
89+
*
90+
* Used as a fallback after the VS Code installation has been checked. Covers
91+
* VS Code forks whose install layout is not recognized by getBinPath, headless
92+
* / CLI hosts with no VS Code installation, and machines where the user has
93+
* installed ripgrep themselves.
94+
*/
95+
async function findRipgrepOnPath(): Promise<string | undefined> {
96+
const pathEnv = process.env.PATH
97+
98+
if (!pathEnv) {
99+
return undefined
100+
}
101+
102+
for (const dir of pathEnv.split(path.delimiter)) {
103+
if (dir.length === 0) {
104+
continue
105+
}
106+
107+
const candidate = path.join(dir, binName)
108+
109+
if (await fileExistsAtPath(candidate)) {
110+
return candidate
111+
}
112+
}
113+
114+
return undefined
115+
}
116+
117+
/**
118+
* Get the path to the ripgrep binary.
119+
*
120+
* Resolution order:
121+
* 1. ripgrep shipped inside the VS Code installation. Both the long-standing
122+
* `@vscode/ripgrep` layout and the newer `@vscode/ripgrep-universal`
123+
* layout are checked — the latter is what VS Code Insiders' staged-install
124+
* builds use (see microsoft/vscode#252063).
125+
* 2. ripgrep on the system PATH — covers VS Code forks with an unrecognized
126+
* install layout, headless / CLI hosts, and a user-installed ripgrep.
127+
*
128+
* Returns `undefined` when ripgrep cannot be located anywhere.
84129
*/
85130
export async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
86131
const checkPath = async (pkgFolder: string) => {
@@ -92,7 +137,10 @@ export async function getBinPath(vscodeAppRoot: string): Promise<string | undefi
92137
(await checkPath("node_modules/@vscode/ripgrep/bin/")) ||
93138
(await checkPath("node_modules/vscode-ripgrep/bin")) ||
94139
(await checkPath("node_modules.asar.unpacked/vscode-ripgrep/bin/")) ||
95-
(await checkPath("node_modules.asar.unpacked/@vscode/ripgrep/bin/"))
140+
(await checkPath("node_modules.asar.unpacked/@vscode/ripgrep/bin/")) ||
141+
(await checkPath(`node_modules/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`)) ||
142+
(await checkPath(`node_modules.asar.unpacked/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`)) ||
143+
(await findRipgrepOnPath())
96144
)
97145
}
98146

0 commit comments

Comments
 (0)