diff --git a/packages/opencode/src/file/fff.ts b/packages/opencode/src/file/fff.ts index 217232b505..cf72276cce 100644 --- a/packages/opencode/src/file/fff.ts +++ b/packages/opencode/src/file/fff.ts @@ -1,23 +1,19 @@ import { FileFinder } from "@ff-labs/bun" import { Log } from "@/util/log" -import { lazy } from "../util/lazy" export namespace FFF { const log = Log.create({ service: "file.fff" }) - let base = "" - const init = lazy(() => { - const result = FileFinder.init({ basePath: base }) - if (!result.ok) { - log.error("init failed", { error: result.error, cwd: base }) - return false - } - return true - }) + + const init = (cwd: string) => { + const result = FileFinder.init({ basePath: cwd }) + if (result.ok) return true + log.error("init failed", { error: result.error, cwd }) + return false + } export async function search(input: { cwd: string; query: string; limit: number }) { if (!input.query) return [] - if (!base) base = input.cwd - if (!init()) return [] + if (!init(input.cwd)) return [] const result = FileFinder.search(input.query, { pageIndex: 0, diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 9e784f911b..341d0f04da 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -548,43 +548,55 @@ export namespace File { const kind = input.type ?? "all" log.info("search", { query, kind }) - if (!query) { - const result = await state().then((x) => x.files()) - if (kind === "file") return result.files.slice(0, limit) - return result.dirs.toSorted().slice(0, limit) + const files = await FFF.search({ + cwd: Instance.directory, + query, + limit: kind === "all" || kind === "directory" ? limit * 20 : limit, + }) + const set = new Set() + for (const file of files) { + let dir = path.dirname(file) + while (true) { + if (dir === ".") break + const next = path.dirname(dir) + set.add(dir + "/") + if (next === dir) break + dir = next + } } + const allDirs = Array.from(set) - if (kind === "directory") { - const result = await state().then((x) => x.files()) - const searchLimit = limit * 20 - const output = fuzzysort - .go(query, result.dirs, { limit: searchLimit }) - .map((r) => r.target) - .slice(0, limit) + if (!query) { + const output = kind === "file" ? files.slice(0, limit) : allDirs.toSorted().slice(0, limit) + log.info("search", { query, kind, results: output.length }) + return output + } + + if (kind === "directory") { + const ranked: string[] = [] + for (const item of fuzzysort.go(query, allDirs, { limit: limit * 20 })) { + ranked.push(item.target) + } + const output = ranked.slice(0, limit) log.info("search", { query, kind, results: output.length }) return output } - const files = await FFF.search({ - cwd: Instance.directory, - query, - limit, - }) - const fileOutput = files.slice(0, limit) if (kind === "file") { - log.info("search", { query, kind, results: fileOutput.length }) - return fileOutput + const output = files.slice(0, limit) + log.info("search", { query, kind, results: output.length }) + return output } - const result = await state().then((x) => x.files()) - const remaining = limit - fileOutput.length - if (remaining <= 0) { - log.info("search", { query, kind, results: fileOutput.length }) - return fileOutput + const rankedDirs: string[] = [] + for (const item of fuzzysort.go(query, allDirs, { limit })) { + rankedDirs.push(item.target) + } + const merged = files.slice(0, limit).concat(rankedDirs) + const output: string[] = [] + for (const item of fuzzysort.go(query, merged, { limit })) { + output.push(item.target) } - const sorted = fuzzysort.go(query, result.dirs, { limit: remaining }).map((r) => r.target) - const output = fileOutput.concat(sorted) - log.info("search", { query, kind, results: output.length }) return output } diff --git a/packages/opencode/test/file/fff.test.ts b/packages/opencode/test/file/fff.test.ts new file mode 100644 index 0000000000..10b927e2e5 --- /dev/null +++ b/packages/opencode/test/file/fff.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { tmpdir } from "../fixture/fixture" +import { FFF } from "../../src/file/fff" +import { File } from "../../src/file" +import { Instance } from "../../src/project/instance" + +describe("file.fff", () => { + test("returns files and supports directory search via File.search", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "src", "app", "index.ts"), "export const app = true") + await Bun.write(path.join(dir, "src", "app", "util.ts"), "export const util = true") + await Bun.write(path.join(dir, "docs", "guide.md"), "# guide") + }, + }) + + const files = await FFF.search({ + cwd: tmp.path, + query: "index", + limit: 20, + }) + expect(files.includes(path.join("src", "app", "index.ts"))).toBe(true) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const found = await File.search({ + query: "index", + type: "file", + limit: 20, + }) + expect(found.includes(path.join("src", "app", "index.ts"))).toBe(true) + + const dirs = await File.search({ + query: "app", + type: "directory", + limit: 20, + }) + expect(dirs.includes("src/app/")).toBe(true) + + const all = await File.search({ + query: "app", + type: "all", + limit: 20, + }) + expect(all.includes(path.join("src", "app", "index.ts"))).toBe(true) + expect(all.includes("src/app/")).toBe(true) + }, + }) + }) +})