mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 11:39:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b49c490a5 |
@@ -123,109 +123,120 @@ export const fffLayer = Layer.effect(
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const result = yield* Effect.try({
|
const scope = yield* Scope.Scope
|
||||||
try: () =>
|
const load = yield* Effect.cached(
|
||||||
Fff.create({
|
Effect.gen(function* () {
|
||||||
basePath: location.directory,
|
const result = yield* Effect.try({
|
||||||
aiMode: true,
|
try: () =>
|
||||||
}),
|
Fff.create({
|
||||||
catch: (cause) => cause,
|
basePath: location.directory,
|
||||||
}).pipe(
|
aiMode: true,
|
||||||
Effect.catch((error) => Effect.logWarning("failed to initialize fff", { error }).pipe(Effect.as(undefined))),
|
|
||||||
)
|
|
||||||
if (!result?.ok) {
|
|
||||||
if (result) yield* Effect.logWarning("failed to initialize fff", { error: result.error })
|
|
||||||
return Service.of({
|
|
||||||
find: () => Effect.succeed([]),
|
|
||||||
glob: () => Effect.succeed([]),
|
|
||||||
grep: () => Effect.succeed([]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore))
|
|
||||||
return Service.of({
|
|
||||||
glob: (input) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "")
|
|
||||||
const found = result.value.glob(prefix ? `${prefix}/${input.pattern}` : input.pattern, {
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: input.limit,
|
|
||||||
})
|
|
||||||
if (!found.ok) throw found.error
|
|
||||||
return found.value.items.map((item) =>
|
|
||||||
FileSystem.Entry.make({
|
|
||||||
path: RelativePath.make(item.relativePath.replaceAll("\\", "/")),
|
|
||||||
type: "file",
|
|
||||||
}),
|
}),
|
||||||
)
|
catch: (cause) => cause,
|
||||||
}),
|
}).pipe(
|
||||||
grep: (input) =>
|
Effect.catch((error) => Effect.logWarning("failed to initialize fff", { error }).pipe(Effect.as(undefined))),
|
||||||
Effect.sync(() => {
|
)
|
||||||
const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "")
|
if (!result?.ok) {
|
||||||
const found = result.value.grep(
|
if (result) yield* Effect.logWarning("failed to initialize fff", { error: result.error })
|
||||||
[prefix ? `${prefix}/**` : undefined, input.include, input.pattern]
|
return Service.of({
|
||||||
.filter((value) => value !== undefined)
|
find: () => Effect.succeed([]),
|
||||||
.join(" "),
|
glob: () => Effect.succeed([]),
|
||||||
{ mode: "regex", pageSize: input.limit, timeBudgetMs: 1_500 },
|
grep: () => Effect.succeed([]),
|
||||||
)
|
|
||||||
if (!found.ok) throw found.error
|
|
||||||
return found.value.items.map((match) => {
|
|
||||||
const bytes = Buffer.from(match.lineContent)
|
|
||||||
return FileSystem.Match.make({
|
|
||||||
entry: FileSystem.Entry.make({
|
|
||||||
path: RelativePath.make(match.relativePath.replaceAll("\\", "/")),
|
|
||||||
type: "file",
|
|
||||||
}),
|
|
||||||
line: match.lineNumber,
|
|
||||||
offset: match.byteOffset,
|
|
||||||
text: match.lineContent.length > 2_000 ? match.lineContent.slice(0, 2_000) + "..." : match.lineContent,
|
|
||||||
submatches: match.matchRanges.map(([start, end]) => ({
|
|
||||||
text: bytes.subarray(start, end).toString("utf8"),
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}),
|
}
|
||||||
find: (input) =>
|
yield* Scope.addFinalizer(scope, Effect.sync(() => result.value.destroy()).pipe(Effect.ignore))
|
||||||
Effect.sync(() => {
|
return Service.of({
|
||||||
const options = { pageIndex: 0, pageSize: input.limit ?? 50 }
|
glob: (input) =>
|
||||||
const items = (() => {
|
Effect.sync(() => {
|
||||||
if (input.type === "file") {
|
const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "")
|
||||||
const found = result.value.fileSearch(input.query.trim(), options)
|
const found = result.value.glob(prefix ? `${prefix}/${input.pattern}` : input.pattern, {
|
||||||
if (!found.ok) throw found.error
|
pageIndex: 0,
|
||||||
return found.value.items.map((item, index) => ({
|
pageSize: input.limit,
|
||||||
path: item.relativePath,
|
|
||||||
type: "file" as const,
|
|
||||||
score: found.value.scores[index]?.total ?? 0,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (input.type === "directory") {
|
|
||||||
const found = result.value.directorySearch(input.query.trim(), options)
|
|
||||||
if (!found.ok) throw found.error
|
|
||||||
return found.value.items.map((item, index) => ({
|
|
||||||
path: item.relativePath,
|
|
||||||
type: "directory" as const,
|
|
||||||
score: found.value.scores[index]?.total ?? 0,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
const found = result.value.mixedSearch(input.query.trim(), options)
|
|
||||||
if (!found.ok) throw found.error
|
|
||||||
return found.value.items.map((item, index) => ({
|
|
||||||
path: item.item.relativePath,
|
|
||||||
type: item.type,
|
|
||||||
score: found.value.scores[index]?.total ?? 0,
|
|
||||||
}))
|
|
||||||
})()
|
|
||||||
return items
|
|
||||||
.sort((a, b) => b.score - a.score || a.path.length - b.path.length)
|
|
||||||
.map((item) => {
|
|
||||||
const relative = item.path.replaceAll("\\", "/").replace(/\/$/, "")
|
|
||||||
return FileSystem.Entry.make({
|
|
||||||
path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")),
|
|
||||||
type: item.type,
|
|
||||||
})
|
})
|
||||||
})
|
if (!found.ok) throw found.error
|
||||||
}),
|
return found.value.items.map((item) =>
|
||||||
|
FileSystem.Entry.make({
|
||||||
|
path: RelativePath.make(item.relativePath.replaceAll("\\", "/")),
|
||||||
|
type: "file",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
grep: (input) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "")
|
||||||
|
const found = result.value.grep(
|
||||||
|
[prefix ? `${prefix}/**` : undefined, input.include, input.pattern]
|
||||||
|
.filter((value) => value !== undefined)
|
||||||
|
.join(" "),
|
||||||
|
{ mode: "regex", pageSize: input.limit, timeBudgetMs: 1_500 },
|
||||||
|
)
|
||||||
|
if (!found.ok) throw found.error
|
||||||
|
return found.value.items.map((match) => {
|
||||||
|
const bytes = Buffer.from(match.lineContent)
|
||||||
|
return FileSystem.Match.make({
|
||||||
|
entry: FileSystem.Entry.make({
|
||||||
|
path: RelativePath.make(match.relativePath.replaceAll("\\", "/")),
|
||||||
|
type: "file",
|
||||||
|
}),
|
||||||
|
line: match.lineNumber,
|
||||||
|
offset: match.byteOffset,
|
||||||
|
text:
|
||||||
|
match.lineContent.length > 2_000 ? match.lineContent.slice(0, 2_000) + "..." : match.lineContent,
|
||||||
|
submatches: match.matchRanges.map(([start, end]) => ({
|
||||||
|
text: bytes.subarray(start, end).toString("utf8"),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
find: (input) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const options = { pageIndex: 0, pageSize: input.limit ?? 50 }
|
||||||
|
const items = (() => {
|
||||||
|
if (input.type === "file") {
|
||||||
|
const found = result.value.fileSearch(input.query.trim(), options)
|
||||||
|
if (!found.ok) throw found.error
|
||||||
|
return found.value.items.map((item, index) => ({
|
||||||
|
path: item.relativePath,
|
||||||
|
type: "file" as const,
|
||||||
|
score: found.value.scores[index]?.total ?? 0,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (input.type === "directory") {
|
||||||
|
const found = result.value.directorySearch(input.query.trim(), options)
|
||||||
|
if (!found.ok) throw found.error
|
||||||
|
return found.value.items.map((item, index) => ({
|
||||||
|
path: item.relativePath,
|
||||||
|
type: "directory" as const,
|
||||||
|
score: found.value.scores[index]?.total ?? 0,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
const found = result.value.mixedSearch(input.query.trim(), options)
|
||||||
|
if (!found.ok) throw found.error
|
||||||
|
return found.value.items.map((item, index) => ({
|
||||||
|
path: item.item.relativePath,
|
||||||
|
type: item.type,
|
||||||
|
score: found.value.scores[index]?.total ?? 0,
|
||||||
|
}))
|
||||||
|
})()
|
||||||
|
return items
|
||||||
|
.sort((a, b) => b.score - a.score || a.path.length - b.path.length)
|
||||||
|
.map((item) => {
|
||||||
|
const relative = item.path.replaceAll("\\", "/").replace(/\/$/, "")
|
||||||
|
return FileSystem.Entry.make({
|
||||||
|
path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")),
|
||||||
|
type: item.type,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return Service.of({
|
||||||
|
find: (input) => Effect.flatMap(load, (service) => service.find(input)),
|
||||||
|
glob: (input) => Effect.flatMap(load, (service) => service.glob(input)),
|
||||||
|
grep: (input) => Effect.flatMap(load, (service) => service.grep(input)),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
|
import { FileFinder } from "@ff-labs/fff-bun"
|
||||||
|
import "@opencode-ai/core/filesystem"
|
||||||
|
import { Fff } from "@opencode-ai/core/filesystem/fff.bun"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||||
|
import { location } from "../fixture/location"
|
||||||
import { tmpdir } from "../fixture/tmpdir"
|
import { tmpdir } from "../fixture/tmpdir"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
@@ -42,3 +47,70 @@ describe("Ripgrep", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("FileSystemSearch", () => {
|
||||||
|
it.live("initializes fff on the first search", () =>
|
||||||
|
withTmp((directory) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
if (!Fff.available()) return
|
||||||
|
yield* Effect.promise(() => fs.writeFile(path.join(directory, "README.md"), "hello\n"))
|
||||||
|
const { FileSystemSearch } = yield* Effect.promise(() => import("@opencode-ai/core/filesystem/search"))
|
||||||
|
const create = FileFinder.create
|
||||||
|
const calls = { create: 0, destroy: 0 }
|
||||||
|
yield* Effect.acquireUseRelease(
|
||||||
|
Effect.sync(() => {
|
||||||
|
FileFinder.create = (options) => {
|
||||||
|
calls.create++
|
||||||
|
const result = create(options)
|
||||||
|
if (!result.ok) return result
|
||||||
|
const destroy = result.value.destroy.bind(result.value)
|
||||||
|
result.value.destroy = () => {
|
||||||
|
calls.destroy++
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.acquireUseRelease(
|
||||||
|
Scope.make(),
|
||||||
|
(scope) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const context = yield* Layer.buildWithScope(
|
||||||
|
FileSystemSearch.fffLayer.pipe(
|
||||||
|
Layer.provide(
|
||||||
|
Layer.succeed(
|
||||||
|
Location.Service,
|
||||||
|
Location.Service.of(location(Location.Ref.make({ directory }))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
const service = Context.get(context, FileSystemSearch.Service)
|
||||||
|
expect(calls.create).toBe(0)
|
||||||
|
|
||||||
|
yield* Effect.all(
|
||||||
|
[
|
||||||
|
service.find({ query: "", type: "file", limit: 1 }),
|
||||||
|
service.find({ query: "", type: "file", limit: 1 }),
|
||||||
|
],
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
)
|
||||||
|
expect(calls.create).toBe(1)
|
||||||
|
|
||||||
|
yield* service.find({ query: "", type: "file", limit: 1 })
|
||||||
|
expect(calls.create).toBe(1)
|
||||||
|
expect(calls.destroy).toBe(0)
|
||||||
|
}),
|
||||||
|
(scope, exit) => Scope.close(scope, exit),
|
||||||
|
)
|
||||||
|
expect(calls.destroy).toBe(1)
|
||||||
|
}),
|
||||||
|
() => Effect.sync(() => (FileFinder.create = create)),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user