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