diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index 2c938aa3f2..bb09b686a9 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -1,7 +1,7 @@ export * as FileSystemSearch from "./search" import path from "path" -import { Context, Effect, Fiber, Layer, Scope } from "effect" +import { Context, Effect, Layer, Scope } from "effect" import { Fff } from "#fff" import fuzzysort from "fuzzysort" import { FileSystem } from "../filesystem" @@ -28,22 +28,20 @@ export const ripgrepLayer = Layer.effect( const state = { files: [] as string[], directories: [] as string[], - scan: undefined as Fiber.Fiber | undefined, } - state.scan = yield* ripgrep.find({ cwd: location.directory, pattern: "*", limit: 100_000 }).pipe( - Effect.tap((result) => + const directories = new Set() + yield* ripgrep.find({ + cwd: location.directory, + pattern: "*", + limit: location.vcs ? Number.MAX_SAFE_INTEGER : 100_000, + onEntry: (entry) => Effect.sync(() => { - state.files = result.map((item) => item.path) - state.directories = Array.from( - new Set( - state.files.flatMap((file) => { - const parts = file.split("/") - return parts.slice(0, -1).map((_, index) => parts.slice(0, index + 1).join("/") + path.sep) - }), - ), - ) + state.files.push(entry.path) + const parts = entry.path.split("/") + parts.slice(0, -1).forEach((_, index) => directories.add(parts.slice(0, index + 1).join("/") + path.sep)) + state.directories = Array.from(directories) }), - ), + }).pipe( Effect.orDie, Effect.asVoid, Effect.forkIn(scope), @@ -104,7 +102,6 @@ export const ripgrepLayer = Layer.effect( }), find: (input) => Effect.gen(function* () { - if (input.query) yield* Fiber.join(state.scan!) const items = input.type === "file" ? state.files diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index c3d13f4e21..18b7b29870 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -56,6 +56,7 @@ export interface FindInput { readonly hidden?: boolean readonly follow?: boolean readonly signal?: AbortSignal + readonly onEntry?: (entry: Entry) => Effect.Effect } export interface GlobInput { @@ -102,6 +103,7 @@ export const layer = Layer.effect( readonly signal?: AbortSignal readonly parse: (line: string) => Effect.Effect readonly pattern?: string + readonly onItem?: (item: A) => Effect.Effect }) => { const program = Effect.scoped( Effect.gen(function* () { @@ -112,11 +114,16 @@ export const layer = Layer.effect( Effect.map((output) => output.buffer.toString("utf8")), Effect.forkScoped, ) + let observed = 0 const rows = yield* Stream.decodeText(handle.stdout).pipe( Stream.splitLines, Stream.filter((line) => line.length > 0), Stream.mapEffect(input.parse), Stream.filter((row): row is A => row !== undefined), + Stream.tap((row) => { + if (!input.onItem || observed++ >= input.limit) return Effect.void + return input.onItem(row) + }), Stream.take(input.limit + 1), Stream.runCollect, Effect.map((chunk) => [...chunk]), @@ -181,7 +188,7 @@ export const layer = Layer.effect( Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))), ), find: (input) => - run({ + run({ cwd: input.cwd, limit: input.limit, signal: input.signal, @@ -194,24 +201,22 @@ export const layer = Layer.effect( `--glob=${input.pattern}`, ".", ], - parse: (line) => - Effect.succeed( - line - .replace(/^(?:\.[\\/])+/u, "") - .replace(/^[\\/]+/u, "") - .replaceAll("\\", "/"), - ), - }).pipe( - Effect.map((result) => - result.items.map((relative) => { - const absolute = path.resolve(input.cwd, relative) - return new Entry({ + parse: (line) => { + const relative = line + .replace(/^(?:\.[\\/])+/u, "") + .replace(/^[\\/]+/u, "") + .replaceAll("\\", "/") + return Effect.succeed( + new Entry({ path: RelativePath.make(relative), type: "file", - mime: FSUtil.mimeType(absolute), - }) - }), - ), + mime: FSUtil.mimeType(path.resolve(input.cwd, relative)), + }), + ) + }, + onItem: input.onEntry, + }).pipe( + Effect.map((result) => result.items), Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))), ), grep: (input) => diff --git a/packages/core/test/ripgrep.test.ts b/packages/core/test/ripgrep.test.ts index d0b4fe33fd..c91d1dd7c9 100644 --- a/packages/core/test/ripgrep.test.ts +++ b/packages/core/test/ripgrep.test.ts @@ -25,6 +25,15 @@ describe("Ripgrep", () => { expect(files.map((item) => item.path)).toContain(RelativePath.make(".opencode/config")) expect(files.map((item) => item.path)).toContain(RelativePath.make(".git/config")) + const observed: string[] = [] + const limited = yield* ripgrep.find({ + cwd: tmp.path, + pattern: "**/*", + limit: 1, + onEntry: (entry) => Effect.sync(() => observed.push(entry.path)), + }) + expect(observed).toEqual(limited.map((item) => item.path)) + const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 }) expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".opencode/config")) expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".git/config"))