summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-16 19:49:52 -0400
committerGitHub <[email protected]>2026-04-16 19:49:52 -0400
commit30fc791480ebdabc9c62c70713e6cb52b44caff2 (patch)
tree325c3902b529d55f8564df2cbbc76bcef47080f9
parente2d161dfdd54fdd30f8e36e8cf4f46e261dab96e (diff)
downloadopencode-30fc791480ebdabc9c62c70713e6cb52b44caff2.tar.gz
opencode-30fc791480ebdabc9c62c70713e6cb52b44caff2.zip
refactor: unwrap Ripgrep namespace + self-reexport (#22965)
-rw-r--r--packages/opencode/src/file/ripgrep.ts972
1 files changed, 486 insertions, 486 deletions
diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts
index 9a78c5b7f..ac450108e 100644
--- a/packages/opencode/src/file/ripgrep.ts
+++ b/packages/opencode/src/file/ripgrep.ts
@@ -8,568 +8,568 @@ import { ripgrep } from "ripgrep"
import { Filesystem } from "@/util"
import { Log } from "@/util"
-export namespace Ripgrep {
- const log = Log.create({ service: "ripgrep" })
-
- const Stats = z.object({
- elapsed: z.object({
- secs: z.number(),
- nanos: z.number(),
- human: z.string(),
+const log = Log.create({ service: "ripgrep" })
+
+const Stats = z.object({
+ elapsed: z.object({
+ secs: z.number(),
+ nanos: z.number(),
+ human: z.string(),
+ }),
+ searches: z.number(),
+ searches_with_match: z.number(),
+ bytes_searched: z.number(),
+ bytes_printed: z.number(),
+ matched_lines: z.number(),
+ matches: z.number(),
+})
+
+const Begin = z.object({
+ type: z.literal("begin"),
+ data: z.object({
+ path: z.object({
+ text: z.string(),
}),
- searches: z.number(),
- searches_with_match: z.number(),
- bytes_searched: z.number(),
- bytes_printed: z.number(),
- matched_lines: z.number(),
- matches: z.number(),
- })
-
- const Begin = z.object({
- type: z.literal("begin"),
- data: z.object({
- path: z.object({
- text: z.string(),
- }),
+ }),
+})
+
+export const Match = z.object({
+ type: z.literal("match"),
+ data: z.object({
+ path: z.object({
+ text: z.string(),
}),
- })
-
- export const Match = z.object({
- type: z.literal("match"),
- data: z.object({
- path: z.object({
- text: z.string(),
- }),
- lines: z.object({
- text: z.string(),
- }),
- line_number: z.number(),
- absolute_offset: z.number(),
- submatches: z.array(
- z.object({
- match: z.object({
- text: z.string(),
- }),
- start: z.number(),
- end: z.number(),
- }),
- ),
+ lines: z.object({
+ text: z.string(),
}),
- })
-
- const End = z.object({
- type: z.literal("end"),
- data: z.object({
- path: z.object({
- text: z.string(),
+ line_number: z.number(),
+ absolute_offset: z.number(),
+ submatches: z.array(
+ z.object({
+ match: z.object({
+ text: z.string(),
+ }),
+ start: z.number(),
+ end: z.number(),
}),
- binary_offset: z.number().nullable(),
- stats: Stats,
+ ),
+ }),
+})
+
+const End = z.object({
+ type: z.literal("end"),
+ data: z.object({
+ path: z.object({
+ text: z.string(),
}),
- })
-
- const Summary = z.object({
- type: z.literal("summary"),
- data: z.object({
- elapsed_total: z.object({
- human: z.string(),
- nanos: z.number(),
- secs: z.number(),
- }),
- stats: Stats,
+ binary_offset: z.number().nullable(),
+ stats: Stats,
+ }),
+})
+
+const Summary = z.object({
+ type: z.literal("summary"),
+ data: z.object({
+ elapsed_total: z.object({
+ human: z.string(),
+ nanos: z.number(),
+ secs: z.number(),
}),
- })
+ stats: Stats,
+ }),
+})
+
+const Result = z.union([Begin, Match, End, Summary])
+
+export type Result = z.infer<typeof Result>
+export type Match = z.infer<typeof Match>
+export type Item = Match["data"]
+export type Begin = z.infer<typeof Begin>
+export type End = z.infer<typeof End>
+export type Summary = z.infer<typeof Summary>
+export type Row = Match["data"]
+
+export interface SearchResult {
+ items: Item[]
+ partial: boolean
+}
- const Result = z.union([Begin, Match, End, Summary])
+export interface FilesInput {
+ cwd: string
+ glob?: string[]
+ hidden?: boolean
+ follow?: boolean
+ maxDepth?: number
+ signal?: AbortSignal
+}
- export type Result = z.infer<typeof Result>
- export type Match = z.infer<typeof Match>
- export type Item = Match["data"]
- export type Begin = z.infer<typeof Begin>
- export type End = z.infer<typeof End>
- export type Summary = z.infer<typeof Summary>
- export type Row = Match["data"]
+export interface SearchInput {
+ cwd: string
+ pattern: string
+ glob?: string[]
+ limit?: number
+ follow?: boolean
+ file?: string[]
+ signal?: AbortSignal
+}
- export interface SearchResult {
- items: Item[]
- partial: boolean
- }
+export interface TreeInput {
+ cwd: string
+ limit?: number
+ signal?: AbortSignal
+}
- export interface FilesInput {
- cwd: string
- glob?: string[]
- hidden?: boolean
- follow?: boolean
- maxDepth?: number
- signal?: AbortSignal
- }
+export interface Interface {
+ readonly files: (input: FilesInput) => Stream.Stream<string, Error>
+ readonly tree: (input: TreeInput) => Effect.Effect<string, Error>
+ readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error>
+}
- export interface SearchInput {
- cwd: string
- pattern: string
- glob?: string[]
- limit?: number
- follow?: boolean
- file?: string[]
- signal?: AbortSignal
- }
+export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
- export interface TreeInput {
- cwd: string
- limit?: number
- signal?: AbortSignal
- }
+type Run = { kind: "files" | "search"; cwd: string; args: string[] }
- export interface Interface {
- readonly files: (input: FilesInput) => Stream.Stream<string, Error>
- readonly tree: (input: TreeInput) => Effect.Effect<string, Error>
- readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error>
- }
+type WorkerResult = {
+ type: "result"
+ code: number
+ stdout: string
+ stderr: string
+}
- export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
+type WorkerLine = {
+ type: "line"
+ line: string
+}
- type Run = { kind: "files" | "search"; cwd: string; args: string[] }
+type WorkerDone = {
+ type: "done"
+ code: number
+ stderr: string
+}
- type WorkerResult = {
- type: "result"
- code: number
- stdout: string
- stderr: string
+type WorkerError = {
+ type: "error"
+ error: {
+ message: string
+ name?: string
+ stack?: string
}
+}
- type WorkerLine = {
- type: "line"
- line: string
- }
+function env() {
+ const env = Object.fromEntries(
+ Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
+ )
+ delete env.RIPGREP_CONFIG_PATH
+ return env
+}
- type WorkerDone = {
- type: "done"
- code: number
- stderr: string
- }
+function text(input: unknown) {
+ if (typeof input === "string") return input
+ if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
+ if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
+ return String(input)
+}
- type WorkerError = {
- type: "error"
- error: {
- message: string
- name?: string
- stack?: string
- }
- }
+function toError(input: unknown) {
+ if (input instanceof Error) return input
+ if (typeof input === "string") return new Error(input)
+ return new Error(String(input))
+}
- function env() {
- const env = Object.fromEntries(
- Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
- )
- delete env.RIPGREP_CONFIG_PATH
- return env
- }
+function abort(signal?: AbortSignal) {
+ const err = signal?.reason
+ if (err instanceof Error) return err
+ const out = new Error("Aborted")
+ out.name = "AbortError"
+ return out
+}
- function text(input: unknown) {
- if (typeof input === "string") return input
- if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
- if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
- return String(input)
- }
+function error(stderr: string, code: number) {
+ const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`)
+ err.name = "RipgrepError"
+ return err
+}
- function toError(input: unknown) {
- if (input instanceof Error) return input
- if (typeof input === "string") return new Error(input)
- return new Error(String(input))
- }
+function clean(file: string) {
+ return path.normalize(file.replace(/^\.[\\/]/, ""))
+}
- function abort(signal?: AbortSignal) {
- const err = signal?.reason
- if (err instanceof Error) return err
- const out = new Error("Aborted")
- out.name = "AbortError"
- return out
+function row(data: Row): Row {
+ return {
+ ...data,
+ path: {
+ ...data.path,
+ text: clean(data.path.text),
+ },
}
+}
- function error(stderr: string, code: number) {
- const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`)
- err.name = "RipgrepError"
- return err
+function opts(cwd: string) {
+ return {
+ env: env(),
+ preopens: { ".": cwd },
}
+}
- function clean(file: string) {
- return path.normalize(file.replace(/^\.[\\/]/, ""))
- }
+function check(cwd: string) {
+ return Effect.tryPromise({
+ try: () => fs.stat(cwd).catch(() => undefined),
+ catch: toError,
+ }).pipe(
+ Effect.flatMap((stat) =>
+ stat?.isDirectory()
+ ? Effect.void
+ : Effect.fail(
+ Object.assign(new Error(`No such file or directory: '${cwd}'`), {
+ code: "ENOENT",
+ errno: -2,
+ path: cwd,
+ }),
+ ),
+ ),
+ )
+}
- function row(data: Row): Row {
- return {
- ...data,
- path: {
- ...data.path,
- text: clean(data.path.text),
- },
+function filesArgs(input: FilesInput) {
+ const args = ["--files", "--glob=!.git/*"]
+ if (input.follow) args.push("--follow")
+ if (input.hidden !== false) args.push("--hidden")
+ if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`)
+ if (input.glob) {
+ for (const glob of input.glob) {
+ args.push(`--glob=${glob}`)
}
}
+ args.push(".")
+ return args
+}
- function opts(cwd: string) {
- return {
- env: env(),
- preopens: { ".": cwd },
+function searchArgs(input: SearchInput) {
+ const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"]
+ if (input.follow) args.push("--follow")
+ if (input.glob) {
+ for (const glob of input.glob) {
+ args.push(`--glob=${glob}`)
}
}
+ if (input.limit) args.push(`--max-count=${input.limit}`)
+ args.push("--", input.pattern, ...(input.file ?? ["."]))
+ return args
+}
- function check(cwd: string) {
- return Effect.tryPromise({
- try: () => fs.stat(cwd).catch(() => undefined),
- catch: toError,
- }).pipe(
- Effect.flatMap((stat) =>
- stat?.isDirectory()
- ? Effect.void
- : Effect.fail(
- Object.assign(new Error(`No such file or directory: '${cwd}'`), {
- code: "ENOENT",
- errno: -2,
- path: cwd,
- }),
- ),
- ),
- )
- }
-
- function filesArgs(input: FilesInput) {
- const args = ["--files", "--glob=!.git/*"]
- if (input.follow) args.push("--follow")
- if (input.hidden !== false) args.push("--hidden")
- if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`)
- if (input.glob) {
- for (const glob of input.glob) {
- args.push(`--glob=${glob}`)
- }
- }
- args.push(".")
- return args
- }
+function parse(stdout: string) {
+ return stdout
+ .trim()
+ .split(/\r?\n/)
+ .filter(Boolean)
+ .map((line) => Result.parse(JSON.parse(line)))
+ .flatMap((item) => (item.type === "match" ? [row(item.data)] : []))
+}
- function searchArgs(input: SearchInput) {
- const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"]
- if (input.follow) args.push("--follow")
- if (input.glob) {
- for (const glob of input.glob) {
- args.push(`--glob=${glob}`)
- }
- }
- if (input.limit) args.push(`--max-count=${input.limit}`)
- args.push("--", input.pattern, ...(input.file ?? ["."]))
- return args
- }
+declare const OPENCODE_RIPGREP_WORKER_PATH: string
- function parse(stdout: string) {
- return stdout
- .trim()
- .split(/\r?\n/)
- .filter(Boolean)
- .map((line) => Result.parse(JSON.parse(line)))
- .flatMap((item) => (item.type === "match" ? [row(item.data)] : []))
+function target(): Effect.Effect<string | URL, Error> {
+ if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") {
+ return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
}
+ const js = new URL("./ripgrep.worker.js", import.meta.url)
+ return Effect.tryPromise({
+ try: () => Filesystem.exists(fileURLToPath(js)),
+ catch: toError,
+ }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
+}
- declare const OPENCODE_RIPGREP_WORKER_PATH: string
+function worker() {
+ return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() }))))
+}
- function target(): Effect.Effect<string | URL, Error> {
- if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") {
- return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
- }
- const js = new URL("./ripgrep.worker.js", import.meta.url)
- return Effect.tryPromise({
- try: () => Filesystem.exists(fileURLToPath(js)),
- catch: toError,
- }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
+function drain(buf: string, chunk: unknown, push: (line: string) => void) {
+ const lines = (buf + text(chunk)).split(/\r?\n/)
+ buf = lines.pop() || ""
+ for (const line of lines) {
+ if (line) push(line)
}
+ return buf
+}
- function worker() {
- return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() }))))
- }
+function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
+ Queue.failCauseUnsafe(queue, Cause.fail(err))
+}
- function drain(buf: string, chunk: unknown, push: (line: string) => void) {
- const lines = (buf + text(chunk)).split(/\r?\n/)
- buf = lines.pop() || ""
- for (const line of lines) {
- if (line) push(line)
- }
- return buf
- }
+function searchDirect(input: SearchInput) {
+ return Effect.tryPromise({
+ try: () =>
+ ripgrep(searchArgs(input), {
+ buffer: true,
+ ...opts(input.cwd),
+ }),
+ catch: toError,
+ }).pipe(
+ Effect.flatMap((ret) => {
+ const out = ret.stdout ?? ""
+ if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) {
+ return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1))
+ }
+ return Effect.sync(() => ({
+ items: ret.code === 1 ? [] : parse(out),
+ partial: ret.code === 2,
+ }))
+ }),
+ )
+}
- function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
- Queue.failCauseUnsafe(queue, Cause.fail(err))
- }
+function searchWorker(input: SearchInput) {
+ if (input.signal?.aborted) return Effect.fail(abort(input.signal))
- function searchDirect(input: SearchInput) {
- return Effect.tryPromise({
- try: () =>
- ripgrep(searchArgs(input), {
- buffer: true,
- ...opts(input.cwd),
- }),
- catch: toError,
- }).pipe(
- Effect.flatMap((ret) => {
- const out = ret.stdout ?? ""
- if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) {
- return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1))
+ return Effect.acquireUseRelease(
+ worker(),
+ (w) =>
+ Effect.callback<SearchResult, Error>((resume, signal) => {
+ let open = true
+ const done = (effect: Effect.Effect<SearchResult, Error>) => {
+ if (!open) return
+ open = false
+ resume(effect)
}
- return Effect.sync(() => ({
- items: ret.code === 1 ? [] : parse(out),
- partial: ret.code === 2,
- }))
- }),
- )
- }
+ const onabort = () => done(Effect.fail(abort(input.signal)))
- function searchWorker(input: SearchInput) {
- if (input.signal?.aborted) return Effect.fail(abort(input.signal))
-
- return Effect.acquireUseRelease(
- worker(),
- (w) =>
- Effect.callback<SearchResult, Error>((resume, signal) => {
- let open = true
- const done = (effect: Effect.Effect<SearchResult, Error>) => {
- if (!open) return
- open = false
- resume(effect)
+ w.onerror = (evt) => {
+ done(Effect.fail(toError(evt.error ?? evt.message)))
+ }
+ w.onmessage = (evt: MessageEvent<WorkerResult | WorkerError>) => {
+ const msg = evt.data
+ if (msg.type === "error") {
+ done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error)))
+ return
}
- const onabort = () => done(Effect.fail(abort(input.signal)))
-
- w.onerror = (evt) => {
- done(Effect.fail(toError(evt.error ?? evt.message)))
+ if (msg.code === 1) {
+ done(Effect.succeed({ items: [], partial: false }))
+ return
}
- w.onmessage = (evt: MessageEvent<WorkerResult | WorkerError>) => {
- const msg = evt.data
- if (msg.type === "error") {
- done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error)))
- return
- }
- if (msg.code === 1) {
- done(Effect.succeed({ items: [], partial: false }))
- return
- }
- if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) {
- done(Effect.fail(error(msg.stderr, msg.code)))
- return
- }
- done(
- Effect.sync(() => ({
- items: parse(msg.stdout),
- partial: msg.code === 2,
- })),
- )
+ if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) {
+ done(Effect.fail(error(msg.stderr, msg.code)))
+ return
}
+ done(
+ Effect.sync(() => ({
+ items: parse(msg.stdout),
+ partial: msg.code === 2,
+ })),
+ )
+ }
- input.signal?.addEventListener("abort", onabort, { once: true })
- signal.addEventListener("abort", onabort, { once: true })
- w.postMessage({
- kind: "search",
- cwd: input.cwd,
- args: searchArgs(input),
- } satisfies Run)
+ input.signal?.addEventListener("abort", onabort, { once: true })
+ signal.addEventListener("abort", onabort, { once: true })
+ w.postMessage({
+ kind: "search",
+ cwd: input.cwd,
+ args: searchArgs(input),
+ } satisfies Run)
+
+ return Effect.sync(() => {
+ input.signal?.removeEventListener("abort", onabort)
+ signal.removeEventListener("abort", onabort)
+ w.onerror = null
+ w.onmessage = null
+ })
+ }),
+ (w) => Effect.sync(() => w.terminate()),
+ )
+}
- return Effect.sync(() => {
- input.signal?.removeEventListener("abort", onabort)
- signal.removeEventListener("abort", onabort)
- w.onerror = null
- w.onmessage = null
- })
- }),
- (w) => Effect.sync(() => w.terminate()),
- )
- }
+function filesDirect(input: FilesInput) {
+ return Stream.callback<string, Error>(
+ Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
+ let buf = ""
+ let err = ""
- function filesDirect(input: FilesInput) {
- return Stream.callback<string, Error>(
- Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
- let buf = ""
- let err = ""
-
- const out = {
- write(chunk: unknown) {
- buf = drain(buf, chunk, (line) => {
- Queue.offerUnsafe(queue, clean(line))
- })
- },
- }
+ const out = {
+ write(chunk: unknown) {
+ buf = drain(buf, chunk, (line) => {
+ Queue.offerUnsafe(queue, clean(line))
+ })
+ },
+ }
- const stderr = {
- write(chunk: unknown) {
- err += text(chunk)
- },
- }
+ const stderr = {
+ write(chunk: unknown) {
+ err += text(chunk)
+ },
+ }
- yield* Effect.forkScoped(
- Effect.gen(function* () {
- yield* check(input.cwd)
- const ret = yield* Effect.tryPromise({
- try: () =>
- ripgrep(filesArgs(input), {
- stdout: out,
- stderr,
- ...opts(input.cwd),
- }),
- catch: toError,
- })
- if (buf) Queue.offerUnsafe(queue, clean(buf))
- if (ret.code === 0 || ret.code === 1) {
- Queue.endUnsafe(queue)
- return
- }
- fail(queue, error(err, ret.code ?? 1))
- }).pipe(
- Effect.catch((err) =>
- Effect.sync(() => {
- fail(queue, err)
+ yield* Effect.forkScoped(
+ Effect.gen(function* () {
+ yield* check(input.cwd)
+ const ret = yield* Effect.tryPromise({
+ try: () =>
+ ripgrep(filesArgs(input), {
+ stdout: out,
+ stderr,
+ ...opts(input.cwd),
}),
- ),
+ catch: toError,
+ })
+ if (buf) Queue.offerUnsafe(queue, clean(buf))
+ if (ret.code === 0 || ret.code === 1) {
+ Queue.endUnsafe(queue)
+ return
+ }
+ fail(queue, error(err, ret.code ?? 1))
+ }).pipe(
+ Effect.catch((err) =>
+ Effect.sync(() => {
+ fail(queue, err)
+ }),
),
- )
- }),
- )
- }
+ ),
+ )
+ }),
+ )
+}
- function filesWorker(input: FilesInput) {
- return Stream.callback<string, Error>(
- Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
- if (input.signal?.aborted) {
- fail(queue, abort(input.signal))
- return
- }
+function filesWorker(input: FilesInput) {
+ return Stream.callback<string, Error>(
+ Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
+ if (input.signal?.aborted) {
+ fail(queue, abort(input.signal))
+ return
+ }
- const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate()))
- let open = true
- const close = () => {
- if (!open) return false
- open = false
- return true
- }
- const onabort = () => {
- if (!close()) return
- fail(queue, abort(input.signal))
- }
+ const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate()))
+ let open = true
+ const close = () => {
+ if (!open) return false
+ open = false
+ return true
+ }
+ const onabort = () => {
+ if (!close()) return
+ fail(queue, abort(input.signal))
+ }
- w.onerror = (evt) => {
- if (!close()) return
- fail(queue, toError(evt.error ?? evt.message))
+ w.onerror = (evt) => {
+ if (!close()) return
+ fail(queue, toError(evt.error ?? evt.message))
+ }
+ w.onmessage = (evt: MessageEvent<WorkerLine | WorkerDone | WorkerError>) => {
+ const msg = evt.data
+ if (msg.type === "line") {
+ if (open) Queue.offerUnsafe(queue, msg.line)
+ return
}
- w.onmessage = (evt: MessageEvent<WorkerLine | WorkerDone | WorkerError>) => {
- const msg = evt.data
- if (msg.type === "line") {
- if (open) Queue.offerUnsafe(queue, msg.line)
- return
- }
- if (!close()) return
- if (msg.type === "error") {
- fail(queue, Object.assign(new Error(msg.error.message), msg.error))
- return
- }
- if (msg.code === 0 || msg.code === 1) {
- Queue.endUnsafe(queue)
- return
- }
- fail(queue, error(msg.stderr, msg.code))
+ if (!close()) return
+ if (msg.type === "error") {
+ fail(queue, Object.assign(new Error(msg.error.message), msg.error))
+ return
}
+ if (msg.code === 0 || msg.code === 1) {
+ Queue.endUnsafe(queue)
+ return
+ }
+ fail(queue, error(msg.stderr, msg.code))
+ }
- yield* Effect.acquireRelease(
+ yield* Effect.acquireRelease(
+ Effect.sync(() => {
+ input.signal?.addEventListener("abort", onabort, { once: true })
+ w.postMessage({
+ kind: "files",
+ cwd: input.cwd,
+ args: filesArgs(input),
+ } satisfies Run)
+ }),
+ () =>
Effect.sync(() => {
- input.signal?.addEventListener("abort", onabort, { once: true })
- w.postMessage({
- kind: "files",
- cwd: input.cwd,
- args: filesArgs(input),
- } satisfies Run)
+ input.signal?.removeEventListener("abort", onabort)
+ w.onerror = null
+ w.onmessage = null
}),
- () =>
- Effect.sync(() => {
- input.signal?.removeEventListener("abort", onabort)
- w.onerror = null
- w.onmessage = null
- }),
- )
- }),
- )
- }
+ )
+ }),
+ )
+}
- export const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const source = (input: FilesInput) => {
- const useWorker = !!input.signal && typeof Worker !== "undefined"
- if (!useWorker && input.signal) {
- log.warn("worker unavailable, ripgrep abort disabled")
- }
- return useWorker ? filesWorker(input) : filesDirect(input)
+export const layer = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const source = (input: FilesInput) => {
+ const useWorker = !!input.signal && typeof Worker !== "undefined"
+ if (!useWorker && input.signal) {
+ log.warn("worker unavailable, ripgrep abort disabled")
}
+ return useWorker ? filesWorker(input) : filesDirect(input)
+ }
- const files: Interface["files"] = (input) => source(input)
+ const files: Interface["files"] = (input) => source(input)
- const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) {
- log.info("tree", input)
- const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect))
+ const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) {
+ log.info("tree", input)
+ const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect))
- interface Node {
- name: string
- children: Map<string, Node>
- }
+ interface Node {
+ name: string
+ children: Map<string, Node>
+ }
- function child(node: Node, name: string) {
- const item = node.children.get(name)
- if (item) return item
- const next = { name, children: new Map() }
- node.children.set(name, next)
- return next
- }
+ function child(node: Node, name: string) {
+ const item = node.children.get(name)
+ if (item) return item
+ const next = { name, children: new Map() }
+ node.children.set(name, next)
+ return next
+ }
- function count(node: Node): number {
- return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0)
- }
+ function count(node: Node): number {
+ return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0)
+ }
- const root: Node = { name: "", children: new Map() }
- for (const file of list) {
- if (file.includes(".opencode")) continue
- const parts = file.split(path.sep)
- if (parts.length < 2) continue
- let node = root
- for (const part of parts.slice(0, -1)) {
- node = child(node, part)
- }
+ const root: Node = { name: "", children: new Map() }
+ for (const file of list) {
+ if (file.includes(".opencode")) continue
+ const parts = file.split(path.sep)
+ if (parts.length < 2) continue
+ let node = root
+ for (const part of parts.slice(0, -1)) {
+ node = child(node, part)
}
+ }
- const total = count(root)
- const limit = input.limit ?? total
- const lines: string[] = []
- const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values())
- .sort((a, b) => a.name.localeCompare(b.name))
- .map((node) => ({ node, path: node.name }))
-
- let used = 0
- for (let i = 0; i < queue.length && used < limit; i++) {
- const item = queue[i]
- lines.push(item.path)
- used++
- queue.push(
- ...Array.from(item.node.children.values())
- .sort((a, b) => a.name.localeCompare(b.name))
- .map((node) => ({ node, path: `${item.path}/${node.name}` })),
- )
- }
+ const total = count(root)
+ const limit = input.limit ?? total
+ const lines: string[] = []
+ const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values())
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((node) => ({ node, path: node.name }))
+
+ let used = 0
+ for (let i = 0; i < queue.length && used < limit; i++) {
+ const item = queue[i]
+ lines.push(item.path)
+ used++
+ queue.push(
+ ...Array.from(item.node.children.values())
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((node) => ({ node, path: `${item.path}/${node.name}` })),
+ )
+ }
- if (total > used) lines.push(`[${total - used} truncated]`)
- return lines.join("\n")
- })
+ if (total > used) lines.push(`[${total - used} truncated]`)
+ return lines.join("\n")
+ })
- const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) {
- const useWorker = !!input.signal && typeof Worker !== "undefined"
- if (!useWorker && input.signal) {
- log.warn("worker unavailable, ripgrep abort disabled")
- }
- return yield* useWorker ? searchWorker(input) : searchDirect(input)
- })
+ const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) {
+ const useWorker = !!input.signal && typeof Worker !== "undefined"
+ if (!useWorker && input.signal) {
+ log.warn("worker unavailable, ripgrep abort disabled")
+ }
+ return yield* useWorker ? searchWorker(input) : searchDirect(input)
+ })
- 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"