diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 17db28145b1..c6a358440b2 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -7,7 +7,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "./location" import { PositiveInt, RelativePath } from "./schema" import { FileSystemSearch } from "./filesystem/search" -import { Entry, FileSystem, FindInput, Match } from "@opencode-ai/schema/filesystem" +import { Entry, FileSystem, FindInput } from "@opencode-ai/schema/filesystem" export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem" export const ReadInput = Schema.Struct({ @@ -53,8 +53,6 @@ export interface Interface { readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> readonly list: (input?: ListInput) => Effect.Effect readonly find: (input: FindInput) => Effect.Effect - readonly glob: (input: GlobInput) => Effect.Effect - readonly grep: (input: GrepInput) => Effect.Effect } export class Service extends Context.Service()("@opencode/FileSystem") {} @@ -76,8 +74,6 @@ const baseLayer = Layer.effect( }) return Service.of({ find: search.find, - glob: search.glob, - grep: search.grep, read: Effect.fn("FileSystem.read")(function* (input) { const target = yield* resolve(input.path) const info = yield* fs.stat(target.real).pipe(Effect.orDie) diff --git a/packages/core/src/filesystem/fff.bun.ts b/packages/core/src/filesystem/fff.bun.ts index 9843419ed11..085cc8de2fb 100644 --- a/packages/core/src/filesystem/fff.bun.ts +++ b/packages/core/src/filesystem/fff.bun.ts @@ -3,9 +3,6 @@ import { type DirItem, type DirSearchResult, type FileItem, - type GrepCursor, - type GrepMatch, - type GrepResult, type InitOptions, type MixedItem, type MixedSearchResult, @@ -45,19 +42,6 @@ export interface MixedSearch { export type File = FileItem export type Directory = DirItem export type Mixed = MixedItem -export type Cursor = GrepCursor | null -export type Hit = GrepMatch - -export interface Grep { - items: GrepResult["items"] - totalMatched: number - totalFilesSearched: number - totalFiles: number - filteredFileCount: number - nextCursor: Cursor - regexFallbackError?: string -} - export interface Picker { destroy(): void isScanning(): boolean @@ -71,14 +55,6 @@ export interface Picker { pageSize?: number }, ): Result - glob( - pattern: string, - opts?: { - currentFile?: string - pageIndex?: number - pageSize?: number - }, - ): Result directorySearch( query: string, opts?: { @@ -95,18 +71,6 @@ export interface Picker { pageSize?: number }, ): Result - grep( - query: string, - opts?: { - mode?: "plain" | "regex" | "fuzzy" - maxMatchesPerFile?: number - timeBudgetMs?: number - beforeContext?: number - afterContext?: number - cursor?: Cursor - pageSize?: number - }, - ): Result trackQuery(query: string, file: string): Result getHistoricalQuery(offset: number): Result } @@ -127,10 +91,8 @@ export function create(opts: Init): Result { waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs), refreshGitStatus: () => pick.refreshGitStatus(), fileSearch: (query, next) => pick.fileSearch(query, next), - glob: (pattern, next) => pick.glob(pattern, next), directorySearch: (query, next) => pick.directorySearch(query, next), mixedSearch: (query, next) => pick.mixedSearch(query, next), - grep: (query, next) => pick.grep(query, next), trackQuery: (query, file) => pick.trackQuery(query, file), getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset), }, diff --git a/packages/core/src/filesystem/fff.node.ts b/packages/core/src/filesystem/fff.node.ts index b6b71cecabf..9f14876e75c 100644 --- a/packages/core/src/filesystem/fff.node.ts +++ b/packages/core/src/filesystem/fff.node.ts @@ -2,9 +2,6 @@ import type { DirItem, DirSearchResult, FileItem, - GrepCursor, - GrepMatch, - GrepResult, InitOptions, MixedItem, MixedSearchResult, @@ -42,19 +39,6 @@ export interface MixedSearch { export type File = FileItem export type Directory = DirItem export type Mixed = MixedItem -export type Cursor = GrepCursor | null -export type Hit = GrepMatch - -export interface Grep { - items: GrepResult["items"] - totalMatched: number - totalFilesSearched: number - totalFiles: number - filteredFileCount: number - nextCursor: Cursor - regexFallbackError?: string -} - export interface Picker { destroy(): void isScanning(): boolean @@ -68,14 +52,6 @@ export interface Picker { pageSize?: number }, ): Result - glob( - pattern: string, - opts?: { - currentFile?: string - pageIndex?: number - pageSize?: number - }, - ): Result directorySearch( query: string, opts?: { @@ -92,18 +68,6 @@ export interface Picker { pageSize?: number }, ): Result - grep( - query: string, - opts?: { - mode?: "plain" | "regex" | "fuzzy" - maxMatchesPerFile?: number - timeBudgetMs?: number - beforeContext?: number - afterContext?: number - cursor?: Cursor - pageSize?: number - }, - ): Result trackQuery(query: string, file: string): Result getHistoricalQuery(offset: number): Result } @@ -125,10 +89,8 @@ export function create(opts: Init): Result { waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs), refreshGitStatus: () => pick.refreshGitStatus(), fileSearch: (query, next) => pick.fileSearch(query, next), - glob: (pattern, next) => pick.glob(pattern, next), directorySearch: (query, next) => pick.directorySearch(query, next), mixedSearch: (query, next) => pick.mixedSearch(query, next), - grep: (query, next) => pick.grep(query, next), trackQuery: (query, file) => pick.trackQuery(query, file), getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset), }, diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index 9aa77413ddf..4778bedd889 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -6,7 +6,6 @@ import { Context, Effect, Layer, Schema, Scope } from "effect" import { Fff } from "#fff" import fuzzysort from "fuzzysort" import { FileSystem } from "../filesystem" -import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" import { Ripgrep } from "../ripgrep" import { RelativePath } from "../schema" @@ -14,8 +13,6 @@ import { Protected } from "./protected" export interface Interface { readonly find: (input: FileSystem.FindInput) => Effect.Effect - readonly glob: (input: FileSystem.GlobInput) => Effect.Effect - readonly grep: (input: FileSystem.GrepInput) => Effect.Effect } export const Options = Schema.Struct({ @@ -28,7 +25,6 @@ export class Service extends Context.Service()("@opencode/Fi export const ripgrepLayer = Layer.effect( Service, Effect.gen(function* () { - const fs = yield* FSUtil.Service const location = yield* Location.Service const ripgrep = yield* Ripgrep.Service const scope = yield* Scope.Scope @@ -50,57 +46,6 @@ export const ripgrepLayer = Layer.effect( }) .pipe(Effect.orDie, Effect.asVoid, Effect.forkIn(scope)) return Service.of({ - glob: (input) => - Effect.gen(function* () { - const target = path.resolve(location.directory, input.path ?? ".") - const info = yield* fs.stat(target).pipe(Effect.orDie) - const cwd = info.type === "File" ? path.dirname(target) : target - return yield* ripgrep - .glob({ - cwd, - pattern: input.pattern, - limit: input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT, - }) - .pipe( - Effect.map((result) => - result.map((entry) => - FileSystem.Entry.make({ - ...entry, - path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), - }), - ), - ), - Effect.orDie, - ) - }), - grep: (input) => - Effect.gen(function* () { - const target = path.resolve(location.directory, input.path ?? ".") - const info = yield* fs.stat(target).pipe(Effect.orDie) - const cwd = info.type === "File" ? path.dirname(target) : target - return yield* ripgrep - .grep({ - cwd, - pattern: input.pattern, - file: info.type === "File" ? path.basename(target) : undefined, - include: input.include, - limit: input.limit ?? FileSystem.DEFAULT_SEARCH_LIMIT, - }) - .pipe( - Effect.map((result) => - result.map((match) => - FileSystem.Match.make({ - ...match, - entry: FileSystem.Entry.make({ - ...match.entry, - path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, match.entry.path))), - }), - }), - ), - ), - Effect.orDie, - ) - }), find: (input) => Effect.gen(function* () { const items = @@ -142,55 +87,10 @@ export const fffLayer = Layer.effect( 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 ?? FileSystem.DEFAULT_SEARCH_LIMIT, - }) - 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 ?? FileSystem.DEFAULT_SEARCH_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 } @@ -247,7 +147,7 @@ export const layer = (options?: Options) => ) export function configured(options?: Options) { - return makeLocationNode({ service: Service, layer: layer(options), deps: [FSUtil.node, Location.node, Ripgrep.node] }) + return makeLocationNode({ service: Service, layer: layer(options), deps: [Location.node, Ripgrep.node] }) } export const node = configured() diff --git a/packages/core/test/filesystem/search.test.ts b/packages/core/test/filesystem/search.test.ts index 488c17bab8d..501932fa462 100644 --- a/packages/core/test/filesystem/search.test.ts +++ b/packages/core/test/filesystem/search.test.ts @@ -1,5 +1,4 @@ import { describe, expect, test } from "bun:test" -import fs from "fs/promises" import os from "os" import path from "path" import { Effect, Layer } from "effect" @@ -7,48 +6,10 @@ 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" import { FileSystemSearch } from "@opencode-ai/core/filesystem/search" -import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Location } from "@opencode-ai/core/location" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" import { location } from "../fixture/location" -import { tmpdir } from "../fixture/tmpdir" -import { testEffect } from "../lib/effect" - -const it = testEffect(LayerNode.compile(Ripgrep.node)) - -const withTmp = (f: (directory: AbsolutePath) => Effect.Effect) => - Effect.acquireRelease( - Effect.promise(() => tmpdir()), - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ).pipe(Effect.flatMap((tmp) => f(AbsolutePath.make(tmp.path)))) - -describe("Ripgrep", () => { - it.live("globs files as an array", () => - withTmp((cwd) => - Effect.gen(function* () { - yield* Effect.promise(() => fs.mkdir(path.join(cwd, "src"))) - yield* Effect.promise(() => fs.writeFile(path.join(cwd, "src", "match.ts"), "needle\n")) - const result = yield* (yield* Ripgrep.Service).glob({ cwd, pattern: "**/*.ts", limit: 10 }) - expect(result.map((item) => item.path)).toEqual([RelativePath.make("src/match.ts")]) - }), - ), - ) - - it.live("greps files with include filtering", () => - withTmp((cwd) => - Effect.gen(function* () { - yield* Effect.promise(() => fs.mkdir(path.join(cwd, "src"))) - yield* Effect.promise(() => fs.writeFile(path.join(cwd, "src", "match.ts"), "needle\n")) - yield* Effect.promise(() => fs.writeFile(path.join(cwd, "src", "skip.txt"), "needle\n")) - const result = yield* (yield* Ripgrep.Service).grep({ cwd, pattern: "needle", include: "*.ts", limit: 10 }) - expect(result).toHaveLength(1) - expect(result[0]?.entry.path).toBe(RelativePath.make("src/match.ts")) - expect(result[0]?.submatches[0]?.text).toBe("needle") - }), - ), - ) -}) describe("FileSystemSearch", () => { test("bounds a home scan even when home is detected as a repository", async () => { diff --git a/packages/core/test/ripgrep.test.ts b/packages/core/test/ripgrep.test.ts index 49a932b732c..f885c09dde3 100644 --- a/packages/core/test/ripgrep.test.ts +++ b/packages/core/test/ripgrep.test.ts @@ -11,6 +11,44 @@ import { testEffect } from "./lib/effect" const it = testEffect(LayerNode.compile(Ripgrep.node)) describe("Ripgrep", () => { + it.live("globs files as an array", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src"))) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "match.ts"), "needle\n")) + + const result = yield* (yield* Ripgrep.Service).glob({ cwd: tmp.path, pattern: "**/*.ts", limit: 10 }) + expect(result.map((item) => item.path)).toEqual([RelativePath.make("src/match.ts")]) + }), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("greps files with include filtering", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src"))) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "match.ts"), "needle\n")) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "skip.txt"), "needle\n")) + + const result = yield* (yield* Ripgrep.Service).grep({ + cwd: tmp.path, + pattern: "needle", + include: "*.ts", + limit: 10, + }) + expect(result).toHaveLength(1) + expect(result[0]?.entry.path).toBe(RelativePath.make("src/match.ts")) + expect(result[0]?.submatches[0]?.text).toBe("needle") + }), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + it.live("keeps ignored files out of catch-all find results", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()),