| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- 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 { Fff } from "#fff"
- import fuzzysort from "fuzzysort"
- import { FileSystem } from "../filesystem.js"
- import { Location } from "../location.js"
- import { Ripgrep } from "../ripgrep.js"
- import { RelativePath } from "../schema.js"
- import { Protected } from "./protected.js"
- export interface Interface {
- readonly find: (input: FileSystem.FindInput) => Effect.Effect<FileSystem.Entry[]>
- }
- export const Options = Schema.Struct({
- fff: Schema.optional(Schema.Boolean),
- })
- export type Options = typeof Options.Type
- export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem/Search") {}
- 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 home = Protected.isHome(location.directory)
- 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)
- const parts = entry.path.split("/")
- parts.slice(0, -1).forEach((_, index) => directories.add(parts.slice(0, index + 1).join("/") + path.sep))
- }),
- })
- .pipe(Effect.orDie, Effect.asVoid, Effect.forkIn(scope))
- return Service.of({
- find: (input) =>
- Effect.gen(function* () {
- const items =
- input.type === "file"
- ? files
- : input.type === "directory"
- ? 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)
- return FileSystem.Entry.make({
- path: RelativePath.make(relative),
- type,
- })
- })
- }),
- })
- }),
- )
- export const fffLayer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const location = yield* Location.Service
- const result = yield* Effect.try({
- try: () =>
- Fff.create({
- basePath: location.directory,
- aiMode: true,
- disableMmapCache: true,
- disableContentIndexing: true,
- }),
- catch: (cause) => cause,
- }).pipe(
- 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([]),
- })
- }
- yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore))
- return Service.of({
- 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,
- })
- })
- }),
- })
- }),
- )
- export const layer = (options?: Options) =>
- Layer.unwrap(
- Effect.gen(function* () {
- if (options?.fff === false || (options?.fff === undefined && process.platform === "win32") || !Fff.available())
- return ripgrepLayer
- const location = yield* Location.Service
- // Non-VCS locations can contain many repositories, so avoid eagerly content-indexing the entire aggregate tree.
- return location.vcs && !Protected.isHome(location.directory) ? fffLayer : ripgrepLayer
- }),
- )
- export function configured(options?: Options) {
- return makeLocationNode({ service: Service, layer: layer(options), deps: [Location.node, Ripgrep.node] })
- }
- export const node = configured()
|