Skip to content

Commit 06c62c2

Browse files
committed
fix: resolve ripgrep via @vscode/ripgrep import per review feedback
Per #248 review (edelauna): replace the appRoot path-probing with an @vscode/ripgrep import. VS Code's extension host aliases @vscode/ripgrep to its own @vscode/ripgrep-universal (extHostRequireInterceptor.ts L97-101), so ripgrep resolution stays in sync with whatever VS Code ships with — including the Insiders staged-install layout — without maintaining a hardcoded path list across VS Code repackagings. getBinPath shrinks to a try/catch around await import + the .asar → .asar.unpacked substitution from VS Code's own resolver (src/vs/base/node/ripgrep.ts). - @vscode/ripgrep added as a src devDep (types only; the binary is not shipped in the VSIX). - @vscode/ripgrep added to external in src/esbuild.mjs so the require is preserved at runtime for VS Code's interceptor. - Tests cover all four branches: rgPath returned, .asar substitution applies, rgPath undefined, rgPath access throws.
1 parent 2671d1e commit 06c62c2

5 files changed

Lines changed: 51 additions & 49 deletions

File tree

pnpm-lock.yaml

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

src/esbuild.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ async function main() {
126126
// global-agent must be external because it dynamically patches Node.js http/https modules
127127
// which breaks when bundled. It needs access to the actual Node.js module instances.
128128
// undici must be bundled because our VSIX is packaged with `--no-dependencies`.
129-
external: ["vscode", "esbuild", "global-agent"],
129+
external: ["vscode", "esbuild", "global-agent", "@vscode/ripgrep"],
130130
}
131131

132132
/**

src/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@
554554
"@types/tmp": "^0.2.6",
555555
"@types/turndown": "^5.0.5",
556556
"@types/vscode": "^1.84.0",
557+
"@vscode/ripgrep": "^1.17.0",
557558
"@vscode/test-electron": "^2.5.2",
558559
"@vscode/vsce": "3.3.2",
559560
"ai": "^6.0.75",

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

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

3-
import path from "path"
43
import { vi, describe, it, expect, beforeEach } from "vitest"
54

65
import { truncateLine, getBinPath } from "../index"
7-
import { fileExistsAtPath } from "../../../utils/fs"
86

9-
vi.mock("../../../utils/fs", () => ({
10-
fileExistsAtPath: vi.fn(),
11-
}))
7+
const ripgrepMock = vi.hoisted(() => ({ value: undefined as string | undefined, throws: false }))
128

13-
const mockFileExists = vi.mocked(fileExistsAtPath)
9+
vi.mock("@vscode/ripgrep", () => ({
10+
get rgPath() {
11+
if (ripgrepMock.throws) {
12+
throw new Error("simulated @vscode/ripgrep failure")
13+
}
14+
return ripgrepMock.value
15+
},
16+
}))
1417

1518
describe("Ripgrep line truncation", () => {
1619
// The default MAX_LINE_LENGTH is 500 in the implementation
@@ -60,32 +63,34 @@ describe("Ripgrep line truncation", () => {
6063
})
6164

6265
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-
6766
beforeEach(() => {
68-
mockFileExists.mockReset()
69-
mockFileExists.mockResolvedValue(false)
67+
ripgrepMock.value = undefined
68+
ripgrepMock.throws = false
69+
})
70+
71+
it("returns the rgPath exported by @vscode/ripgrep", async () => {
72+
ripgrepMock.value = "/path/to/rg"
73+
74+
expect(await getBinPath("/ignored")).toBe("/path/to/rg")
7075
})
7176

72-
it("resolves ripgrep from the classic @vscode/ripgrep layout", async () => {
73-
const rg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName)
74-
mockFileExists.mockImplementation(async (p: string) => p === rg)
77+
it("rewrites node_modules.asar to node_modules.asar.unpacked", async () => {
78+
ripgrepMock.value = "/app/node_modules.asar/@vscode/ripgrep-universal/bin/win32-x64/rg.exe"
7579

76-
expect(await getBinPath(appRoot)).toBe(rg)
80+
expect(await getBinPath("/ignored")).toBe(
81+
"/app/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/win32-x64/rg.exe",
82+
)
7783
})
7884

79-
it("resolves ripgrep from the @vscode/ripgrep-universal layout (VS Code Insiders)", async () => {
80-
const rg = path.join(appRoot, "node_modules/@vscode/ripgrep-universal/bin", platformDir, binName)
81-
mockFileExists.mockImplementation(async (p: string) => p === rg)
85+
it("returns undefined when rgPath is not exported", async () => {
86+
ripgrepMock.value = undefined
8287

83-
expect(await getBinPath(appRoot)).toBe(rg)
88+
expect(await getBinPath("/ignored")).toBeUndefined()
8489
})
8590

86-
it("returns undefined when ripgrep cannot be found", async () => {
87-
mockFileExists.mockResolvedValue(false)
91+
it("returns undefined when rgPath resolution throws", async () => {
92+
ripgrepMock.throws = true
8893

89-
expect(await getBinPath(appRoot)).toBeUndefined()
94+
expect(await getBinPath("/ignored")).toBeUndefined()
9095
})
9196
})

src/services/ripgrep/index.ts

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ import * as readline from "readline"
55
import * as vscode from "vscode"
66

77
import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
8-
import { fileExistsAtPath } from "../../utils/fs"
98
/*
109
This file provides functionality to perform regex searches on files using ripgrep.
1110
Inspired by: https://github.com/DiscreteTom/vscode-ripgrep-utils
1211
1312
Key components:
14-
1. getBinPath: Locates the ripgrep binary (in the VS Code install, or on the system PATH).
13+
1. getBinPath: Resolves the ripgrep binary via the `@vscode/ripgrep` package — VS Code's require interceptor aliases it to the bundled binary at runtime.
1514
2. execRipgrep: Executes the ripgrep command and returns the output.
1615
3. regexSearchFiles: The main function that performs regex searches on files.
1716
- Parameters:
@@ -51,11 +50,6 @@ rel/path/to/helper.ts
5150
const isWindows = process.platform.startsWith("win")
5251
const binName = isWindows ? "rg.exe" : "rg"
5352

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-
5953
interface SearchFileResult {
6054
file: string
6155
searchResults: SearchResult[]
@@ -85,28 +79,27 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH):
8579
return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line
8680
}
8781
/**
88-
* Get the path to the ripgrep binary shipped inside the VS Code installation.
82+
* Get the path to the ripgrep binary.
8983
*
90-
* Both the long-standing `@vscode/ripgrep` layout and the newer
91-
* `@vscode/ripgrep-universal` layout are checked — the latter is what VS Code
92-
* Insiders' staged-install builds use (see microsoft/vscode#252063).
84+
* Imports `@vscode/ripgrep` and returns its `rgPath` export. In the extension
85+
* host this import is intercepted by VS Code (see microsoft/vscode
86+
* `src/vs/workbench/api/common/extHostRequireInterceptor.ts`) and aliased to
87+
* VS Code's own `@vscode/ripgrep-universal`, so we inherit whatever ripgrep
88+
* VS Code ships with — including the Insiders staged-install layout that the
89+
* old hardcoded path list missed (microsoft/vscode#252063). The
90+
* `node_modules.asar` → `node_modules.asar.unpacked` substitution mirrors
91+
* VS Code's own resolution in `src/vs/base/node/ripgrep.ts`.
9392
*
94-
* Returns `undefined` when ripgrep cannot be located.
93+
* The `vscodeAppRoot` parameter is retained for API stability and ignored.
94+
* Returns `undefined` if `@vscode/ripgrep` is unavailable.
9595
*/
96-
export async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
97-
const checkPath = async (pkgFolder: string) => {
98-
const fullPath = path.join(vscodeAppRoot, pkgFolder, binName)
99-
return (await fileExistsAtPath(fullPath)) ? fullPath : undefined
96+
export async function getBinPath(_vscodeAppRoot: string): Promise<string | undefined> {
97+
try {
98+
const m = await import("@vscode/ripgrep")
99+
return m.rgPath?.replace(/\bnode_modules\.asar\b/, "node_modules.asar.unpacked")
100+
} catch {
101+
return undefined
100102
}
101-
102-
return (
103-
(await checkPath("node_modules/@vscode/ripgrep/bin/")) ||
104-
(await checkPath("node_modules/vscode-ripgrep/bin")) ||
105-
(await checkPath("node_modules.asar.unpacked/vscode-ripgrep/bin/")) ||
106-
(await checkPath("node_modules.asar.unpacked/@vscode/ripgrep/bin/")) ||
107-
(await checkPath(`node_modules/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`)) ||
108-
(await checkPath(`node_modules.asar.unpacked/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`))
109-
)
110103
}
111104

112105
async function execRipgrep(bin: string, args: string[]): Promise<string> {

0 commit comments

Comments
 (0)