From 051b67bd640cbdbb9fd4c6561e3894675cd6a4d9 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Fri, 22 May 2026 06:26:39 +0000 Subject: [PATCH] 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