From 26ae6832fa9f23640e5d421308f12f3b830a6afa Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:56 +0000 Subject: [PATCH 1/5] feat: add Zoo Code: Show Ripgrep Diagnostic command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a user-triggerable diagnostic for the "Could not find ripgrep binary" error class. The command runs the same hybrid resolution that #248 tested — try require("@vscode/ripgrep"), then probe known appRoot-relative paths — and writes a verbose report to a dedicated output channel, also copying it to the clipboard. Motivated by #248: debugging that bug required a custom test build to inspect resolution state on the user's machine. With this command shipped, future users hitting weird ripgrep-resolution behavior can paste the diagnostic into a bug report without us needing to build instrumented VSIXs. Reintroduces the @vscode/ripgrep devDep and esbuild external entry that #248 dropped — the diagnostic needs to call require() to report what happens. The require attempt also serves as forward- compat: when VS Code completes the @vscode/ripgrep → @vscode/ ripgrep-universal package-rename migration, it will start succeeding broadly, and the diagnostic output will be the signal that we can revisit the require approach in getBinPath itself. - src/services/ripgrep/diagnostic.ts: new module (data fn + cmd wrapper) - src/services/ripgrep/internal/loadRipgrep.ts: testable require wrapper - zoo-code.showRipgrepDiagnostic command wired up in registerCommands.ts and contributed via package.json - Tests for the data function --- pnpm-lock.yaml | 3 + src/activate/registerCommands.ts | 3 + src/esbuild.mjs | 2 +- src/package.json | 6 + .../ripgrep/__tests__/diagnostic.spec.ts | 108 ++++++++++++++++++ src/services/ripgrep/diagnostic.ts | 82 +++++++++++++ src/services/ripgrep/internal/loadRipgrep.ts | 16 +++ 7 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/services/ripgrep/__tests__/diagnostic.spec.ts create mode 100644 src/services/ripgrep/diagnostic.ts create mode 100644 src/services/ripgrep/internal/loadRipgrep.ts 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/activate/registerCommands.ts b/src/activate/registerCommands.ts index 0fb2c9d040..fa51f48ee4 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -13,6 +13,7 @@ import { handleNewTask } from "./handleTask" import { CodeIndexManager } from "../services/code-index/manager" import { importSettingsWithFeedback } from "../core/config/importExport" import { MdmService } from "../services/mdm/MdmService" +import { registerRipgrepDiagnosticCommand } from "../services/ripgrep/diagnostic" import { t } from "../i18n" /** @@ -68,6 +69,8 @@ export const registerCommands = (options: RegisterCommandOptions) => { const command = getCommand(id as CommandId) context.subscriptions.push(vscode.commands.registerCommand(command, callback)) } + + context.subscriptions.push(registerRipgrepDiagnosticCommand()) } const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions): Record => ({ 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..045f539839 100644 --- a/src/package.json +++ b/src/package.json @@ -160,6 +160,11 @@ "title": "%command.acceptInput.title%", "category": "%configuration.title%" }, + { + "command": "zoo-code.showRipgrepDiagnostic", + "title": "Show Ripgrep Diagnostic", + "category": "%configuration.title%" + }, { "command": "zoo-code.toggleAutoApprove", "title": "%command.toggleAutoApprove.title%", @@ -554,6 +559,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__/diagnostic.spec.ts b/src/services/ripgrep/__tests__/diagnostic.spec.ts new file mode 100644 index 0000000000..b54ace303c --- /dev/null +++ b/src/services/ripgrep/__tests__/diagnostic.spec.ts @@ -0,0 +1,108 @@ +// npx vitest run src/services/ripgrep/__tests__/diagnostic.spec.ts + +import * as path from "path" + +import { vi, describe, it, expect, beforeEach } from "vitest" + +import { getRipgrepDiagnostic } from "../diagnostic" + +const ripgrepMock = vi.hoisted(() => ({ + value: undefined as { rgPath?: string } | undefined, +})) + +const fsMock = vi.hoisted(() => ({ + existing: new Set(), +})) + +vi.mock("../internal/loadRipgrep", () => ({ + loadRipgrep: () => ripgrepMock.value, +})) + +vi.mock("../../../utils/fs", () => ({ + fileExistsAtPath: (p: string) => Promise.resolve(fsMock.existing.has(p)), +})) + +const APP_ROOT = "/app" + +const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" +const universalRelBin = `bin/${process.platform}-${process.arch}/${binName}` + +const expectedCandidates = [ + path.join(APP_ROOT, "node_modules", "@vscode", "ripgrep", "bin", binName), + path.join(APP_ROOT, "node_modules", "vscode-ripgrep", "bin", binName), + path.join(APP_ROOT, "node_modules.asar.unpacked", "vscode-ripgrep", "bin", binName), + path.join(APP_ROOT, "node_modules.asar.unpacked", "@vscode", "ripgrep", "bin", binName), + path.join(APP_ROOT, "node_modules", "@vscode", "ripgrep-universal", ...universalRelBin.split("/")), + path.join(APP_ROOT, "node_modules.asar.unpacked", "@vscode", "ripgrep-universal", ...universalRelBin.split("/")), +] + +describe("getRipgrepDiagnostic", () => { + beforeEach(() => { + ripgrepMock.value = undefined + fsMock.existing = new Set() + }) + + it("includes rgPath and fileExistsAtPath: true when loadRipgrep returns an existing path", async () => { + const rgPath = "/some/path" + ripgrepMock.value = { rgPath } + fsMock.existing = new Set([rgPath]) + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain("rgPath: /some/path") + expect(report).toContain("fileExistsAtPath: true") + expect(report).toContain("after .asar→.asar.unpacked: /some/path") + }) + + it("rewrites node_modules.asar to node_modules.asar.unpacked in the report", async () => { + const rgPath = "/app/node_modules.asar/foo/rg" + const substituted = "/app/node_modules.asar.unpacked/foo/rg" + ripgrepMock.value = { rgPath } + fsMock.existing = new Set([substituted]) + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain(`after .asar→.asar.unpacked: ${substituted}`) + expect(report).toContain("fileExistsAtPath: true") + }) + + it("reports require failure when loadRipgrep returns undefined", async () => { + ripgrepMock.value = undefined + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain("loadRipgrep() returned undefined (require threw)") + }) + + it("reports rgPath: (undefined) when loadRipgrep returns an object without rgPath", async () => { + ripgrepMock.value = {} + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain("rgPath: (undefined)") + expect(report).not.toContain("after .asar→.asar.unpacked:") + }) + + it("marks only the first probe candidate as found when only it exists", async () => { + fsMock.existing = new Set([expectedCandidates[0]]) + + const report = await getRipgrepDiagnostic(APP_ROOT) + + const found = expectedCandidates.filter((c) => report.includes(`✓ ${c}`)) + const missing = expectedCandidates.filter((c) => report.includes(`✗ ${c}`)) + + expect(found).toEqual([expectedCandidates[0]]) + expect(missing).toEqual(expectedCandidates.slice(1)) + }) + + it("marks all probe candidates as missing when none exist", async () => { + fsMock.existing = new Set() + + const report = await getRipgrepDiagnostic(APP_ROOT) + + for (const candidate of expectedCandidates) { + expect(report).toContain(`✗ ${candidate}`) + } + expect(report).not.toContain("✓ ") + }) +}) diff --git a/src/services/ripgrep/diagnostic.ts b/src/services/ripgrep/diagnostic.ts new file mode 100644 index 0000000000..bbfc614bee --- /dev/null +++ b/src/services/ripgrep/diagnostic.ts @@ -0,0 +1,82 @@ +import * as path from "path" +import * as vscode from "vscode" + +import { fileExistsAtPath } from "../../utils/fs" +import { loadRipgrep } from "./internal/loadRipgrep" + +const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" +const universalBin = `bin/${process.platform}-${process.arch}/${binName}` + +function probeCandidates(vscodeAppRoot: string): readonly string[] { + return [ + path.join(vscodeAppRoot, "node_modules", "@vscode", "ripgrep", "bin", binName), + path.join(vscodeAppRoot, "node_modules", "vscode-ripgrep", "bin", binName), + path.join(vscodeAppRoot, "node_modules.asar.unpacked", "vscode-ripgrep", "bin", binName), + path.join(vscodeAppRoot, "node_modules.asar.unpacked", "@vscode", "ripgrep", "bin", binName), + path.join(vscodeAppRoot, "node_modules", "@vscode", "ripgrep-universal", ...universalBin.split("/")), + path.join( + vscodeAppRoot, + "node_modules.asar.unpacked", + "@vscode", + "ripgrep-universal", + ...universalBin.split("/"), + ), + ] +} + +/** + * Produces a textual diagnostic report of how ripgrep would be resolved + * for the given VS Code installation. Pure data function — no UI side + * effects — so it's fully unit-testable. + * + * Step 1 tries `loadRipgrep()` (CommonJS require, hits VS Code's + * extHost interceptor on builds that have completed the + * `@vscode/ripgrep` → `@vscode/ripgrep-universal` migration). + * Step 2 probes every known `vscode.env.appRoot`-relative path and + * reports which ones exist on disk. + */ +export async function getRipgrepDiagnostic(vscodeAppRoot: string): Promise { + const lines: string[] = [ + `Zoo Code Ripgrep Diagnostic (${new Date().toISOString()})`, + `vscode.version: ${vscode.version}`, + `vscode.env.appRoot: ${vscodeAppRoot}`, + `process.platform/arch: ${process.platform}/${process.arch}`, + ``, + `--- step 1: require("@vscode/ripgrep") via loadRipgrep ---`, + ] + const m = loadRipgrep() + if (!m) { + lines.push(`loadRipgrep() returned undefined (require threw)`) + } else { + const keys = Object.keys(m).join(",") || "(none)" + lines.push(`loadRipgrep() returned object. keys: ${keys}`) + lines.push(`rgPath: ${m.rgPath ?? "(undefined)"}`) + if (m.rgPath) { + const fixed = m.rgPath.replace(/\bnode_modules\.asar\b/, "node_modules.asar.unpacked") + lines.push(`after .asar→.asar.unpacked: ${fixed}`) + lines.push(`fileExistsAtPath: ${await fileExistsAtPath(fixed)}`) + } + } + lines.push(``) + lines.push(`--- step 2: path probe under appRoot ---`) + for (const candidate of probeCandidates(vscodeAppRoot)) { + lines.push(` ${(await fileExistsAtPath(candidate)) ? "✓" : "✗"} ${candidate}`) + } + return lines.join("\n") +} + +/** + * Registers the `zoo-code.showRipgrepDiagnostic` command. Thin wrapper — + * runs `getRipgrepDiagnostic`, shows the result in an output channel, + * copies it to the clipboard, and shows an info toast. + */ +export function registerRipgrepDiagnosticCommand(): vscode.Disposable { + return vscode.commands.registerCommand("zoo-code.showRipgrepDiagnostic", async () => { + const report = await getRipgrepDiagnostic(vscode.env.appRoot) + const channel = vscode.window.createOutputChannel("Zoo Code Ripgrep Diagnostic") + channel.appendLine(report) + channel.show(true) + await vscode.env.clipboard.writeText(report) + await vscode.window.showInformationMessage("Zoo Code: ripgrep diagnostic copied to clipboard.") + }) +} diff --git a/src/services/ripgrep/internal/loadRipgrep.ts b/src/services/ripgrep/internal/loadRipgrep.ts new file mode 100644 index 0000000000..463718f8fd --- /dev/null +++ b/src/services/ripgrep/internal/loadRipgrep.ts @@ -0,0 +1,16 @@ +/** + * Loads `@vscode/ripgrep` via CommonJS `require()`. Lives in its own + * module so unit tests can `vi.mock` the wrapper — vitest's mock registry + * hooks the import graph, not Node's native CJS resolver, and + * `@vscode/ripgrep` resolves through the latter at test time because it's + * a real devDep installed in `node_modules`. + * + * Returns `undefined` if the package can't be loaded for any reason. + */ +export function loadRipgrep(): { rgPath?: string } | undefined { + try { + return require("@vscode/ripgrep") as { rgPath?: string } + } catch { + return undefined + } +} From 3c106685a968f92244f22637a825cf070bc79a28 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:57 +0000 Subject: [PATCH 2/5] fix: address review feedback on ripgrep diagnostic command - diagnostic.ts: fix .asar->.asar.unpacked regex (the \b boundary was matching inside node_modules.asar.unpacked too, producing .unpacked.unpacked). Replaced with a path-separator lookahead. - diagnostic.ts: create the OutputChannel once at registration and return a composite Disposable that disposes both the command and the channel; clear the channel before appending so repeated runs are readable. - diagnostic.ts: route the command ID through getCommand() instead of hardcoding 'zoo-code.showRipgrepDiagnostic', and add 'showRipgrepDiagnostic' to the CommandId union in @roo-code/types. Exclude it from getCommandsMap so the diagnostic's separate registration owns the OutputChannel lifecycle. - package.json + package.nls.*.json: switch the command title to a %command.showRipgrepDiagnostic.title% NLS key across all 18 locale files. - loadRipgrep.ts: preserve the require() error message in a loadError field instead of swallowing it; surface it in the diagnostic report. - Tests updated for the loadError case, the already-unpacked path guard, and widened the mock value type. --- packages/types/src/vscode.ts | 2 ++ src/activate/registerCommands.ts | 10 ++++++- src/package.json | 2 +- src/package.nls.ca.json | 1 + src/package.nls.de.json | 1 + src/package.nls.es.json | 1 + src/package.nls.fr.json | 1 + src/package.nls.hi.json | 1 + src/package.nls.id.json | 1 + src/package.nls.it.json | 1 + src/package.nls.ja.json | 1 + src/package.nls.json | 1 + src/package.nls.ko.json | 1 + src/package.nls.nl.json | 1 + src/package.nls.pl.json | 1 + src/package.nls.pt-BR.json | 1 + src/package.nls.ru.json | 1 + src/package.nls.tr.json | 1 + src/package.nls.vi.json | 1 + src/package.nls.zh-CN.json | 1 + src/package.nls.zh-TW.json | 1 + .../ripgrep/__tests__/diagnostic.spec.ts | 27 ++++++++++++++++++- src/services/ripgrep/diagnostic.ts | 19 ++++++++++--- src/services/ripgrep/internal/loadRipgrep.ts | 15 +++++++---- 24 files changed, 81 insertions(+), 12 deletions(-) diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index 71818276db..fd4e31116d 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -46,6 +46,8 @@ export const commandIds = [ "acceptInput", "focusPanel", "toggleAutoApprove", + + "showRipgrepDiagnostic", ] as const export type CommandId = (typeof commandIds)[number] diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index fa51f48ee4..dabc8bde5e 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -73,7 +73,15 @@ export const registerCommands = (options: RegisterCommandOptions) => { context.subscriptions.push(registerRipgrepDiagnosticCommand()) } -const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions): Record => ({ +// `showRipgrepDiagnostic` is registered separately by +// `registerRipgrepDiagnosticCommand` (above), which owns the OutputChannel +// lifecycle alongside the command registration, so it's intentionally +// excluded from this map. +const getCommandsMap = ({ + context, + outputChannel, + provider, +}: RegisterCommandOptions): Record, any> => ({ activationCompleted: () => {}, plusButtonClicked: async () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) diff --git a/src/package.json b/src/package.json index 045f539839..120480f361 100644 --- a/src/package.json +++ b/src/package.json @@ -162,7 +162,7 @@ }, { "command": "zoo-code.showRipgrepDiagnostic", - "title": "Show Ripgrep Diagnostic", + "title": "%command.showRipgrepDiagnostic.title%", "category": "%configuration.title%" }, { diff --git a/src/package.nls.ca.json b/src/package.nls.ca.json index a809b001e8..d338a425d6 100644 --- a/src/package.nls.ca.json +++ b/src/package.nls.ca.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Corregir Aquesta Ordre", "command.terminal.explainCommand.title": "Explicar Aquesta Ordre", "command.acceptInput.title": "Acceptar Entrada/Suggeriment", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Alternar Auto-Aprovació", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.de.json b/src/package.nls.de.json index c847db0b72..e49d57a0b6 100644 --- a/src/package.nls.de.json +++ b/src/package.nls.de.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Diesen Befehl Reparieren", "command.terminal.explainCommand.title": "Diesen Befehl Erklären", "command.acceptInput.title": "Eingabe/Vorschlag Akzeptieren", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Auto-Genehmigung Umschalten", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.es.json b/src/package.nls.es.json index 056b679933..117c0a70a3 100644 --- a/src/package.nls.es.json +++ b/src/package.nls.es.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Corregir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", "command.acceptInput.title": "Aceptar Entrada/Sugerencia", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Alternar Auto-Aprobación", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.fr.json b/src/package.nls.fr.json index bfb5cfec77..26ec85c773 100644 --- a/src/package.nls.fr.json +++ b/src/package.nls.fr.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Corriger cette Commande", "command.terminal.explainCommand.title": "Expliquer cette Commande", "command.acceptInput.title": "Accepter l'Entrée/Suggestion", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Basculer Auto-Approbation", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.hi.json b/src/package.nls.hi.json index 7bdfb4f62c..67b716cdb8 100644 --- a/src/package.nls.hi.json +++ b/src/package.nls.hi.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "यह कमांड ठीक करें", "command.terminal.explainCommand.title": "यह कमांड समझाएं", "command.acceptInput.title": "इनपुट/सुझाव स्वीकारें", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "ऑटो-अनुमोदन टॉगल करें", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.id.json b/src/package.nls.id.json index 3c685429a9..d2c81e69a9 100644 --- a/src/package.nls.id.json +++ b/src/package.nls.id.json @@ -23,6 +23,7 @@ "command.terminal.fixCommand.title": "Perbaiki Perintah Ini", "command.terminal.explainCommand.title": "Jelaskan Perintah Ini", "command.acceptInput.title": "Terima Input/Saran", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Alihkan Persetujuan Otomatis", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "Perintah yang dapat dijalankan secara otomatis ketika 'Selalu setujui operasi eksekusi' diaktifkan", diff --git a/src/package.nls.it.json b/src/package.nls.it.json index 69dddaeae5..dad1b5641e 100644 --- a/src/package.nls.it.json +++ b/src/package.nls.it.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Correggi Questo Comando", "command.terminal.explainCommand.title": "Spiega Questo Comando", "command.acceptInput.title": "Accetta Input/Suggerimento", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Attiva/Disattiva Auto-Approvazione", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.ja.json b/src/package.nls.ja.json index 2e6498925a..32633b7481 100644 --- a/src/package.nls.ja.json +++ b/src/package.nls.ja.json @@ -23,6 +23,7 @@ "command.terminal.fixCommand.title": "このコマンドを修正", "command.terminal.explainCommand.title": "このコマンドを説明", "command.acceptInput.title": "入力/提案を承認", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "自動承認を切替", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "'常に実行操作を承認する'が有効な場合に自動実行できるコマンド", diff --git a/src/package.nls.json b/src/package.nls.json index 23c9b02d92..d665068e37 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -23,6 +23,7 @@ "command.terminal.fixCommand.title": "Fix This Command", "command.terminal.explainCommand.title": "Explain This Command", "command.acceptInput.title": "Accept Input/Suggestion", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Toggle Auto-Approve", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled", diff --git a/src/package.nls.ko.json b/src/package.nls.ko.json index 9d925f48a6..740d92cba3 100644 --- a/src/package.nls.ko.json +++ b/src/package.nls.ko.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "이 명령어 수정", "command.terminal.explainCommand.title": "이 명령어 설명", "command.acceptInput.title": "입력/제안 수락", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "자동 승인 전환", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.nl.json b/src/package.nls.nl.json index f070f44175..f60d3b5a90 100644 --- a/src/package.nls.nl.json +++ b/src/package.nls.nl.json @@ -23,6 +23,7 @@ "command.terminal.fixCommand.title": "Repareer Dit Commando", "command.terminal.explainCommand.title": "Leg Dit Commando Uit", "command.acceptInput.title": "Invoer/Suggestie Accepteren", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Auto-Goedkeuring Schakelen", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "Commando's die automatisch kunnen worden uitgevoerd wanneer 'Altijd goedkeuren uitvoerbewerkingen' is ingeschakeld", diff --git a/src/package.nls.pl.json b/src/package.nls.pl.json index 960ae676ab..92f152f7a3 100644 --- a/src/package.nls.pl.json +++ b/src/package.nls.pl.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Napraw tę Komendę", "command.terminal.explainCommand.title": "Wyjaśnij tę Komendę", "command.acceptInput.title": "Akceptuj Wprowadzanie/Sugestię", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Przełącz Auto-Zatwierdzanie", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.pt-BR.json b/src/package.nls.pt-BR.json index 22913edd24..dbd554a9f9 100644 --- a/src/package.nls.pt-BR.json +++ b/src/package.nls.pt-BR.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Corrigir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", "command.acceptInput.title": "Aceitar Entrada/Sugestão", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Alternar Auto-Aprovação", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.ru.json b/src/package.nls.ru.json index 0c6c2e5dc1..2e73b981c0 100644 --- a/src/package.nls.ru.json +++ b/src/package.nls.ru.json @@ -23,6 +23,7 @@ "command.terminal.fixCommand.title": "Исправить эту команду", "command.terminal.explainCommand.title": "Объяснить эту команду", "command.acceptInput.title": "Принять ввод/предложение", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Переключить Авто-Подтверждение", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "Команды, которые могут быть автоматически выполнены, когда включена опция 'Всегда подтверждать операции выполнения'", diff --git a/src/package.nls.tr.json b/src/package.nls.tr.json index 6742a15f6c..bee9b50177 100644 --- a/src/package.nls.tr.json +++ b/src/package.nls.tr.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Bu Komutu Düzelt", "command.terminal.explainCommand.title": "Bu Komutu Açıkla", "command.acceptInput.title": "Girişi/Öneriyi Kabul Et", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Otomatik Onayı Değiştir", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.vi.json b/src/package.nls.vi.json index c0a55709a9..0ce3173f8d 100644 --- a/src/package.nls.vi.json +++ b/src/package.nls.vi.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Sửa Lệnh Này", "command.terminal.explainCommand.title": "Giải Thích Lệnh Này", "command.acceptInput.title": "Chấp Nhận Đầu Vào/Gợi Ý", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "Bật/Tắt Tự Động Phê Duyệt", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.zh-CN.json b/src/package.nls.zh-CN.json index 13ff532046..b24bf6aff6 100644 --- a/src/package.nls.zh-CN.json +++ b/src/package.nls.zh-CN.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "修复此命令", "command.terminal.explainCommand.title": "解释此命令", "command.acceptInput.title": "接受输入/建议", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "切换自动批准", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.zh-TW.json b/src/package.nls.zh-TW.json index 5493375a5e..6ef6499998 100644 --- a/src/package.nls.zh-TW.json +++ b/src/package.nls.zh-TW.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "修復此命令", "command.terminal.explainCommand.title": "解釋此命令", "command.acceptInput.title": "接受輸入/建議", + "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", "command.toggleAutoApprove.title": "切換自動批准", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/services/ripgrep/__tests__/diagnostic.spec.ts b/src/services/ripgrep/__tests__/diagnostic.spec.ts index b54ace303c..1c9f62182a 100644 --- a/src/services/ripgrep/__tests__/diagnostic.spec.ts +++ b/src/services/ripgrep/__tests__/diagnostic.spec.ts @@ -7,7 +7,7 @@ import { vi, describe, it, expect, beforeEach } from "vitest" import { getRipgrepDiagnostic } from "../diagnostic" const ripgrepMock = vi.hoisted(() => ({ - value: undefined as { rgPath?: string } | undefined, + value: undefined as { rgPath?: string; loadError?: string } | undefined, })) const fsMock = vi.hoisted(() => ({ @@ -66,6 +66,20 @@ describe("getRipgrepDiagnostic", () => { expect(report).toContain("fileExistsAtPath: true") }) + it("does not double-substitute when rgPath already contains node_modules.asar.unpacked", async () => { + // A previous `\b` regex matched the `r`/`.` boundary inside + // `node_modules.asar.unpacked`, producing `.unpacked.unpacked`. + const rgPath = "/app/node_modules.asar.unpacked/foo/rg" + ripgrepMock.value = { rgPath } + fsMock.existing = new Set([rgPath]) + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain(`after .asar→.asar.unpacked: ${rgPath}`) + expect(report).not.toContain("node_modules.asar.unpacked.unpacked") + expect(report).toContain("fileExistsAtPath: true") + }) + it("reports require failure when loadRipgrep returns undefined", async () => { ripgrepMock.value = undefined @@ -74,6 +88,17 @@ describe("getRipgrepDiagnostic", () => { expect(report).toContain("loadRipgrep() returned undefined (require threw)") }) + it("reports the loadError when loadRipgrep returns a loadError field", async () => { + ripgrepMock.value = { loadError: "Cannot find module '@vscode/ripgrep'" } + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain("loadRipgrep() returned loadError: Cannot find module '@vscode/ripgrep'") + // Should not also push the success-branch lines. + expect(report).not.toContain("after .asar→.asar.unpacked:") + expect(report).not.toContain("rgPath:") + }) + it("reports rgPath: (undefined) when loadRipgrep returns an object without rgPath", async () => { ripgrepMock.value = {} diff --git a/src/services/ripgrep/diagnostic.ts b/src/services/ripgrep/diagnostic.ts index bbfc614bee..60f54b4b30 100644 --- a/src/services/ripgrep/diagnostic.ts +++ b/src/services/ripgrep/diagnostic.ts @@ -2,6 +2,7 @@ import * as path from "path" import * as vscode from "vscode" import { fileExistsAtPath } from "../../utils/fs" +import { getCommand } from "../../utils/commands" import { loadRipgrep } from "./internal/loadRipgrep" const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" @@ -47,12 +48,16 @@ export async function getRipgrepDiagnostic(vscodeAppRoot: string): Promise { + const channel = vscode.window.createOutputChannel("Zoo Code Ripgrep Diagnostic") + const command = vscode.commands.registerCommand(getCommand("showRipgrepDiagnostic"), async () => { const report = await getRipgrepDiagnostic(vscode.env.appRoot) - const channel = vscode.window.createOutputChannel("Zoo Code Ripgrep Diagnostic") + channel.clear() channel.appendLine(report) channel.show(true) await vscode.env.clipboard.writeText(report) await vscode.window.showInformationMessage("Zoo Code: ripgrep diagnostic copied to clipboard.") }) + return vscode.Disposable.from(command, channel) } +/* c8 ignore stop */ diff --git a/src/services/ripgrep/internal/loadRipgrep.ts b/src/services/ripgrep/internal/loadRipgrep.ts index 463718f8fd..b1e0d6fcc1 100644 --- a/src/services/ripgrep/internal/loadRipgrep.ts +++ b/src/services/ripgrep/internal/loadRipgrep.ts @@ -5,12 +5,17 @@ * `@vscode/ripgrep` resolves through the latter at test time because it's * a real devDep installed in `node_modules`. * - * Returns `undefined` if the package can't be loaded for any reason. + * On require failure returns `{ loadError }` so the diagnostic can surface + * the actual error message instead of dropping it. */ -export function loadRipgrep(): { rgPath?: string } | undefined { +export type LoadRipgrepResult = { rgPath?: string; loadError?: string } + +export function loadRipgrep(): LoadRipgrepResult | undefined { try { - return require("@vscode/ripgrep") as { rgPath?: string } - } catch { - return undefined + return require("@vscode/ripgrep") as LoadRipgrepResult + } catch (error) { + return { + loadError: error instanceof Error ? error.message : String(error), + } } } From 7d2fc7d5180cf89ae9bba899f3bbd7d0b572b8bc Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:57 +0000 Subject: [PATCH 3/5] test(ripgrep diagnostic): add Windows-path coverage + appRoot validation Two polish items from review: - getRipgrepDiagnostic now early-returns an explanatory message when vscode.env.appRoot is empty, instead of silently producing a report with a meaningless path probe. Closes the input- validation gap surfaced in re-reading the diagnostic. - New test exercises the .asar->.asar.unpacked substitution on Windows-style backslash paths. The regex already handles both separators via [\\/], this just pins it. --- .../ripgrep/__tests__/diagnostic.spec.ts | 23 +++++++++++++++++++ src/services/ripgrep/diagnostic.ts | 12 ++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/services/ripgrep/__tests__/diagnostic.spec.ts b/src/services/ripgrep/__tests__/diagnostic.spec.ts index 1c9f62182a..cb5eed5b4a 100644 --- a/src/services/ripgrep/__tests__/diagnostic.spec.ts +++ b/src/services/ripgrep/__tests__/diagnostic.spec.ts @@ -130,4 +130,27 @@ describe("getRipgrepDiagnostic", () => { } expect(report).not.toContain("✓ ") }) + + it("rewrites node_modules.asar to node_modules.asar.unpacked on Windows paths (backslash separator)", async () => { + // Pure string-substitution test: the literal `win32-x64` segment is + // not derived from process.platform/arch, so this runs on any host. + const rgPath = "C:\\app\\node_modules.asar\\@vscode\\ripgrep-universal\\bin\\win32-x64\\rg.exe" + const substituted = "C:\\app\\node_modules.asar.unpacked\\@vscode\\ripgrep-universal\\bin\\win32-x64\\rg.exe" + ripgrepMock.value = { rgPath } + fsMock.existing = new Set([substituted]) + + const report = await getRipgrepDiagnostic("C:\\app") + + expect(report).toContain(`after .asar→.asar.unpacked: ${substituted}`) + expect(report).toContain("fileExistsAtPath: true") + }) + + it("returns an explanatory report when vscode.env.appRoot is empty", async () => { + const report = await getRipgrepDiagnostic("") + + expect(report).toContain("vscode.env.appRoot: (empty)") + expect(report).toContain("Cannot run diagnostic") + // path probe should NOT have run + expect(report).not.toContain("--- step 2: path probe under appRoot ---") + }) }) diff --git a/src/services/ripgrep/diagnostic.ts b/src/services/ripgrep/diagnostic.ts index 60f54b4b30..66602441b8 100644 --- a/src/services/ripgrep/diagnostic.ts +++ b/src/services/ripgrep/diagnostic.ts @@ -37,6 +37,18 @@ function probeCandidates(vscodeAppRoot: string): readonly string[] { * reports which ones exist on disk. */ export async function getRipgrepDiagnostic(vscodeAppRoot: string): Promise { + if (!vscodeAppRoot || vscodeAppRoot.trim() === "") { + return [ + `Zoo Code Ripgrep Diagnostic (${new Date().toISOString()})`, + `vscode.version: ${vscode.version}`, + `vscode.env.appRoot: (empty)`, + ``, + `Cannot run diagnostic: vscode.env.appRoot is empty. This usually means`, + `the extension activated outside a VS Code-style host (e.g., a remote`, + `extension host that doesn't expose appRoot). The require-interceptor`, + `path may still work; the path-probe step requires a valid appRoot.`, + ].join("\n") + } const lines: string[] = [ `Zoo Code Ripgrep Diagnostic (${new Date().toISOString()})`, `vscode.version: ${vscode.version}`, From 156c65c37021305ec23dc8862c5d3c39d1114275 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Sun, 24 May 2026 12:16:57 +0000 Subject: [PATCH 4/5] refactor(registerCommands): tighten command map value type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the residual `any` in the command-callback map value with a `CommandCallback` alias of `(...args: any[]) => unknown`. Mirrors VS Code's own `commands.registerCommand` signature while narrowing the return to `unknown` so callers must inspect before use. Rest-args stay `any[]` intentionally: the callbacks in this map are heterogeneous (`importSettings` takes an optional `filePath?: string`, the rest take none), and VS Code dispatches positional args dynamically — a single tight per-arity type would be lossy without splitting the map. --- src/activate/registerCommands.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index dabc8bde5e..b817787020 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -77,11 +77,19 @@ export const registerCommands = (options: RegisterCommandOptions) => { // `registerRipgrepDiagnosticCommand` (above), which owns the OutputChannel // lifecycle alongside the command registration, so it's intentionally // excluded from this map. +// +// Callback shape mirrors VS Code's own `commands.registerCommand` signature +// (`(...args: any[]) => any`), with the return narrowed to `unknown` so +// callers must inspect before using. `any[]` for args is unavoidable: the +// callbacks here are heterogeneous (`importSettings` takes an optional +// `filePath?: string`, others take none) and VS Code dispatches positional +// args dynamically. +type CommandCallback = (...args: any[]) => unknown const getCommandsMap = ({ context, outputChannel, provider, -}: RegisterCommandOptions): Record, any> => ({ +}: RegisterCommandOptions): Record, CommandCallback> => ({ activationCompleted: () => {}, plusButtonClicked: async () => { const visibleProvider = getVisibleProviderOrLog(outputChannel) From 1e7a84618fae13d2813ab5cfc9e1ac4bbb3e876f Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Wed, 10 Jun 2026 02:08:21 +0000 Subject: [PATCH 5/5] feat(ripgrep): add Show Ripgrep Diagnostic command and translations --- knip.json | 1 + .../__tests__/registerCommands.spec.ts | 12 + src/package.nls.ca.json | 2 +- src/package.nls.de.json | 2 +- src/package.nls.es.json | 2 +- src/package.nls.fr.json | 2 +- src/package.nls.hi.json | 2 +- src/package.nls.id.json | 2 +- src/package.nls.it.json | 2 +- src/package.nls.ja.json | 2 +- src/package.nls.ko.json | 2 +- src/package.nls.nl.json | 2 +- src/package.nls.pl.json | 2 +- src/package.nls.pt-BR.json | 2 +- src/package.nls.ru.json | 2 +- src/package.nls.tr.json | 2 +- src/package.nls.vi.json | 2 +- src/package.nls.zh-CN.json | 2 +- src/package.nls.zh-TW.json | 2 +- .../ripgrep/__tests__/diagnostic.spec.ts | 215 +++++++++++++++++- src/services/ripgrep/diagnostic.ts | 117 ++++++---- src/services/ripgrep/index.ts | 35 ++- 22 files changed, 343 insertions(+), 71 deletions(-) diff --git a/knip.json b/knip.json index 293e728ac5..9037fa042d 100644 --- a/knip.json +++ b/knip.json @@ -21,6 +21,7 @@ "@types/node-cache", "@types/vscode", "@vscode/codicons", + "@vscode/ripgrep", "esbuild-wasm", "sambanova-ai-provider", "tree-sitter-wasms", diff --git a/src/activate/__tests__/registerCommands.spec.ts b/src/activate/__tests__/registerCommands.spec.ts index 0f3a98b5b3..6e25f62eaf 100644 --- a/src/activate/__tests__/registerCommands.spec.ts +++ b/src/activate/__tests__/registerCommands.spec.ts @@ -81,6 +81,10 @@ vi.mock("../../i18n", () => ({ t: (key: string) => key, })) +vi.mock("../../services/ripgrep/diagnostic", () => ({ + registerRipgrepDiagnosticCommand: vi.fn().mockReturnValue({ dispose: vi.fn() }), +})) + describe("getVisibleProviderOrLog", () => { let mockOutputChannel: vscode.OutputChannel @@ -172,6 +176,14 @@ describe("registerCommands handlers", () => { setPanel(undefined, "tab") }) + it("registers the ripgrep diagnostic command and stores its disposable in context.subscriptions", async () => { + const { registerRipgrepDiagnosticCommand } = await import("../../services/ripgrep/diagnostic") + const mock = vi.mocked(registerRipgrepDiagnosticCommand) + const disposable = mock.mock.results[0]?.value + expect(mock).toHaveBeenCalled() + expect(mockContext.subscriptions).toContain(disposable) + }) + it("settingsButtonClicked posts both settingsButtonClicked and didBecomeVisible actions", () => { handlers["zoo-code.settingsButtonClicked"]() diff --git a/src/package.nls.ca.json b/src/package.nls.ca.json index d338a425d6..a6be24e6a4 100644 --- a/src/package.nls.ca.json +++ b/src/package.nls.ca.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Corregir Aquesta Ordre", "command.terminal.explainCommand.title": "Explicar Aquesta Ordre", "command.acceptInput.title": "Acceptar Entrada/Suggeriment", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Mostra el diagnòstic de Ripgrep", "command.toggleAutoApprove.title": "Alternar Auto-Aprovació", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.de.json b/src/package.nls.de.json index e49d57a0b6..e59bf07f53 100644 --- a/src/package.nls.de.json +++ b/src/package.nls.de.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Diesen Befehl Reparieren", "command.terminal.explainCommand.title": "Diesen Befehl Erklären", "command.acceptInput.title": "Eingabe/Vorschlag Akzeptieren", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Ripgrep-Diagnose anzeigen", "command.toggleAutoApprove.title": "Auto-Genehmigung Umschalten", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.es.json b/src/package.nls.es.json index 117c0a70a3..e66066a8c4 100644 --- a/src/package.nls.es.json +++ b/src/package.nls.es.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Corregir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", "command.acceptInput.title": "Aceptar Entrada/Sugerencia", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Mostrar diagnóstico de Ripgrep", "command.toggleAutoApprove.title": "Alternar Auto-Aprobación", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.fr.json b/src/package.nls.fr.json index 26ec85c773..8a68a0ab26 100644 --- a/src/package.nls.fr.json +++ b/src/package.nls.fr.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Corriger cette Commande", "command.terminal.explainCommand.title": "Expliquer cette Commande", "command.acceptInput.title": "Accepter l'Entrée/Suggestion", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Afficher le diagnostic Ripgrep", "command.toggleAutoApprove.title": "Basculer Auto-Approbation", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.hi.json b/src/package.nls.hi.json index 67b716cdb8..7fdd3daa5a 100644 --- a/src/package.nls.hi.json +++ b/src/package.nls.hi.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "यह कमांड ठीक करें", "command.terminal.explainCommand.title": "यह कमांड समझाएं", "command.acceptInput.title": "इनपुट/सुझाव स्वीकारें", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Ripgrep डायग्नोस्टिक दिखाएं", "command.toggleAutoApprove.title": "ऑटो-अनुमोदन टॉगल करें", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.id.json b/src/package.nls.id.json index d2c81e69a9..dc3c04fbe4 100644 --- a/src/package.nls.id.json +++ b/src/package.nls.id.json @@ -23,7 +23,7 @@ "command.terminal.fixCommand.title": "Perbaiki Perintah Ini", "command.terminal.explainCommand.title": "Jelaskan Perintah Ini", "command.acceptInput.title": "Terima Input/Saran", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Tampilkan Diagnostik Ripgrep", "command.toggleAutoApprove.title": "Alihkan Persetujuan Otomatis", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "Perintah yang dapat dijalankan secara otomatis ketika 'Selalu setujui operasi eksekusi' diaktifkan", diff --git a/src/package.nls.it.json b/src/package.nls.it.json index dad1b5641e..8cbe4a6e67 100644 --- a/src/package.nls.it.json +++ b/src/package.nls.it.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Correggi Questo Comando", "command.terminal.explainCommand.title": "Spiega Questo Comando", "command.acceptInput.title": "Accetta Input/Suggerimento", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Mostra diagnostica Ripgrep", "command.toggleAutoApprove.title": "Attiva/Disattiva Auto-Approvazione", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.ja.json b/src/package.nls.ja.json index 32633b7481..aa7a6d11e9 100644 --- a/src/package.nls.ja.json +++ b/src/package.nls.ja.json @@ -23,7 +23,7 @@ "command.terminal.fixCommand.title": "このコマンドを修正", "command.terminal.explainCommand.title": "このコマンドを説明", "command.acceptInput.title": "入力/提案を承認", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Ripgrep 診断を表示", "command.toggleAutoApprove.title": "自動承認を切替", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "'常に実行操作を承認する'が有効な場合に自動実行できるコマンド", diff --git a/src/package.nls.ko.json b/src/package.nls.ko.json index 740d92cba3..378d1cbbd4 100644 --- a/src/package.nls.ko.json +++ b/src/package.nls.ko.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "이 명령어 수정", "command.terminal.explainCommand.title": "이 명령어 설명", "command.acceptInput.title": "입력/제안 수락", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Ripgrep 진단 표시", "command.toggleAutoApprove.title": "자동 승인 전환", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.nl.json b/src/package.nls.nl.json index f60d3b5a90..8842c1ae9c 100644 --- a/src/package.nls.nl.json +++ b/src/package.nls.nl.json @@ -23,7 +23,7 @@ "command.terminal.fixCommand.title": "Repareer Dit Commando", "command.terminal.explainCommand.title": "Leg Dit Commando Uit", "command.acceptInput.title": "Invoer/Suggestie Accepteren", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Ripgrep-diagnose weergeven", "command.toggleAutoApprove.title": "Auto-Goedkeuring Schakelen", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "Commando's die automatisch kunnen worden uitgevoerd wanneer 'Altijd goedkeuren uitvoerbewerkingen' is ingeschakeld", diff --git a/src/package.nls.pl.json b/src/package.nls.pl.json index 92f152f7a3..ecba1da6d6 100644 --- a/src/package.nls.pl.json +++ b/src/package.nls.pl.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Napraw tę Komendę", "command.terminal.explainCommand.title": "Wyjaśnij tę Komendę", "command.acceptInput.title": "Akceptuj Wprowadzanie/Sugestię", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Pokaż diagnostykę Ripgrep", "command.toggleAutoApprove.title": "Przełącz Auto-Zatwierdzanie", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.pt-BR.json b/src/package.nls.pt-BR.json index dbd554a9f9..14c717d6c3 100644 --- a/src/package.nls.pt-BR.json +++ b/src/package.nls.pt-BR.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Corrigir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", "command.acceptInput.title": "Aceitar Entrada/Sugestão", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Mostrar diagnóstico do Ripgrep", "command.toggleAutoApprove.title": "Alternar Auto-Aprovação", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.ru.json b/src/package.nls.ru.json index 2e73b981c0..b4c6cf778d 100644 --- a/src/package.nls.ru.json +++ b/src/package.nls.ru.json @@ -23,7 +23,7 @@ "command.terminal.fixCommand.title": "Исправить эту команду", "command.terminal.explainCommand.title": "Объяснить эту команду", "command.acceptInput.title": "Принять ввод/предложение", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Показать диагностику Ripgrep", "command.toggleAutoApprove.title": "Переключить Авто-Подтверждение", "configuration.title": "Zoo Code", "commands.allowedCommands.description": "Команды, которые могут быть автоматически выполнены, когда включена опция 'Всегда подтверждать операции выполнения'", diff --git a/src/package.nls.tr.json b/src/package.nls.tr.json index bee9b50177..5f7cd02787 100644 --- a/src/package.nls.tr.json +++ b/src/package.nls.tr.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Bu Komutu Düzelt", "command.terminal.explainCommand.title": "Bu Komutu Açıkla", "command.acceptInput.title": "Girişi/Öneriyi Kabul Et", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Ripgrep Tanılamasını Göster", "command.toggleAutoApprove.title": "Otomatik Onayı Değiştir", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.vi.json b/src/package.nls.vi.json index 0ce3173f8d..e750a3dee9 100644 --- a/src/package.nls.vi.json +++ b/src/package.nls.vi.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "Sửa Lệnh Này", "command.terminal.explainCommand.title": "Giải Thích Lệnh Này", "command.acceptInput.title": "Chấp Nhận Đầu Vào/Gợi Ý", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "Hiển thị chẩn đoán Ripgrep", "command.toggleAutoApprove.title": "Bật/Tắt Tự Động Phê Duyệt", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.zh-CN.json b/src/package.nls.zh-CN.json index b24bf6aff6..a70a38af7d 100644 --- a/src/package.nls.zh-CN.json +++ b/src/package.nls.zh-CN.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "修复此命令", "command.terminal.explainCommand.title": "解释此命令", "command.acceptInput.title": "接受输入/建议", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "显示 Ripgrep 诊断", "command.toggleAutoApprove.title": "切换自动批准", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/package.nls.zh-TW.json b/src/package.nls.zh-TW.json index 6ef6499998..989be5e22c 100644 --- a/src/package.nls.zh-TW.json +++ b/src/package.nls.zh-TW.json @@ -14,7 +14,7 @@ "command.terminal.fixCommand.title": "修復此命令", "command.terminal.explainCommand.title": "解釋此命令", "command.acceptInput.title": "接受輸入/建議", - "command.showRipgrepDiagnostic.title": "Show Ripgrep Diagnostic", + "command.showRipgrepDiagnostic.title": "顯示 Ripgrep 診斷", "command.toggleAutoApprove.title": "切換自動批准", "views.activitybar.title": "Zoo Code", "views.contextMenu.label": "Zoo Code", diff --git a/src/services/ripgrep/__tests__/diagnostic.spec.ts b/src/services/ripgrep/__tests__/diagnostic.spec.ts index cb5eed5b4a..736388efbf 100644 --- a/src/services/ripgrep/__tests__/diagnostic.spec.ts +++ b/src/services/ripgrep/__tests__/diagnostic.spec.ts @@ -1,10 +1,13 @@ // npx vitest run src/services/ripgrep/__tests__/diagnostic.spec.ts import * as path from "path" +import { EventEmitter } from "events" import { vi, describe, it, expect, beforeEach } from "vitest" +import * as vscode from "vscode" +import * as childProcess from "child_process" -import { getRipgrepDiagnostic } from "../diagnostic" +import { getRipgrepDiagnostic, registerRipgrepDiagnosticCommand, trySpawnRipgrep } from "../diagnostic" const ripgrepMock = vi.hoisted(() => ({ value: undefined as { rgPath?: string; loadError?: string } | undefined, @@ -14,6 +17,36 @@ const fsMock = vi.hoisted(() => ({ existing: new Set(), })) +const getBinPathMock = vi.hoisted(() => ({ value: undefined as string | undefined })) + +type SpawnResult = Awaited> +const spawnMock = vi.hoisted(() => ({ + result: { stdout: "ripgrep 14.1.0", exitCode: 0 } as SpawnResult, +})) + +function makeFakeProc(result: SpawnResult) { + const proc = new EventEmitter() as EventEmitter & { + stdout: EventEmitter + stderr: EventEmitter + kill: () => void + } + proc.stdout = new EventEmitter() + proc.stderr = new EventEmitter() + proc.kill = vi.fn() + setImmediate(() => { + if (result.spawnError) { + proc.emit("error", new Error(result.spawnError)) + } else if (result.timedOut) { + proc.emit("close", null, "SIGTERM") + } else { + if (result.stdout) proc.stdout.emit("data", Buffer.from(result.stdout)) + if (result.stderr) proc.stderr.emit("data", Buffer.from(result.stderr)) + proc.emit("close", result.exitCode ?? 0, null) + } + }) + return proc +} + vi.mock("../internal/loadRipgrep", () => ({ loadRipgrep: () => ripgrepMock.value, })) @@ -22,6 +55,41 @@ vi.mock("../../../utils/fs", () => ({ fileExistsAtPath: (p: string) => Promise.resolve(fsMock.existing.has(p)), })) +vi.mock("../index", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getBinPath: () => Promise.resolve(getBinPathMock.value), + } +}) + +vi.mock("child_process", async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, spawn: vi.fn() } +}) + +const mockChannel = { + clear: vi.fn(), + appendLine: vi.fn(), + show: vi.fn(), + dispose: vi.fn(), +} + +const mockCommand = { dispose: vi.fn() } + +vi.mock("vscode", () => ({ + version: "1.99.0", + env: { appRoot: "/mock/appRoot", clipboard: { writeText: vi.fn() } }, + window: { + createOutputChannel: vi.fn(() => mockChannel), + showInformationMessage: vi.fn(), + }, + commands: { registerCommand: vi.fn(() => mockCommand) }, + Disposable: { + from: vi.fn((...items: { dispose(): void }[]) => ({ dispose: () => items.forEach((d) => d.dispose()) })), + }, +})) + const APP_ROOT = "/app" const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" @@ -40,6 +108,9 @@ describe("getRipgrepDiagnostic", () => { beforeEach(() => { ripgrepMock.value = undefined fsMock.existing = new Set() + getBinPathMock.value = undefined + spawnMock.result = { stdout: "ripgrep 14.1.0", exitCode: 0 } + vi.mocked(childProcess.spawn).mockImplementation(() => makeFakeProc(spawnMock.result) as any) }) it("includes rgPath and fileExistsAtPath: true when loadRipgrep returns an existing path", async () => { @@ -149,8 +220,146 @@ describe("getRipgrepDiagnostic", () => { const report = await getRipgrepDiagnostic("") expect(report).toContain("vscode.env.appRoot: (empty)") - expect(report).toContain("Cannot run diagnostic") - // path probe should NOT have run + expect(report).toContain("Cannot probe paths: vscode.env.appRoot is empty.") + // step 1 should still run + expect(report).toContain('--- step 1: require("@vscode/ripgrep") via loadRipgrep ---') + // path probe and spawn test should NOT have run expect(report).not.toContain("--- step 2: path probe under appRoot ---") + expect(report).not.toContain("--- step 3: spawn rg --version on selected path ---") + }) + + it("reports getBinPath() undefined when no candidate exists", async () => { + getBinPathMock.value = undefined + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain("--- step 3: spawn rg --version on selected path ---") + expect(report).toContain("getBinPath() returned undefined") + }) + + it("reports spawn success with exit code and stdout", async () => { + getBinPathMock.value = expectedCandidates[4] + fsMock.existing = new Set([expectedCandidates[4]]) + spawnMock.result = { stdout: "ripgrep 14.1.0", exitCode: 0 } + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain(`getBinPath() selected: ${expectedCandidates[4]}`) + expect(report).toContain(`selectedPath JSON: ${JSON.stringify(expectedCandidates[4])}`) + expect(report).toContain("exit code: 0") + expect(report).toContain("stdout: ripgrep 14.1.0") + }) + + it("reports spawn ENOENT — the file-exists-but-spawn-fails failure mode", async () => { + getBinPathMock.value = expectedCandidates[4] + fsMock.existing = new Set([expectedCandidates[4]]) + spawnMock.result = { spawnError: "spawn ENOENT" } + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain("spawn error: spawn ENOENT") + expect(report).not.toContain("exit code:") + }) + + it("passes the selected path, ['--version'], and timeout option to spawn", async () => { + getBinPathMock.value = expectedCandidates[4] + fsMock.existing = new Set([expectedCandidates[4]]) + + await getRipgrepDiagnostic(APP_ROOT) + + expect(vi.mocked(childProcess.spawn)).toHaveBeenCalledWith(expectedCandidates[4], ["--version"], { + timeout: 5_000, + }) + }) + + it("reports timed out when the process is killed with SIGTERM", async () => { + getBinPathMock.value = expectedCandidates[4] + fsMock.existing = new Set([expectedCandidates[4]]) + spawnMock.result = { timedOut: true } + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain("timed out after 5000ms") + expect(report).not.toContain("exit code:") + }) + + it("reports stderr when the process exits with a non-zero code", async () => { + getBinPathMock.value = expectedCandidates[4] + fsMock.existing = new Set([expectedCandidates[4]]) + spawnMock.result = { exitCode: 1, stderr: "error: unrecognised flag" } + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain("exit code: 1") + expect(report).toContain("stderr: error: unrecognised flag") + }) + + it("omits the stderr line when stderr is empty", async () => { + getBinPathMock.value = expectedCandidates[4] + fsMock.existing = new Set([expectedCandidates[4]]) + spawnMock.result = { stdout: "ripgrep 14.1.0", exitCode: 0, stderr: "" } + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).not.toContain("stderr:") + }) + + it("includes JSON-escaped path for the rgPath from loadRipgrep", async () => { + ripgrepMock.value = { rgPath: "/path/with spaces/rg" } + fsMock.existing = new Set(["/path/with spaces/rg"]) + + const report = await getRipgrepDiagnostic(APP_ROOT) + + expect(report).toContain(`rgPath JSON: ${JSON.stringify("/path/with spaces/rg")}`) + }) +}) + +describe("registerRipgrepDiagnosticCommand", () => { + beforeEach(() => { + vi.clearAllMocks() + ripgrepMock.value = undefined + fsMock.existing = new Set() + }) + + it("creates an output channel and registers the command", () => { + registerRipgrepDiagnosticCommand() + + expect(vscode.window.createOutputChannel).toHaveBeenCalledWith("Zoo Code Ripgrep Diagnostic") + expect(vscode.commands.registerCommand).toHaveBeenCalledWith( + expect.stringContaining("showRipgrepDiagnostic"), + expect.any(Function), + ) + }) + + it("returns a Disposable that disposes both the command and channel", () => { + const disposable = registerRipgrepDiagnosticCommand() + disposable.dispose() + + expect(vscode.Disposable.from).toHaveBeenCalledWith(mockCommand, mockChannel) + }) + + it("clears, appends, and shows the channel when the command runs", async () => { + registerRipgrepDiagnosticCommand() + + const [[, handler]] = vi.mocked(vscode.commands.registerCommand).mock.calls + await handler() + + expect(mockChannel.clear).toHaveBeenCalled() + expect(mockChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining("Zoo Code Ripgrep Diagnostic")) + expect(mockChannel.show).toHaveBeenCalledWith(true) + }) + + it("writes the report to the clipboard and shows a toast when the command runs", async () => { + registerRipgrepDiagnosticCommand() + + const [[, handler]] = vi.mocked(vscode.commands.registerCommand).mock.calls + await handler() + + expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith( + expect.stringContaining("Zoo Code Ripgrep Diagnostic"), + ) + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + "Zoo Code: ripgrep diagnostic copied to clipboard.", + ) }) }) diff --git a/src/services/ripgrep/diagnostic.ts b/src/services/ripgrep/diagnostic.ts index 66602441b8..678f0f7c71 100644 --- a/src/services/ripgrep/diagnostic.ts +++ b/src/services/ripgrep/diagnostic.ts @@ -1,28 +1,48 @@ -import * as path from "path" +import * as childProcess from "child_process" import * as vscode from "vscode" import { fileExistsAtPath } from "../../utils/fs" import { getCommand } from "../../utils/commands" import { loadRipgrep } from "./internal/loadRipgrep" +import { getBinPath, ripgrepCandidatePaths } from "./index" -const binName = process.platform.startsWith("win") ? "rg.exe" : "rg" -const universalBin = `bin/${process.platform}-${process.arch}/${binName}` +const SPAWN_TIMEOUT_MS = 5_000 -function probeCandidates(vscodeAppRoot: string): readonly string[] { - return [ - path.join(vscodeAppRoot, "node_modules", "@vscode", "ripgrep", "bin", binName), - path.join(vscodeAppRoot, "node_modules", "vscode-ripgrep", "bin", binName), - path.join(vscodeAppRoot, "node_modules.asar.unpacked", "vscode-ripgrep", "bin", binName), - path.join(vscodeAppRoot, "node_modules.asar.unpacked", "@vscode", "ripgrep", "bin", binName), - path.join(vscodeAppRoot, "node_modules", "@vscode", "ripgrep-universal", ...universalBin.split("/")), - path.join( - vscodeAppRoot, - "node_modules.asar.unpacked", - "@vscode", - "ripgrep-universal", - ...universalBin.split("/"), - ), - ] +/** + * Attempts `rg --version` with a 5 s timeout. + * Returns { stdout, stderr, exitCode } on normal exit, { timedOut } on timeout, + * or { spawnError } when spawn itself fails (e.g. ENOENT). + */ +export function trySpawnRipgrep(rgPath: string): Promise<{ + stdout?: string + stderr?: string + exitCode?: number + timedOut?: true + spawnError?: string +}> { + return new Promise((resolve) => { + let proc: childProcess.ChildProcess + try { + proc = childProcess.spawn(rgPath, ["--version"], { timeout: SPAWN_TIMEOUT_MS }) + } catch (err) { + resolve({ spawnError: err instanceof Error ? err.message : String(err) }) + return + } + + let stdout = "" + let stderr = "" + proc.stdout?.on("data", (d: Buffer) => (stdout += d.toString())) + proc.stderr?.on("data", (d: Buffer) => (stderr += d.toString())) + + proc.on("error", (err) => resolve({ spawnError: err.message })) + proc.on("close", (code, signal) => { + if (signal === "SIGTERM" && code === null) { + resolve({ timedOut: true }) + } else { + resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code ?? -1 }) + } + }) + }) } /** @@ -34,29 +54,21 @@ function probeCandidates(vscodeAppRoot: string): readonly string[] { * extHost interceptor on builds that have completed the * `@vscode/ripgrep` → `@vscode/ripgrep-universal` migration). * Step 2 probes every known `vscode.env.appRoot`-relative path and - * reports which ones exist on disk. + * reports which ones exist on disk. Step 2 is skipped when appRoot is empty. + * Step 3 runs `rg --version` on the path getBinPath() would select, directly + * testing whether spawn succeeds (the failure mode Naved reported). */ export async function getRipgrepDiagnostic(vscodeAppRoot: string): Promise { - if (!vscodeAppRoot || vscodeAppRoot.trim() === "") { - return [ - `Zoo Code Ripgrep Diagnostic (${new Date().toISOString()})`, - `vscode.version: ${vscode.version}`, - `vscode.env.appRoot: (empty)`, - ``, - `Cannot run diagnostic: vscode.env.appRoot is empty. This usually means`, - `the extension activated outside a VS Code-style host (e.g., a remote`, - `extension host that doesn't expose appRoot). The require-interceptor`, - `path may still work; the path-probe step requires a valid appRoot.`, - ].join("\n") - } + const appRootEmpty = !vscodeAppRoot || vscodeAppRoot.trim() === "" const lines: string[] = [ `Zoo Code Ripgrep Diagnostic (${new Date().toISOString()})`, `vscode.version: ${vscode.version}`, - `vscode.env.appRoot: ${vscodeAppRoot}`, - `process.platform/arch: ${process.platform}/${process.arch}`, + `vscode.env.appRoot: ${appRootEmpty ? "(empty)" : vscodeAppRoot}`, + ...(appRootEmpty ? [] : [`process.platform/arch: ${process.platform}/${process.arch}`]), ``, `--- step 1: require("@vscode/ripgrep") via loadRipgrep ---`, ] + const m = loadRipgrep() if (!m) { lines.push(`loadRipgrep() returned undefined (require threw)`) @@ -66,6 +78,7 @@ export async function getRipgrepDiagnostic(vscodeAppRoot: string): Promise { @@ -102,4 +142,3 @@ export function registerRipgrepDiagnosticCommand(): vscode.Disposable { }) return vscode.Disposable.from(command, channel) } -/* c8 ignore stop */ diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 59cdb03cf9..99d91faad3 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -84,6 +84,26 @@ const MAX_LINE_LENGTH = 500 export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string { return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line } +/** + * Returns the ordered list of absolute candidate paths where ripgrep may + * live under the given VS Code appRoot. Used by both getBinPath (first-match + * resolution) and the diagnostic command (existence report for all paths). + */ +export function ripgrepCandidatePaths(vscodeAppRoot: string): readonly string[] { + return [ + path.join(vscodeAppRoot, "node_modules/@vscode/ripgrep/bin/", binName), + path.join(vscodeAppRoot, "node_modules/vscode-ripgrep/bin", binName), + path.join(vscodeAppRoot, "node_modules.asar.unpacked/vscode-ripgrep/bin/", binName), + path.join(vscodeAppRoot, "node_modules.asar.unpacked/@vscode/ripgrep/bin/", binName), + path.join(vscodeAppRoot, `node_modules/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`, binName), + path.join( + vscodeAppRoot, + `node_modules.asar.unpacked/@vscode/ripgrep-universal/${ripgrepUniversalBinDir}`, + binName, + ), + ] +} + /** * Get the path to the ripgrep binary shipped inside the VS Code installation. * @@ -94,19 +114,10 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): * Returns `undefined` when ripgrep cannot be located. */ 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 + for (const candidate of ripgrepCandidatePaths(vscodeAppRoot)) { + if (await fileExistsAtPath(candidate)) return candidate } - - 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}`)) - ) + return undefined } async function execRipgrep(bin: string, args: string[]): Promise {