refactor: unwrap Ripgrep namespace + self-reexport (#22965)
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user