From 42e35a6fff0790532918aa508b42ec4120fd7c40 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:44 +0000 Subject: [PATCH 1/6] fix: resolve ripgrep from @vscode/ripgrep-universal and the system PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/-/ — 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). --- src/services/ripgrep/__tests__/index.spec.ts | 70 +++++++++++++++++++- src/services/ripgrep/index.ts | 54 ++++++++++++++- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/services/ripgrep/__tests__/index.spec.ts b/src/services/ripgrep/__tests__/index.spec.ts index 0c4d79f09e..fb4ad2a7af 100644 --- a/src/services/ripgrep/__tests__/index.spec.ts +++ b/src/services/ripgrep/__tests__/index.spec.ts @@ -1,6 +1,16 @@ // npx vitest run src/services/ripgrep/__tests__/index.spec.ts -import { truncateLine } from "../index" +import path from "path" +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest" + +import { truncateLine, getBinPath } from "../index" +import { fileExistsAtPath } from "../../../utils/fs" + +vi.mock("../../../utils/fs", () => ({ + fileExistsAtPath: vi.fn(), +})) + +const mockFileExists = vi.mocked(fileExistsAtPath) describe("Ripgrep line truncation", () => { // The default MAX_LINE_LENGTH is 500 in the implementation @@ -48,3 +58,61 @@ describe("Ripgrep line truncation", () => { expect(truncated).toContain("[truncated...]") }) }) + +describe("getBinPath", () => { + const appRoot = "/fake/vscode/appRoot" + const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" + const platformDir = `${process.platform}-${process.arch}` + const originalPath = process.env.PATH + + beforeEach(() => { + mockFileExists.mockReset() + mockFileExists.mockResolvedValue(false) + }) + + afterEach(() => { + if (originalPath === undefined) { + delete process.env.PATH + } else { + process.env.PATH = originalPath + } + }) + + it("resolves ripgrep from the classic @vscode/ripgrep layout", async () => { + const rg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName) + mockFileExists.mockImplementation(async (p: string) => p === rg) + + expect(await getBinPath(appRoot)).toBe(rg) + }) + + it("resolves ripgrep from the @vscode/ripgrep-universal layout (VS Code Insiders)", async () => { + const rg = path.join(appRoot, "node_modules/@vscode/ripgrep-universal/bin", platformDir, binName) + mockFileExists.mockImplementation(async (p: string) => p === rg) + + expect(await getBinPath(appRoot)).toBe(rg) + }) + + it("falls back to ripgrep on the system PATH when the VS Code copy is absent", async () => { + process.env.PATH = ["/fake/empty", "/fake/tools"].join(path.delimiter) + const rg = path.join("/fake/tools", binName) + mockFileExists.mockImplementation(async (p: string) => p === rg) + + expect(await getBinPath(appRoot)).toBe(rg) + }) + + it("prefers the VS Code copy over the system PATH", async () => { + process.env.PATH = "/fake/tools" + const vscodeRg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName) + const pathRg = path.join("/fake/tools", binName) + mockFileExists.mockImplementation(async (p: string) => p === vscodeRg || p === pathRg) + + expect(await getBinPath(appRoot)).toBe(vscodeRg) + }) + + it("returns undefined when ripgrep cannot be found anywhere", async () => { + process.env.PATH = "/fake/empty" + mockFileExists.mockResolvedValue(false) + + expect(await getBinPath(appRoot)).toBeUndefined() + }) +}) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 5dd800ac6f..1ded9d727a 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -11,7 +11,7 @@ This file provides functionality to perform regex searches on files using ripgre Inspired by: https://github.com/DiscreteTom/vscode-ripgrep-utils Key components: -1. getBinPath: Locates the ripgrep binary within the VSCode installation. +1. getBinPath: Locates the ripgrep binary (in the VS Code install, or on the system PATH). 2. execRipgrep: Executes the ripgrep command and returns the output. 3. regexSearchFiles: The main function that performs regex searches on files. - Parameters: @@ -51,6 +51,11 @@ rel/path/to/helper.ts 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/-/ rather than directly in bin/. +const ripgrepUniversalBinDir = `bin/${process.platform}-${process.arch}` + interface SearchFileResult { file: string searchResults: SearchResult[] @@ -80,7 +85,47 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line } /** - * Get the path to the ripgrep binary within the VSCode installation + * Look up the ripgrep binary on the system PATH. + * + * Used as a fallback after the VS Code installation has been checked. Covers + * VS Code forks whose install layout is not recognized by getBinPath, headless + * / CLI hosts with no VS Code installation, and machines where the user has + * installed ripgrep themselves. + */ +async function findRipgrepOnPath(): Promise { + const pathEnv = process.env.PATH + + if (!pathEnv) { + return undefined + } + + for (const dir of pathEnv.split(path.delimiter)) { + if (dir.length === 0) { + continue + } + + const candidate = path.join(dir, binName) + + if (await fileExistsAtPath(candidate)) { + return candidate + } + } + + return undefined +} + +/** + * Get the path to the ripgrep binary. + * + * Resolution order: + * 1. ripgrep shipped inside the VS Code installation. Both the long-standing + * `@vscode/ripgrep` layout and the newer `@vscode/ripgrep-universal` + * layout are checked — the latter is what VS Code Insiders' staged-install + * builds use (see microsoft/vscode#252063). + * 2. ripgrep on the system PATH — covers VS Code forks with an unrecognized + * install layout, headless / CLI hosts, and a user-installed ripgrep. + * + * Returns `undefined` when ripgrep cannot be located anywhere. */ export async function getBinPath(vscodeAppRoot: string): Promise { const checkPath = async (pkgFolder: string) => { @@ -92,7 +137,10 @@ export async function getBinPath(vscodeAppRoot: string): Promise Date: Sun, 24 May 2026 12:16:44 +0000 Subject: [PATCH 2/6] fix: drop the PATH fallback per review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per #248 review (edelauna): rely only on VS Code's bundled ripgrep — the fix keeps the @vscode/ripgrep-universal/bin/-/ resolution that VS Code Insiders' staged-install builds use (the original bug) but drops the system-PATH probe. This also addresses the Copilot trust-model note at the old line 111 (a PATH-resolved rg could be user-controlled) and clears the codecov gap — the uncovered lines were the PATH helper. --- src/services/ripgrep/__tests__/index.spec.ts | 31 +------------ src/services/ripgrep/index.ts | 47 +++----------------- 2 files changed, 8 insertions(+), 70 deletions(-) diff --git a/src/services/ripgrep/__tests__/index.spec.ts b/src/services/ripgrep/__tests__/index.spec.ts index fb4ad2a7af..d3ec39faee 100644 --- a/src/services/ripgrep/__tests__/index.spec.ts +++ b/src/services/ripgrep/__tests__/index.spec.ts @@ -1,7 +1,7 @@ // npx vitest run src/services/ripgrep/__tests__/index.spec.ts import path from "path" -import { vi, describe, it, expect, beforeEach, afterEach } from "vitest" +import { vi, describe, it, expect, beforeEach } from "vitest" import { truncateLine, getBinPath } from "../index" import { fileExistsAtPath } from "../../../utils/fs" @@ -63,21 +63,12 @@ describe("getBinPath", () => { const appRoot = "/fake/vscode/appRoot" const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" const platformDir = `${process.platform}-${process.arch}` - const originalPath = process.env.PATH beforeEach(() => { mockFileExists.mockReset() mockFileExists.mockResolvedValue(false) }) - afterEach(() => { - if (originalPath === undefined) { - delete process.env.PATH - } else { - process.env.PATH = originalPath - } - }) - it("resolves ripgrep from the classic @vscode/ripgrep layout", async () => { const rg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName) mockFileExists.mockImplementation(async (p: string) => p === rg) @@ -92,25 +83,7 @@ describe("getBinPath", () => { expect(await getBinPath(appRoot)).toBe(rg) }) - it("falls back to ripgrep on the system PATH when the VS Code copy is absent", async () => { - process.env.PATH = ["/fake/empty", "/fake/tools"].join(path.delimiter) - const rg = path.join("/fake/tools", binName) - mockFileExists.mockImplementation(async (p: string) => p === rg) - - expect(await getBinPath(appRoot)).toBe(rg) - }) - - it("prefers the VS Code copy over the system PATH", async () => { - process.env.PATH = "/fake/tools" - const vscodeRg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName) - const pathRg = path.join("/fake/tools", binName) - mockFileExists.mockImplementation(async (p: string) => p === vscodeRg || p === pathRg) - - expect(await getBinPath(appRoot)).toBe(vscodeRg) - }) - - it("returns undefined when ripgrep cannot be found anywhere", async () => { - process.env.PATH = "/fake/empty" + it("returns undefined when ripgrep cannot be found", async () => { mockFileExists.mockResolvedValue(false) expect(await getBinPath(appRoot)).toBeUndefined() diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 1ded9d727a..028aff1f68 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -85,47 +85,13 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line } /** - * Look up the ripgrep binary on the system PATH. + * Get the path to the ripgrep binary shipped inside the VS Code installation. * - * Used as a fallback after the VS Code installation has been checked. Covers - * VS Code forks whose install layout is not recognized by getBinPath, headless - * / CLI hosts with no VS Code installation, and machines where the user has - * installed ripgrep themselves. - */ -async function findRipgrepOnPath(): Promise { - const pathEnv = process.env.PATH - - if (!pathEnv) { - return undefined - } - - for (const dir of pathEnv.split(path.delimiter)) { - if (dir.length === 0) { - continue - } - - const candidate = path.join(dir, binName) - - if (await fileExistsAtPath(candidate)) { - return candidate - } - } - - return undefined -} - -/** - * Get the path to the ripgrep binary. - * - * Resolution order: - * 1. ripgrep shipped inside the VS Code installation. Both the long-standing - * `@vscode/ripgrep` layout and the newer `@vscode/ripgrep-universal` - * layout are checked — the latter is what VS Code Insiders' staged-install - * builds use (see microsoft/vscode#252063). - * 2. ripgrep on the system PATH — covers VS Code forks with an unrecognized - * install layout, headless / CLI hosts, and a user-installed ripgrep. + * Both the long-standing `@vscode/ripgrep` layout and the newer + * `@vscode/ripgrep-universal` layout are checked — the latter is what VS Code + * Insiders' staged-install builds use (see microsoft/vscode#252063). * - * Returns `undefined` when ripgrep cannot be located anywhere. + * Returns `undefined` when ripgrep cannot be located. */ export async function getBinPath(vscodeAppRoot: string): Promise { const checkPath = async (pkgFolder: string) => { @@ -139,8 +105,7 @@ export async function getBinPath(vscodeAppRoot: string): Promise Date: Sun, 24 May 2026 12:16:45 +0000 Subject: [PATCH 3/6] fix: resolve ripgrep via @vscode/ripgrep import per review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- pnpm-lock.yaml | 3 ++ src/esbuild.mjs | 2 +- src/package.json | 1 + src/services/ripgrep/__tests__/index.spec.ts | 51 +++++++++++--------- src/services/ripgrep/index.ts | 43 +++++++---------- 5 files changed, 51 insertions(+), 49 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9959d3570..c82aefbb43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -788,6 +788,9 @@ importers: '@vitest/coverage-v8': specifier: ^3.2.3 version: 3.2.4(vitest@3.2.4) + '@vscode/ripgrep': + specifier: ^1.17.0 + version: 1.17.0 '@vscode/test-electron': specifier: ^2.5.2 version: 2.5.2 diff --git a/src/esbuild.mjs b/src/esbuild.mjs index 890318cd26..8159581f36 100644 --- a/src/esbuild.mjs +++ b/src/esbuild.mjs @@ -126,7 +126,7 @@ async function main() { // global-agent must be external because it dynamically patches Node.js http/https modules // which breaks when bundled. It needs access to the actual Node.js module instances. // undici must be bundled because our VSIX is packaged with `--no-dependencies`. - external: ["vscode", "esbuild", "global-agent"], + external: ["vscode", "esbuild", "global-agent", "@vscode/ripgrep"], } /** diff --git a/src/package.json b/src/package.json index f62f421d43..06713f4247 100644 --- a/src/package.json +++ b/src/package.json @@ -554,6 +554,7 @@ "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.5", "@types/vscode": "^1.84.0", + "@vscode/ripgrep": "^1.17.0", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "3.3.2", "ai": "^6.0.75", diff --git a/src/services/ripgrep/__tests__/index.spec.ts b/src/services/ripgrep/__tests__/index.spec.ts index d3ec39faee..1287e3d373 100644 --- a/src/services/ripgrep/__tests__/index.spec.ts +++ b/src/services/ripgrep/__tests__/index.spec.ts @@ -1,16 +1,19 @@ // npx vitest run src/services/ripgrep/__tests__/index.spec.ts -import path from "path" import { vi, describe, it, expect, beforeEach } from "vitest" import { truncateLine, getBinPath } from "../index" -import { fileExistsAtPath } from "../../../utils/fs" -vi.mock("../../../utils/fs", () => ({ - fileExistsAtPath: vi.fn(), -})) +const ripgrepMock = vi.hoisted(() => ({ value: undefined as string | undefined, throws: false })) -const mockFileExists = vi.mocked(fileExistsAtPath) +vi.mock("@vscode/ripgrep", () => ({ + get rgPath() { + if (ripgrepMock.throws) { + throw new Error("simulated @vscode/ripgrep failure") + } + return ripgrepMock.value + }, +})) describe("Ripgrep line truncation", () => { // The default MAX_LINE_LENGTH is 500 in the implementation @@ -60,32 +63,34 @@ describe("Ripgrep line truncation", () => { }) describe("getBinPath", () => { - const appRoot = "/fake/vscode/appRoot" - const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" - const platformDir = `${process.platform}-${process.arch}` - beforeEach(() => { - mockFileExists.mockReset() - mockFileExists.mockResolvedValue(false) + ripgrepMock.value = undefined + ripgrepMock.throws = false + }) + + it("returns the rgPath exported by @vscode/ripgrep", async () => { + ripgrepMock.value = "/path/to/rg" + + expect(await getBinPath("/ignored")).toBe("/path/to/rg") }) - it("resolves ripgrep from the classic @vscode/ripgrep layout", async () => { - const rg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName) - mockFileExists.mockImplementation(async (p: string) => p === rg) + it("rewrites node_modules.asar to node_modules.asar.unpacked", async () => { + ripgrepMock.value = "/app/node_modules.asar/@vscode/ripgrep-universal/bin/win32-x64/rg.exe" - expect(await getBinPath(appRoot)).toBe(rg) + expect(await getBinPath("/ignored")).toBe( + "/app/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/win32-x64/rg.exe", + ) }) - it("resolves ripgrep from the @vscode/ripgrep-universal layout (VS Code Insiders)", async () => { - const rg = path.join(appRoot, "node_modules/@vscode/ripgrep-universal/bin", platformDir, binName) - mockFileExists.mockImplementation(async (p: string) => p === rg) + it("returns undefined when rgPath is not exported", async () => { + ripgrepMock.value = undefined - expect(await getBinPath(appRoot)).toBe(rg) + expect(await getBinPath("/ignored")).toBeUndefined() }) - it("returns undefined when ripgrep cannot be found", async () => { - mockFileExists.mockResolvedValue(false) + it("returns undefined when rgPath resolution throws", async () => { + ripgrepMock.throws = true - expect(await getBinPath(appRoot)).toBeUndefined() + expect(await getBinPath("/ignored")).toBeUndefined() }) }) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 028aff1f68..dd0ceeb7d0 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -5,13 +5,12 @@ 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 (in the VS Code install, or on the system PATH). +1. getBinPath: Resolves the ripgrep binary via the `@vscode/ripgrep` package — VS Code's require interceptor aliases it to the bundled binary at runtime. 2. execRipgrep: Executes the ripgrep command and returns the output. 3. regexSearchFiles: The main function that performs regex searches on files. - Parameters: @@ -51,11 +50,6 @@ rel/path/to/helper.ts 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/-/ rather than directly in bin/. -const ripgrepUniversalBinDir = `bin/${process.platform}-${process.arch}` - interface SearchFileResult { file: string searchResults: SearchResult[] @@ -85,28 +79,27 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line } /** - * Get the path to the ripgrep binary shipped inside the VS Code installation. + * Get the path to the ripgrep binary. * - * Both the long-standing `@vscode/ripgrep` layout and the newer - * `@vscode/ripgrep-universal` layout are checked — the latter is what VS Code - * Insiders' staged-install builds use (see microsoft/vscode#252063). + * Imports `@vscode/ripgrep` and returns its `rgPath` export. In the extension + * host this import is intercepted by VS Code (see microsoft/vscode + * `src/vs/workbench/api/common/extHostRequireInterceptor.ts`) and aliased to + * VS Code's own `@vscode/ripgrep-universal`, so we inherit whatever ripgrep + * VS Code ships with — including the Insiders staged-install layout that the + * old hardcoded path list missed (microsoft/vscode#252063). The + * `node_modules.asar` → `node_modules.asar.unpacked` substitution mirrors + * VS Code's own resolution in `src/vs/base/node/ripgrep.ts`. * - * Returns `undefined` when ripgrep cannot be located. + * The `vscodeAppRoot` parameter is retained for API stability and ignored. + * Returns `undefined` if `@vscode/ripgrep` is unavailable. */ -export async function getBinPath(vscodeAppRoot: string): Promise { - const checkPath = async (pkgFolder: string) => { - const fullPath = path.join(vscodeAppRoot, pkgFolder, binName) - return (await fileExistsAtPath(fullPath)) ? fullPath : undefined +export async function getBinPath(_vscodeAppRoot: string): Promise { + try { + const m = await import("@vscode/ripgrep") + return m.rgPath?.replace(/\bnode_modules\.asar\b/, "node_modules.asar.unpacked") + } catch { + return undefined } - - return ( - (await checkPath("node_modules/@vscode/ripgrep/bin/")) || - (await checkPath("node_modules/vscode-ripgrep/bin")) || - (await checkPath("node_modules.asar.unpacked/vscode-ripgrep/bin/")) || - (await checkPath("node_modules.asar.unpacked/@vscode/ripgrep/bin/")) || - (await checkPath(`node_modules/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`)) || - (await checkPath(`node_modules.asar.unpacked/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`)) - ) } async function execRipgrep(bin: string, args: string[]): Promise { From a21e29b32eea2414f106fc9e8cc76f4e8688fecf Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:45 +0000 Subject: [PATCH 4/6] revert: drop @vscode/ripgrep require attempt for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnostic from a Windows VS Code stable 1.121.0 install (see #248 review thread) showed that require("@vscode/ripgrep") throws — VS Code's extHost interceptor aliases the require to @vscode/ripgrep-universal, but that package isn't installed on builds that haven't completed the package-rename migration (which includes current stable). The path-probe fallback in the diagnostic test build was what actually resolved ripgrep on that install. Reverts to the prior shape: hardcoded paths covering both the @vscode/ripgrep and @vscode/ripgrep-universal layouts under vscode.env.appRoot. Also drops the @vscode/ripgrep devDep, the esbuild external entry, the loadRipgrep wrapper file, and the tests that mocked it — all dead with the revert. VS Code's package-rename migration will be tracked in a separate issue; once it lands across stable + Insiders the require approach can be revisited with empirical evidence of which mechanism VS Code expects 3rd-party extensions to use. --- pnpm-lock.yaml | 3 -- src/esbuild.mjs | 2 +- src/package.json | 1 - src/services/ripgrep/__tests__/index.spec.ts | 51 +++++++++----------- src/services/ripgrep/index.ts | 43 ++++++++++------- 5 files changed, 49 insertions(+), 51 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c82aefbb43..b9959d3570 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -788,9 +788,6 @@ importers: '@vitest/coverage-v8': specifier: ^3.2.3 version: 3.2.4(vitest@3.2.4) - '@vscode/ripgrep': - specifier: ^1.17.0 - version: 1.17.0 '@vscode/test-electron': specifier: ^2.5.2 version: 2.5.2 diff --git a/src/esbuild.mjs b/src/esbuild.mjs index 8159581f36..890318cd26 100644 --- a/src/esbuild.mjs +++ b/src/esbuild.mjs @@ -126,7 +126,7 @@ async function main() { // global-agent must be external because it dynamically patches Node.js http/https modules // which breaks when bundled. It needs access to the actual Node.js module instances. // undici must be bundled because our VSIX is packaged with `--no-dependencies`. - external: ["vscode", "esbuild", "global-agent", "@vscode/ripgrep"], + external: ["vscode", "esbuild", "global-agent"], } /** diff --git a/src/package.json b/src/package.json index 06713f4247..f62f421d43 100644 --- a/src/package.json +++ b/src/package.json @@ -554,7 +554,6 @@ "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.5", "@types/vscode": "^1.84.0", - "@vscode/ripgrep": "^1.17.0", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "3.3.2", "ai": "^6.0.75", diff --git a/src/services/ripgrep/__tests__/index.spec.ts b/src/services/ripgrep/__tests__/index.spec.ts index 1287e3d373..d3ec39faee 100644 --- a/src/services/ripgrep/__tests__/index.spec.ts +++ b/src/services/ripgrep/__tests__/index.spec.ts @@ -1,20 +1,17 @@ // npx vitest run src/services/ripgrep/__tests__/index.spec.ts +import path from "path" import { vi, describe, it, expect, beforeEach } from "vitest" import { truncateLine, getBinPath } from "../index" +import { fileExistsAtPath } from "../../../utils/fs" -const ripgrepMock = vi.hoisted(() => ({ value: undefined as string | undefined, throws: false })) - -vi.mock("@vscode/ripgrep", () => ({ - get rgPath() { - if (ripgrepMock.throws) { - throw new Error("simulated @vscode/ripgrep failure") - } - return ripgrepMock.value - }, +vi.mock("../../../utils/fs", () => ({ + fileExistsAtPath: vi.fn(), })) +const mockFileExists = vi.mocked(fileExistsAtPath) + describe("Ripgrep line truncation", () => { // The default MAX_LINE_LENGTH is 500 in the implementation const MAX_LINE_LENGTH = 500 @@ -63,34 +60,32 @@ describe("Ripgrep line truncation", () => { }) describe("getBinPath", () => { - beforeEach(() => { - ripgrepMock.value = undefined - ripgrepMock.throws = false - }) - - it("returns the rgPath exported by @vscode/ripgrep", async () => { - ripgrepMock.value = "/path/to/rg" + const appRoot = "/fake/vscode/appRoot" + const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" + const platformDir = `${process.platform}-${process.arch}` - expect(await getBinPath("/ignored")).toBe("/path/to/rg") + beforeEach(() => { + mockFileExists.mockReset() + mockFileExists.mockResolvedValue(false) }) - it("rewrites node_modules.asar to node_modules.asar.unpacked", async () => { - ripgrepMock.value = "/app/node_modules.asar/@vscode/ripgrep-universal/bin/win32-x64/rg.exe" + it("resolves ripgrep from the classic @vscode/ripgrep layout", async () => { + const rg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", binName) + mockFileExists.mockImplementation(async (p: string) => p === rg) - expect(await getBinPath("/ignored")).toBe( - "/app/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/win32-x64/rg.exe", - ) + expect(await getBinPath(appRoot)).toBe(rg) }) - it("returns undefined when rgPath is not exported", async () => { - ripgrepMock.value = undefined + it("resolves ripgrep from the @vscode/ripgrep-universal layout (VS Code Insiders)", async () => { + const rg = path.join(appRoot, "node_modules/@vscode/ripgrep-universal/bin", platformDir, binName) + mockFileExists.mockImplementation(async (p: string) => p === rg) - expect(await getBinPath("/ignored")).toBeUndefined() + expect(await getBinPath(appRoot)).toBe(rg) }) - it("returns undefined when rgPath resolution throws", async () => { - ripgrepMock.throws = true + it("returns undefined when ripgrep cannot be found", async () => { + mockFileExists.mockResolvedValue(false) - expect(await getBinPath("/ignored")).toBeUndefined() + expect(await getBinPath(appRoot)).toBeUndefined() }) }) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index dd0ceeb7d0..028aff1f68 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -5,12 +5,13 @@ 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: Resolves the ripgrep binary via the `@vscode/ripgrep` package — VS Code's require interceptor aliases it to the bundled binary at runtime. +1. getBinPath: Locates the ripgrep binary (in the VS Code install, or on the system PATH). 2. execRipgrep: Executes the ripgrep command and returns the output. 3. regexSearchFiles: The main function that performs regex searches on files. - Parameters: @@ -50,6 +51,11 @@ rel/path/to/helper.ts 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/-/ rather than directly in bin/. +const ripgrepUniversalBinDir = `bin/${process.platform}-${process.arch}` + interface SearchFileResult { file: string searchResults: SearchResult[] @@ -79,27 +85,28 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line } /** - * Get the path to the ripgrep binary. + * Get the path to the ripgrep binary shipped inside the VS Code installation. * - * Imports `@vscode/ripgrep` and returns its `rgPath` export. In the extension - * host this import is intercepted by VS Code (see microsoft/vscode - * `src/vs/workbench/api/common/extHostRequireInterceptor.ts`) and aliased to - * VS Code's own `@vscode/ripgrep-universal`, so we inherit whatever ripgrep - * VS Code ships with — including the Insiders staged-install layout that the - * old hardcoded path list missed (microsoft/vscode#252063). The - * `node_modules.asar` → `node_modules.asar.unpacked` substitution mirrors - * VS Code's own resolution in `src/vs/base/node/ripgrep.ts`. + * Both the long-standing `@vscode/ripgrep` layout and the newer + * `@vscode/ripgrep-universal` layout are checked — the latter is what VS Code + * Insiders' staged-install builds use (see microsoft/vscode#252063). * - * The `vscodeAppRoot` parameter is retained for API stability and ignored. - * Returns `undefined` if `@vscode/ripgrep` is unavailable. + * Returns `undefined` when ripgrep cannot be located. */ -export async function getBinPath(_vscodeAppRoot: string): Promise { - try { - const m = await import("@vscode/ripgrep") - return m.rgPath?.replace(/\bnode_modules\.asar\b/, "node_modules.asar.unpacked") - } catch { - return undefined +export async function getBinPath(vscodeAppRoot: string): Promise { + const checkPath = async (pkgFolder: string) => { + const fullPath = path.join(vscodeAppRoot, pkgFolder, binName) + return (await fileExistsAtPath(fullPath)) ? fullPath : undefined } + + return ( + (await checkPath("node_modules/@vscode/ripgrep/bin/")) || + (await checkPath("node_modules/vscode-ripgrep/bin")) || + (await checkPath("node_modules.asar.unpacked/vscode-ripgrep/bin/")) || + (await checkPath("node_modules.asar.unpacked/@vscode/ripgrep/bin/")) || + (await checkPath(`node_modules/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`)) || + (await checkPath(`node_modules.asar.unpacked/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`)) + ) } async function execRipgrep(bin: string, args: string[]): Promise { From 062b5284bd376e6b6f96b70111102afd6483cf6a Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:45 +0000 Subject: [PATCH 5/6] docs: drop stale PATH reference from getBinPath header Per CodeRabbit's review of the revert commit: the module-level comment still listed "or on the system PATH" as a resolution source, but the PATH probe was removed two commits back. Updated to match the actual behavior (probe paths under vscode.env.appRoot only). --- src/services/ripgrep/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 028aff1f68..59cdb03cf9 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -11,7 +11,7 @@ This file provides functionality to perform regex searches on files using ripgre Inspired by: https://github.com/DiscreteTom/vscode-ripgrep-utils Key components: -1. getBinPath: Locates the ripgrep binary (in the VS Code install, or on the system PATH). +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: From 417bc717ff86a589a7d5285e03f1301dd52ba552 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:45 +0000 Subject: [PATCH 6/6] test: cover the asar.unpacked universal layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit nit on the latest commit: add explicit coverage for the node_modules.asar.unpacked/@vscode/ripgrep-universal/-/ candidate path. Mirrors the existing universal-layout test but targets the .asar.unpacked variant — the exact staged-install shape that motivated the universal-layout entries in the first place. --- src/services/ripgrep/__tests__/index.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/ripgrep/__tests__/index.spec.ts b/src/services/ripgrep/__tests__/index.spec.ts index d3ec39faee..e6a613ad67 100644 --- a/src/services/ripgrep/__tests__/index.spec.ts +++ b/src/services/ripgrep/__tests__/index.spec.ts @@ -83,6 +83,13 @@ describe("getBinPath", () => { expect(await getBinPath(appRoot)).toBe(rg) }) + it("resolves ripgrep from the unpacked `@vscode/ripgrep-universal` layout", async () => { + const rg = path.join(appRoot, "node_modules.asar.unpacked/@vscode/ripgrep-universal/bin", platformDir, binName) + mockFileExists.mockImplementation(async (p: string) => p === rg) + + expect(await getBinPath(appRoot)).toBe(rg) + }) + it("returns undefined when ripgrep cannot be found", async () => { mockFileExists.mockResolvedValue(false)