From cdb51c6c290babe8405596d7d48eff950331b961 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 4 Aug 2026 21:38:12 -0400 Subject: [PATCH 1/2] fix(core): avoid eager directory snapshots --- packages/core/src/filesystem/search.ts | 14 ++-- packages/core/test/filesystem/search.test.ts | 69 +++++++++++++++++++- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index 23473381e543..281d127ce999 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -31,10 +31,7 @@ export const ripgrepLayer = Layer.effect( const location = yield* Location.Service const ripgrep = yield* Ripgrep.Service const scope = yield* Scope.Scope - const state = { - files: [] as string[], - directories: [] as string[], - } + const files: string[] = [] const directories = new Set() yield* ripgrep .find({ @@ -43,10 +40,9 @@ export const ripgrepLayer = Layer.effect( limit: location.vcs ? Number.MAX_SAFE_INTEGER : 100_000, onEntry: (entry) => Effect.sync(() => { - state.files.push(entry.path) + 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)) @@ -106,10 +102,10 @@ export const ripgrepLayer = Layer.effect( Effect.gen(function* () { const items = input.type === "file" - ? state.files + ? files : input.type === "directory" - ? state.directories - : [...state.files, ...state.directories] + ? Array.from(directories) + : [...files, ...directories] return fuzzysort.go(input.query, items, { limit: input.limit ?? 50 }).map((item) => { const relative = item.target const type = relative.endsWith(path.sep) ? ("directory" as const) : ("file" as const) diff --git a/packages/core/test/filesystem/search.test.ts b/packages/core/test/filesystem/search.test.ts index ab75d7165c08..5f11320be7d8 100644 --- a/packages/core/test/filesystem/search.test.ts +++ b/packages/core/test/filesystem/search.test.ts @@ -1,14 +1,20 @@ import { describe, expect } from "bun:test" import fs from "fs/promises" import path from "path" -import { Effect } from "effect" +import { Deferred, Effect, Layer } from "effect" import { LayerNode } from "@opencode-ai/util/effect/layer-node" +import { FileSystem } from "@opencode-ai/core/filesystem" +import { FileSystemSearch } from "@opencode-ai/core/filesystem/search" +import { FSUtil } from "@opencode-ai/util/fs-util" +import { Location } from "@opencode-ai/core/location" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" +import { location } from "../fixture/location" import { tmpdir } from "../fixture/tmpdir" import { testEffect } from "../lib/effect" const it = testEffect(LayerNode.compile(Ripgrep.node)) +const searchIt = testEffect(Layer.empty) const withTmp = (f: (directory: AbsolutePath) => Effect.Effect) => Effect.acquireRelease( @@ -42,3 +48,64 @@ describe("Ripgrep", () => { ), ) }) + +describe("FileSystemSearch", () => { + searchIt.live("finds partial file and directory results while indexing", () => + Effect.gen(function* () { + const firstIndexed = yield* Deferred.make() + const release = yield* Deferred.make() + const secondIndexed = yield* Deferred.make() + const first = FileSystem.Entry.make({ path: RelativePath.make("src/first.ts"), type: "file" }) + const second = FileSystem.Entry.make({ path: RelativePath.make("test/nested/second.ts"), type: "file" }) + const ripgrep = Layer.succeed( + Ripgrep.Service, + Ripgrep.Service.of({ + find: (input) => + Effect.gen(function* () { + if (input.onEntry) yield* input.onEntry(first) + yield* Deferred.succeed(firstIndexed, undefined) + yield* Deferred.await(release) + if (input.onEntry) yield* input.onEntry(second) + yield* Deferred.succeed(secondIndexed, undefined) + return [first, second] + }), + glob: () => Effect.die("unused"), + grep: () => Effect.die("unused"), + }), + ) + const layer = FileSystemSearch.ripgrepLayer.pipe( + Layer.provide( + Layer.mergeAll( + LayerNode.compile(FSUtil.node), + Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/repo") }))), + ripgrep, + ), + ), + ) + + yield* Effect.gen(function* () { + const search = yield* FileSystemSearch.Service + yield* Deferred.await(firstIndexed) + + expect(yield* search.find({ query: "first", type: "file" })).toEqual([first]) + expect(yield* search.find({ query: "src", type: "directory" })).toEqual([ + FileSystem.Entry.make({ path: RelativePath.make(`src${path.sep}`), type: "directory" }), + ]) + expect(yield* search.find({ query: "src" })).toEqual([ + FileSystem.Entry.make({ path: RelativePath.make(`src${path.sep}`), type: "directory" }), + first, + ]) + + yield* Deferred.succeed(release, undefined) + yield* Deferred.await(secondIndexed) + + expect(yield* search.find({ query: "nested", type: "directory" })).toEqual([ + FileSystem.Entry.make({ + path: RelativePath.make(`test${path.sep}nested${path.sep}`), + type: "directory", + }), + ]) + }).pipe(Effect.provide(layer)) + }), + ) +}) From dd86e47762ae65c5e202b8812af3f34f3fa4e469 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 4 Aug 2026 21:42:55 -0400 Subject: [PATCH 2/2] test(core): remove directory search regression test --- packages/core/test/filesystem/search.test.ts | 69 +------------------- 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/packages/core/test/filesystem/search.test.ts b/packages/core/test/filesystem/search.test.ts index 5f11320be7d8..ab75d7165c08 100644 --- a/packages/core/test/filesystem/search.test.ts +++ b/packages/core/test/filesystem/search.test.ts @@ -1,20 +1,14 @@ import { describe, expect } from "bun:test" import fs from "fs/promises" import path from "path" -import { Deferred, Effect, Layer } from "effect" +import { Effect } from "effect" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { FileSystem } from "@opencode-ai/core/filesystem" -import { FileSystemSearch } from "@opencode-ai/core/filesystem/search" -import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "@opencode-ai/core/location" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" -import { location } from "../fixture/location" import { tmpdir } from "../fixture/tmpdir" import { testEffect } from "../lib/effect" const it = testEffect(LayerNode.compile(Ripgrep.node)) -const searchIt = testEffect(Layer.empty) const withTmp = (f: (directory: AbsolutePath) => Effect.Effect) => Effect.acquireRelease( @@ -48,64 +42,3 @@ describe("Ripgrep", () => { ), ) }) - -describe("FileSystemSearch", () => { - searchIt.live("finds partial file and directory results while indexing", () => - Effect.gen(function* () { - const firstIndexed = yield* Deferred.make() - const release = yield* Deferred.make() - const secondIndexed = yield* Deferred.make() - const first = FileSystem.Entry.make({ path: RelativePath.make("src/first.ts"), type: "file" }) - const second = FileSystem.Entry.make({ path: RelativePath.make("test/nested/second.ts"), type: "file" }) - const ripgrep = Layer.succeed( - Ripgrep.Service, - Ripgrep.Service.of({ - find: (input) => - Effect.gen(function* () { - if (input.onEntry) yield* input.onEntry(first) - yield* Deferred.succeed(firstIndexed, undefined) - yield* Deferred.await(release) - if (input.onEntry) yield* input.onEntry(second) - yield* Deferred.succeed(secondIndexed, undefined) - return [first, second] - }), - glob: () => Effect.die("unused"), - grep: () => Effect.die("unused"), - }), - ) - const layer = FileSystemSearch.ripgrepLayer.pipe( - Layer.provide( - Layer.mergeAll( - LayerNode.compile(FSUtil.node), - Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/repo") }))), - ripgrep, - ), - ), - ) - - yield* Effect.gen(function* () { - const search = yield* FileSystemSearch.Service - yield* Deferred.await(firstIndexed) - - expect(yield* search.find({ query: "first", type: "file" })).toEqual([first]) - expect(yield* search.find({ query: "src", type: "directory" })).toEqual([ - FileSystem.Entry.make({ path: RelativePath.make(`src${path.sep}`), type: "directory" }), - ]) - expect(yield* search.find({ query: "src" })).toEqual([ - FileSystem.Entry.make({ path: RelativePath.make(`src${path.sep}`), type: "directory" }), - first, - ]) - - yield* Deferred.succeed(release, undefined) - yield* Deferred.await(secondIndexed) - - expect(yield* search.find({ query: "nested", type: "directory" })).toEqual([ - FileSystem.Entry.make({ - path: RelativePath.make(`test${path.sep}nested${path.sep}`), - type: "directory", - }), - ]) - }).pipe(Effect.provide(layer)) - }), - ) -})