refactor(core): unify filesystem search service (#31566)

This commit is contained in:
Dax
2026-06-09 20:38:02 -04:00
committed by GitHub
parent ce4e658e3f
commit a0409e64d8
57 changed files with 965 additions and 2855 deletions
@@ -1,5 +1,4 @@
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { LSP } from "@/lsp/lsp"
import { Schema } from "effect"
@@ -38,6 +37,20 @@ export const FindSymbolQuery = Schema.Struct({
query: Schema.String,
})
export const LegacyMatch = Schema.Struct({
path: Schema.Struct({ text: Schema.String }),
lines: Schema.Struct({ text: Schema.String }),
line_number: NonNegativeInt,
absolute_offset: NonNegativeInt,
submatches: Schema.Array(
Schema.Struct({
match: Schema.Struct({ text: Schema.String }),
start: NonNegativeInt,
end: NonNegativeInt,
}),
),
})
export const LegacyEntry = Schema.Struct({
name: Schema.String,
path: Schema.String,
@@ -94,7 +107,7 @@ export const FileApi = HttpApi.make("file")
.add(
HttpApiEndpoint.get("findText", FilePaths.findText, {
query: FindTextQuery,
success: described(Schema.Array(Ripgrep.SearchMatch), "Matches"),
success: described(Schema.Array(LegacyMatch), "Matches"),
}).annotateMerge(
OpenApi.annotations({
identifier: "find.text",
@@ -1,8 +1,7 @@
import * as InstanceState from "@/effect/instance-state"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
@@ -15,7 +14,6 @@ import { InstanceHttpApi } from "../api"
export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
Effect.gen(function* () {
const ripgrep = yield* Ripgrep.Service
const search = yield* Search.Service
const locations = yield* LocationServiceMap
const filesystem = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
@@ -26,8 +24,18 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) {
return (yield* ripgrep
.search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 })
.pipe(Effect.orDie)).items
.grep({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 })
.pipe(Effect.orDie)).map((match) => ({
path: { text: match.entry.path },
lines: { text: match.text },
line_number: match.line,
absolute_offset: match.offset,
submatches: match.submatches.map((submatch) => ({
match: { text: submatch.text },
start: submatch.start,
end: submatch.end,
})),
}))
})
const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: {
@@ -35,19 +43,18 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
}) {
const directory = (yield* InstanceState.context).directory
const limit = ctx.query.limit ?? 10
const kind = ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : "all")
const type = ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : undefined)
const started = performance.now()
const fff = yield* search.file({ cwd: directory, query: ctx.query.query, limit, kind }).pipe(Effect.orDie)
const found = yield* filesystem(FileSystem.Service.use((fs) => fs.find({ query: ctx.query.query, limit, type })))
yield* Effect.logInfo("find file", {
engine: "fff",
query: ctx.query.query,
kind,
type,
directory,
limit,
results: fff.length,
results: found.length,
duration: Math.round(performance.now() - started),
})
return fff.map((item) => item.path)
return found.map((item) => item.path)
})
const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
@@ -73,10 +80,10 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
return (yield* fs.list({ path: RelativePath.make(ctx.query.path) })).map((item) => ({
name: path.basename(item.path),
path: item.path,
absolute: path.join(directory, item.path),
absolute: path.resolve(location.directory, item.path),
type: item.type,
ignored: ignored.ignores(
path.relative(location.project.directory, path.join(location.directory, item.path)) +
path.relative(location.project.directory, path.resolve(location.directory, item.path)) +
(item.type === "directory" ? "/" : ""),
),
}))
@@ -112,4 +119,4 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
.handle("content", content)
.handle("status", status)
}),
).pipe(Layer.provide(LocationServiceMap.layer), Layer.provide(Search.defaultLayer))
).pipe(Layer.provide(LocationServiceMap.layer))
@@ -17,7 +17,7 @@ import { BackgroundJob } from "@/background/job"
import { Config } from "@/config/config"
import { Command } from "@/command"
import * as Observability from "@opencode-ai/core/observability"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Format } from "@/format"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { LSP } from "@/lsp/lsp"
@@ -234,7 +234,6 @@ export function createRoutes(
Provider.defaultLayer,
PtyTicket.defaultLayer,
Question.defaultLayer,
Ripgrep.defaultLayer,
RuntimeFlags.defaultLayer,
Session.defaultLayer,
SessionCompaction.defaultLayer,
@@ -259,6 +258,7 @@ export function createRoutes(
HttpServer.layerServices,
]),
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
Layer.provideMerge(Ripgrep.defaultLayer),
Layer.provide(InstanceLayer.layer),
Layer.provideMerge(Observability.layer),
)