mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-13 20:33:28 +00:00
fix(core): refresh fallback file search (#42348)
This commit is contained in:
parent
8bcc245142
commit
20929b3081
2 changed files with 103 additions and 12 deletions
|
|
@ -2,7 +2,7 @@ export * as FileSystemSearch from "./search.js"
|
|||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import path from "path"
|
||||
import { Context, Effect, Layer, Schema, Scope } from "effect"
|
||||
import { Clock, Context, Duration, Effect, Layer, Schema, Scope } from "effect"
|
||||
import { Fff } from "#fff"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { FileSystem } from "../filesystem.js"
|
||||
|
|
@ -22,38 +22,64 @@ export type Options = typeof Options.Type
|
|||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem/Search") {}
|
||||
|
||||
const REFRESH_INTERVAL = Duration.toMillis("10 seconds")
|
||||
|
||||
export const ripgrepLayer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
const ripgrep = yield* Ripgrep.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const files: string[] = []
|
||||
const directories = new Set<string>()
|
||||
const clock = yield* Clock.Clock
|
||||
const home = Protected.isHome(location.directory)
|
||||
yield* ripgrep
|
||||
.find({
|
||||
let index = { files: [] as string[], directories: new Set<string>() }
|
||||
let initialized = false
|
||||
let settledAt = Number.NEGATIVE_INFINITY
|
||||
let refreshing = false
|
||||
const scan = Effect.gen(function* () {
|
||||
const next = { files: [] as string[], directories: new Set<string>() }
|
||||
if (!initialized) index = next
|
||||
yield* ripgrep.find({
|
||||
cwd: location.directory,
|
||||
pattern: "*",
|
||||
limit: location.vcs && !home ? Number.MAX_SAFE_INTEGER : 100_000,
|
||||
exclude: home ? [...Protected.names()].map((name) => `${name}/**`) : undefined,
|
||||
onEntry: (entry) =>
|
||||
Effect.sync(() => {
|
||||
files.push(entry.path)
|
||||
next.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))
|
||||
parts.slice(0, -1).forEach((_, offset) =>
|
||||
next.directories.add(parts.slice(0, offset + 1).join("/") + path.sep),
|
||||
)
|
||||
}),
|
||||
})
|
||||
.pipe(Effect.orDie, Effect.asVoid, Effect.forkIn(scope))
|
||||
index = next
|
||||
initialized = true
|
||||
}).pipe(
|
||||
Effect.orDie,
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
settledAt = clock.currentTimeMillisUnsafe()
|
||||
refreshing = false
|
||||
}),
|
||||
),
|
||||
)
|
||||
const refresh = Effect.sync(() => {
|
||||
if (refreshing || clock.currentTimeMillisUnsafe() < settledAt + REFRESH_INTERVAL) return
|
||||
refreshing = true
|
||||
return scan
|
||||
}).pipe(Effect.flatMap((effect) => (effect ? effect.pipe(Effect.forkIn(scope)) : Effect.void)))
|
||||
yield* refresh
|
||||
return Service.of({
|
||||
find: (input) =>
|
||||
Effect.gen(function* () {
|
||||
yield* refresh
|
||||
const items =
|
||||
input.type === "file"
|
||||
? files
|
||||
? index.files
|
||||
: input.type === "directory"
|
||||
? Array.from(directories)
|
||||
: [...files, ...directories]
|
||||
? Array.from(index.directories)
|
||||
: [...index.files, ...index.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)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Deferred, Effect, Layer } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Protected } from "@opencode-ai/core/filesystem/protected"
|
||||
|
|
@ -56,4 +57,68 @@ describe("FileSystemSearch", () => {
|
|||
}).pipe(Effect.provide(layer), Effect.scoped),
|
||||
)
|
||||
})
|
||||
|
||||
test("refreshes a stale ripgrep index atomically without blocking search", async () => {
|
||||
let scans = 0
|
||||
const initial = Effect.runSync(Deferred.make<void>())
|
||||
const started = Effect.runSync(Deferred.make<void>())
|
||||
const release = Effect.runSync(Deferred.make<void>())
|
||||
const layer = AppNodeBuilder.build(FileSystemSearch.node, [
|
||||
[
|
||||
Location.node,
|
||||
Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make(path.join(os.tmpdir(), "opencode-search-atomic")) })),
|
||||
),
|
||||
],
|
||||
[
|
||||
Ripgrep.node,
|
||||
Layer.succeed(
|
||||
Ripgrep.Service,
|
||||
Ripgrep.Service.of({
|
||||
find: (input) =>
|
||||
Effect.gen(function* () {
|
||||
scans++
|
||||
if (scans > 1) {
|
||||
yield* Deferred.succeed(started, undefined)
|
||||
yield* Deferred.await(release)
|
||||
}
|
||||
const entry = FileSystem.Entry.make({
|
||||
path: RelativePath.make(scans === 1 ? "src/old.ts" : "src/new.ts"),
|
||||
type: "file",
|
||||
})
|
||||
if (input.onEntry) yield* input.onEntry(entry)
|
||||
if (scans === 1) yield* Deferred.succeed(initial, undefined)
|
||||
return [entry]
|
||||
}),
|
||||
glob: () => Effect.succeed([]),
|
||||
grep: () => Effect.succeed([]),
|
||||
}),
|
||||
),
|
||||
],
|
||||
])
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const search = yield* FileSystemSearch.Service
|
||||
yield* Deferred.await(initial)
|
||||
expect((yield* search.find({ query: "old", type: "file" }))[0]?.path).toBe(RelativePath.make("src/old.ts"))
|
||||
expect(scans).toBe(1)
|
||||
|
||||
yield* TestClock.adjust("10 seconds")
|
||||
yield* search.find({ query: "old", type: "file" })
|
||||
yield* Deferred.await(started)
|
||||
|
||||
expect((yield* search.find({ query: "old", type: "file" }))[0]?.path).toBe(RelativePath.make("src/old.ts"))
|
||||
expect(scans).toBe(2)
|
||||
yield* Deferred.succeed(release, undefined)
|
||||
const refreshed = yield* Effect.gen(function* () {
|
||||
yield* Effect.yieldNow
|
||||
return yield* search.find({ query: "new", type: "file" })
|
||||
}).pipe(Effect.repeat({ until: (entries) => entries.length > 0 }))
|
||||
expect(refreshed[0]?.path).toBe(RelativePath.make("src/new.ts"))
|
||||
expect(scans).toBe(2)
|
||||
}).pipe(Effect.provide(layer), Effect.provide(TestClock.layer()), Effect.scoped),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue