refactor: unwrap Ripgrep namespace + self-reexport (#22965)

This commit is contained in:
Kit Langton
2026-04-16 19:49:52 -04:00
committed by GitHub
parent e2d161dfdd
commit 30fc791480
+85 -85
View File
@@ -8,10 +8,9 @@ import { ripgrep } from "ripgrep"
import { Filesystem } from "@/util" import { Filesystem } from "@/util"
import { Log } from "@/util" import { Log } from "@/util"
export namespace Ripgrep { const log = Log.create({ service: "ripgrep" })
const log = Log.create({ service: "ripgrep" })
const Stats = z.object({ const Stats = z.object({
elapsed: z.object({ elapsed: z.object({
secs: z.number(), secs: z.number(),
nanos: z.number(), nanos: z.number(),
@@ -23,18 +22,18 @@ export namespace Ripgrep {
bytes_printed: z.number(), bytes_printed: z.number(),
matched_lines: z.number(), matched_lines: z.number(),
matches: z.number(), matches: z.number(),
}) })
const Begin = z.object({ const Begin = z.object({
type: z.literal("begin"), type: z.literal("begin"),
data: z.object({ data: z.object({
path: z.object({ path: z.object({
text: z.string(), text: z.string(),
}), }),
}), }),
}) })
export const Match = z.object({ export const Match = z.object({
type: z.literal("match"), type: z.literal("match"),
data: z.object({ data: z.object({
path: z.object({ path: z.object({
@@ -55,9 +54,9 @@ export namespace Ripgrep {
}), }),
), ),
}), }),
}) })
const End = z.object({ const End = z.object({
type: z.literal("end"), type: z.literal("end"),
data: z.object({ data: z.object({
path: z.object({ path: z.object({
@@ -66,9 +65,9 @@ export namespace Ripgrep {
binary_offset: z.number().nullable(), binary_offset: z.number().nullable(),
stats: Stats, stats: Stats,
}), }),
}) })
const Summary = z.object({ const Summary = z.object({
type: z.literal("summary"), type: z.literal("summary"),
data: z.object({ data: z.object({
elapsed_total: z.object({ elapsed_total: z.object({
@@ -78,33 +77,33 @@ export namespace Ripgrep {
}), }),
stats: Stats, stats: Stats,
}), }),
}) })
const Result = z.union([Begin, Match, End, Summary]) const Result = z.union([Begin, Match, End, Summary])
export type Result = z.infer<typeof Result> export type Result = z.infer<typeof Result>
export type Match = z.infer<typeof Match> export type Match = z.infer<typeof Match>
export type Item = Match["data"] export type Item = Match["data"]
export type Begin = z.infer<typeof Begin> export type Begin = z.infer<typeof Begin>
export type End = z.infer<typeof End> export type End = z.infer<typeof End>
export type Summary = z.infer<typeof Summary> export type Summary = z.infer<typeof Summary>
export type Row = Match["data"] export type Row = Match["data"]
export interface SearchResult { export interface SearchResult {
items: Item[] items: Item[]
partial: boolean partial: boolean
} }
export interface FilesInput { export interface FilesInput {
cwd: string cwd: string
glob?: string[] glob?: string[]
hidden?: boolean hidden?: boolean
follow?: boolean follow?: boolean
maxDepth?: number maxDepth?: number
signal?: AbortSignal signal?: AbortSignal
} }
export interface SearchInput { export interface SearchInput {
cwd: string cwd: string
pattern: string pattern: string
glob?: string[] glob?: string[]
@@ -112,91 +111,91 @@ export namespace Ripgrep {
follow?: boolean follow?: boolean
file?: string[] file?: string[]
signal?: AbortSignal signal?: AbortSignal
} }
export interface TreeInput { export interface TreeInput {
cwd: string cwd: string
limit?: number limit?: number
signal?: AbortSignal signal?: AbortSignal
} }
export interface Interface { export interface Interface {
readonly files: (input: FilesInput) => Stream.Stream<string, Error> readonly files: (input: FilesInput) => Stream.Stream<string, Error>
readonly tree: (input: TreeInput) => Effect.Effect<string, Error> readonly tree: (input: TreeInput) => Effect.Effect<string, Error>
readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error> readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
type Run = { kind: "files" | "search"; cwd: string; args: string[] } type Run = { kind: "files" | "search"; cwd: string; args: string[] }
type WorkerResult = { type WorkerResult = {
type: "result" type: "result"
code: number code: number
stdout: string stdout: string
stderr: string stderr: string
} }
type WorkerLine = { type WorkerLine = {
type: "line" type: "line"
line: string line: string
} }
type WorkerDone = { type WorkerDone = {
type: "done" type: "done"
code: number code: number
stderr: string stderr: string
} }
type WorkerError = { type WorkerError = {
type: "error" type: "error"
error: { error: {
message: string message: string
name?: string name?: string
stack?: string stack?: string
} }
} }
function env() { function env() {
const env = Object.fromEntries( const env = Object.fromEntries(
Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined), Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
) )
delete env.RIPGREP_CONFIG_PATH delete env.RIPGREP_CONFIG_PATH
return env return env
} }
function text(input: unknown) { function text(input: unknown) {
if (typeof input === "string") return input if (typeof input === "string") return input
if (input instanceof ArrayBuffer) return Buffer.from(input).toString() if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString() if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
return String(input) return String(input)
} }
function toError(input: unknown) { function toError(input: unknown) {
if (input instanceof Error) return input if (input instanceof Error) return input
if (typeof input === "string") return new Error(input) if (typeof input === "string") return new Error(input)
return new Error(String(input)) return new Error(String(input))
} }
function abort(signal?: AbortSignal) { function abort(signal?: AbortSignal) {
const err = signal?.reason const err = signal?.reason
if (err instanceof Error) return err if (err instanceof Error) return err
const out = new Error("Aborted") const out = new Error("Aborted")
out.name = "AbortError" out.name = "AbortError"
return out return out
} }
function error(stderr: string, code: number) { function error(stderr: string, code: number) {
const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`) const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`)
err.name = "RipgrepError" err.name = "RipgrepError"
return err return err
} }
function clean(file: string) { function clean(file: string) {
return path.normalize(file.replace(/^\.[\\/]/, "")) return path.normalize(file.replace(/^\.[\\/]/, ""))
} }
function row(data: Row): Row { function row(data: Row): Row {
return { return {
...data, ...data,
path: { path: {
@@ -204,16 +203,16 @@ export namespace Ripgrep {
text: clean(data.path.text), text: clean(data.path.text),
}, },
} }
} }
function opts(cwd: string) { function opts(cwd: string) {
return { return {
env: env(), env: env(),
preopens: { ".": cwd }, preopens: { ".": cwd },
} }
} }
function check(cwd: string) { function check(cwd: string) {
return Effect.tryPromise({ return Effect.tryPromise({
try: () => fs.stat(cwd).catch(() => undefined), try: () => fs.stat(cwd).catch(() => undefined),
catch: toError, catch: toError,
@@ -230,9 +229,9 @@ export namespace Ripgrep {
), ),
), ),
) )
} }
function filesArgs(input: FilesInput) { function filesArgs(input: FilesInput) {
const args = ["--files", "--glob=!.git/*"] const args = ["--files", "--glob=!.git/*"]
if (input.follow) args.push("--follow") if (input.follow) args.push("--follow")
if (input.hidden !== false) args.push("--hidden") if (input.hidden !== false) args.push("--hidden")
@@ -244,9 +243,9 @@ export namespace Ripgrep {
} }
args.push(".") args.push(".")
return args return args
} }
function searchArgs(input: SearchInput) { function searchArgs(input: SearchInput) {
const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"] const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"]
if (input.follow) args.push("--follow") if (input.follow) args.push("--follow")
if (input.glob) { if (input.glob) {
@@ -257,20 +256,20 @@ export namespace Ripgrep {
if (input.limit) args.push(`--max-count=${input.limit}`) if (input.limit) args.push(`--max-count=${input.limit}`)
args.push("--", input.pattern, ...(input.file ?? ["."])) args.push("--", input.pattern, ...(input.file ?? ["."]))
return args return args
} }
function parse(stdout: string) { function parse(stdout: string) {
return stdout return stdout
.trim() .trim()
.split(/\r?\n/) .split(/\r?\n/)
.filter(Boolean) .filter(Boolean)
.map((line) => Result.parse(JSON.parse(line))) .map((line) => Result.parse(JSON.parse(line)))
.flatMap((item) => (item.type === "match" ? [row(item.data)] : [])) .flatMap((item) => (item.type === "match" ? [row(item.data)] : []))
} }
declare const OPENCODE_RIPGREP_WORKER_PATH: string declare const OPENCODE_RIPGREP_WORKER_PATH: string
function target(): Effect.Effect<string | URL, Error> { function target(): Effect.Effect<string | URL, Error> {
if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") { if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") {
return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH) return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
} }
@@ -279,26 +278,26 @@ export namespace Ripgrep {
try: () => Filesystem.exists(fileURLToPath(js)), try: () => Filesystem.exists(fileURLToPath(js)),
catch: toError, catch: toError,
}).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url)))) }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
} }
function worker() { function worker() {
return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() })))) return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() }))))
} }
function drain(buf: string, chunk: unknown, push: (line: string) => void) { function drain(buf: string, chunk: unknown, push: (line: string) => void) {
const lines = (buf + text(chunk)).split(/\r?\n/) const lines = (buf + text(chunk)).split(/\r?\n/)
buf = lines.pop() || "" buf = lines.pop() || ""
for (const line of lines) { for (const line of lines) {
if (line) push(line) if (line) push(line)
} }
return buf return buf
} }
function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) { function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
Queue.failCauseUnsafe(queue, Cause.fail(err)) Queue.failCauseUnsafe(queue, Cause.fail(err))
} }
function searchDirect(input: SearchInput) { function searchDirect(input: SearchInput) {
return Effect.tryPromise({ return Effect.tryPromise({
try: () => try: () =>
ripgrep(searchArgs(input), { ripgrep(searchArgs(input), {
@@ -318,9 +317,9 @@ export namespace Ripgrep {
})) }))
}), }),
) )
} }
function searchWorker(input: SearchInput) { function searchWorker(input: SearchInput) {
if (input.signal?.aborted) return Effect.fail(abort(input.signal)) if (input.signal?.aborted) return Effect.fail(abort(input.signal))
return Effect.acquireUseRelease( return Effect.acquireUseRelease(
@@ -377,9 +376,9 @@ export namespace Ripgrep {
}), }),
(w) => Effect.sync(() => w.terminate()), (w) => Effect.sync(() => w.terminate()),
) )
} }
function filesDirect(input: FilesInput) { function filesDirect(input: FilesInput) {
return Stream.callback<string, Error>( return Stream.callback<string, Error>(
Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) { Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
let buf = "" let buf = ""
@@ -427,9 +426,9 @@ export namespace Ripgrep {
) )
}), }),
) )
} }
function filesWorker(input: FilesInput) { function filesWorker(input: FilesInput) {
return Stream.callback<string, Error>( return Stream.callback<string, Error>(
Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) { Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
if (input.signal?.aborted) { if (input.signal?.aborted) {
@@ -489,9 +488,9 @@ export namespace Ripgrep {
) )
}), }),
) )
} }
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const source = (input: FilesInput) => { const source = (input: FilesInput) => {
@@ -569,7 +568,8 @@ export namespace Ripgrep {
return Service.of({ files, tree, search }) return Service.of({ files, tree, search })
}), }),
) )
export const defaultLayer = layer export const defaultLayer = layer
}
export * as Ripgrep from "./ripgrep"