129 lines
5.0 KiB
TypeScript
129 lines
5.0 KiB
TypeScript
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 { FSUtil } from "@opencode-ai/core/fs-util"
|
|
import { Log } from "@opencode-ai/core/util/log"
|
|
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
|
import { Effect, Layer } from "effect"
|
|
import path from "path"
|
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
import { InstanceHttpApi } from "../api"
|
|
|
|
const log = Log.create({ service: "server.file" })
|
|
|
|
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>) {
|
|
return yield* effect.pipe(
|
|
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
|
)
|
|
})
|
|
|
|
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
|
|
})
|
|
|
|
const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: {
|
|
query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number }
|
|
}) {
|
|
const directory = (yield* InstanceState.context).directory
|
|
const limit = ctx.query.limit ?? 10
|
|
const kind = ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : "all")
|
|
const started = performance.now()
|
|
// Prefer fff (frecency + fuzzy ranking) and trust its ordering. Fall back
|
|
// to the ripgrep-backed FileSystem.find when fff is unavailable.
|
|
const fff = yield* search.file({ cwd: directory, query: ctx.query.query, limit, kind }).pipe(Effect.orDie)
|
|
if (fff !== undefined) {
|
|
log.info("find file", {
|
|
engine: "fff",
|
|
query: ctx.query.query,
|
|
kind,
|
|
directory,
|
|
limit,
|
|
results: fff.length,
|
|
duration: Math.round(performance.now() - started),
|
|
})
|
|
return fff
|
|
}
|
|
const fallback = (yield* filesystem(
|
|
FileSystem.Service.use((fs) =>
|
|
fs.find({
|
|
query: ctx.query.query,
|
|
limit,
|
|
type: ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : undefined),
|
|
}),
|
|
),
|
|
)).map((item) => item.path)
|
|
log.info("find file", {
|
|
engine: "ripgrep",
|
|
query: ctx.query.query,
|
|
kind,
|
|
directory,
|
|
limit,
|
|
results: fallback.length,
|
|
duration: Math.round(performance.now() - started),
|
|
})
|
|
return fallback
|
|
})
|
|
|
|
const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
|
|
return []
|
|
})
|
|
|
|
const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) {
|
|
const directory = (yield* InstanceState.context).directory
|
|
return yield* filesystem(
|
|
FileSystem.Service.use((fs) =>
|
|
fs.list({ path: RelativePath.make(ctx.query.path) }).pipe(
|
|
Effect.map((items) =>
|
|
items.map((item) => ({
|
|
name: path.basename(item.path),
|
|
path: item.path,
|
|
absolute: path.join(directory, item.path),
|
|
type: item.type,
|
|
ignored: fs.isIgnored(item.path, item.type),
|
|
})),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
})
|
|
|
|
const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) {
|
|
const directory = (yield* InstanceState.context).directory
|
|
const file = path.resolve(directory, ctx.query.path)
|
|
if (!FSUtil.contains(directory, file)) return yield* Effect.die(new Error("Path escapes the location"))
|
|
if (!(yield* FSUtil.Service.use((fs) => fs.existsSafe(file)))) return { type: "text" as const, content: "" }
|
|
return yield* filesystem(
|
|
FileSystem.Service.use((fs) => fs.read({ path: RelativePath.make(ctx.query.path) })),
|
|
).pipe(
|
|
Effect.map((item) => ({
|
|
type: item.type,
|
|
content: item.type === "text" ? item.content.trim() : item.content,
|
|
...(item.type === "binary" ? { encoding: item.encoding, mimeType: item.mime } : {}),
|
|
})),
|
|
)
|
|
})
|
|
|
|
const status = Effect.fn("FileHttpApi.status")(function* () {
|
|
return []
|
|
})
|
|
|
|
return handlers
|
|
.handle("findText", findText)
|
|
.handle("findFile", findFile)
|
|
.handle("findSymbol", findSymbol)
|
|
.handle("list", list)
|
|
.handle("content", content)
|
|
.handle("status", status)
|
|
}),
|
|
).pipe(Layer.provide(LocationServiceMap.layer), Layer.provide(Search.defaultLayer))
|