mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-15 13:24:13 +00:00
Compare commits
4 Commits
dev
...
feat/fff-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8056842baf | ||
|
|
8ea1df1560 | ||
|
|
52ae62d4cc | ||
|
|
3c11eda350 |
27
bun.lock
27
bun.lock
@@ -288,6 +288,7 @@
|
||||
"@ai-sdk/vercel": "1.0.33",
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@ff-labs/bun": "0.1.37",
|
||||
"@gitlab/gitlab-ai-provider": "3.5.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.2",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
@@ -961,6 +962,8 @@
|
||||
|
||||
"@fastify/rate-limit": ["@fastify/rate-limit@10.3.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q=="],
|
||||
|
||||
"@ff-labs/bun": ["@ff-labs/bun@0.1.37", "", { "peerDependencies": { "bun": ">=1.0.0" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "fff": "scripts/cli.ts", "fff-demo": "examples/search.ts" } }, "sha512-kDdRSLXCKgZjqVQoVAy2SaXAtAKUsg1GXgYGYTQYGbLcqS1j8oBRA5abnDikamgFbH4QHMC/ml1fQTXUN94TUQ=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
|
||||
@@ -1305,6 +1308,28 @@
|
||||
|
||||
"@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="],
|
||||
|
||||
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-df7smckMWSUfaT5mzwN9Lfpd3ZGkOqo+vmQ8VV2a32gl14v6uZ/qeeo+1RlANXn8M0uzXPWWCkrKZIWSZUR0qw=="],
|
||||
|
||||
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-YiLxfsPzQqaVvT2a+nxH9do0YfUjrlxF3tKP0b1DDgvfgCcVKGsrQH3Wa82qHgL4dnT8h2bqi94JxXESEuPmcA=="],
|
||||
|
||||
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-XbhsA2XAFzvFr0vPSV6SNqGxab4xHKdPmVTLqoSHAx9tffrSq/012BDptOskulwnD+YNsrJUx2D2Ve1xvfgGcg=="],
|
||||
|
||||
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-VaNQTu0Up4gnwZLQ6/Hmho6jAlLxTQ1PwxEth8EsXHf82FOXXPV5OCQ6KC9mmmocjKlmWFaIGebThrOy8DUo4g=="],
|
||||
|
||||
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-t8uimCVBTw5f9K2QTZE5wN6UOrFETNrh/Xr7qtXT9nAOzaOnIFvYA+HcHbGfi31fRlCVfTxqm/EiCwJ1gEw9YQ=="],
|
||||
|
||||
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-oQyAW3+ugulvXTZ+XYeUMmNPR94sJeMokfHQoKwPvVwhVkgRuMhcLGV2ZesHCADVu30Oz2MFXbgdC8x4/o9dRg=="],
|
||||
|
||||
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-nZ12g22cy7pEOBwAxz2tp0wVqekaCn9QRKuGTHqOdLlyAqR4SCdErDvDhUWd51bIyHTQoCmj72TegGTgG0WNPw=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-4ZjIUgCxEyKwcKXideB5sX0KJpnHTZtu778w73VNq2uNH2fNpMZv98+DBgJyQ9OfFoRhmKn1bmLmSefvnHzI9w=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-3FXQgtYFsT0YOmAdMcJn56pLM5kzSl6y942rJJIl5l2KummB9Ea3J/vMJMzQk7NCAGhleZGWU/pJSS/uXKGa7w=="],
|
||||
|
||||
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/d6vAmgKvkoYlsGPsRPlPmOK1slPis/F40UG02pYwypTH0wmY0smgzdFqR4YmryxFh17XrW1kITv+U99Oajk9Q=="],
|
||||
|
||||
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-a/+hSrrDpMD7THyXvE2KJy1skxzAD0cnW4K1WjuI/91VqsphjNzvf5t/ZgxEVL4wb6f+hKrSJ5J3aH47zPr61g=="],
|
||||
|
||||
"@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.96.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lzeIEMu/v6Y+La5JSesq4hvyKtKBq84cgQpKYTYM/yGuNk2tfd5Ha31hnC+mTh48lp/5vZH+WBfjVUjjINCfug=="],
|
||||
|
||||
"@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.96.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-i0LkJAUXb4BeBFrJQbMKQPoxf8+cFEffDyLSb7NEzzKuPcH8qrVsnEItoOzeAdYam8Sr6qCHVwmBNEQzl7PWpw=="],
|
||||
@@ -2177,6 +2202,8 @@
|
||||
|
||||
"buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="],
|
||||
|
||||
"bun": ["bun@1.3.9", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.9", "@oven/bun-darwin-x64": "1.3.9", "@oven/bun-darwin-x64-baseline": "1.3.9", "@oven/bun-linux-aarch64": "1.3.9", "@oven/bun-linux-aarch64-musl": "1.3.9", "@oven/bun-linux-x64": "1.3.9", "@oven/bun-linux-x64-baseline": "1.3.9", "@oven/bun-linux-x64-musl": "1.3.9", "@oven/bun-linux-x64-musl-baseline": "1.3.9", "@oven/bun-windows-x64": "1.3.9", "@oven/bun-windows-x64-baseline": "1.3.9" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-v5hkh1us7sMNjfimWE70flYbD5I1/qWQaqmJ45q2qk5H/7muQVa478LSVRSFyGTBUBog2LsPQnfIRdjyWJRY+A=="],
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"@ai-sdk/vercel": "1.0.33",
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@ff-labs/bun": "0.1.37",
|
||||
"@gitlab/gitlab-ai-provider": "3.5.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.2",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
|
||||
29
packages/opencode/src/file/fff.ts
Normal file
29
packages/opencode/src/file/fff.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { FileFinder } from "@ff-labs/bun"
|
||||
import { Log } from "@/util/log"
|
||||
|
||||
export namespace FFF {
|
||||
const log = Log.create({ service: "file.fff" })
|
||||
|
||||
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 (!init(input.cwd)) return []
|
||||
|
||||
const result = FileFinder.search(input.query, {
|
||||
pageIndex: 0,
|
||||
pageSize: input.limit,
|
||||
})
|
||||
if (!result.ok) {
|
||||
log.error("search failed", { error: result.error, query: input.query, cwd: input.cwd })
|
||||
return []
|
||||
}
|
||||
|
||||
return result.value.items.map((item: { relativePath: string }) => item.relativePath)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import ignore from "ignore"
|
||||
import { Log } from "../util/log"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { FFF } from "./fff"
|
||||
import { Ripgrep } from "./ripgrep"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { Global } from "../global"
|
||||
@@ -541,42 +542,61 @@ export namespace File {
|
||||
})
|
||||
}
|
||||
|
||||
export async function search(input: { query: string; limit?: number; dirs?: boolean; type?: "file" | "directory" }) {
|
||||
export async function search(input: { query: string; limit?: number; type?: "file" | "directory" | "all" }) {
|
||||
const query = input.query.trim()
|
||||
const limit = input.limit ?? 100
|
||||
const kind = input.type ?? (input.dirs === false ? "file" : "all")
|
||||
const kind = input.type ?? "all"
|
||||
log.info("search", { query, kind })
|
||||
|
||||
const result = await state().then((x) => x.files())
|
||||
|
||||
const hidden = (item: string) => {
|
||||
const normalized = item.replaceAll("\\", "/").replace(/\/+$/, "")
|
||||
return normalized.split("/").some((p) => p.startsWith(".") && p.length > 1)
|
||||
}
|
||||
const preferHidden = query.startsWith(".") || query.includes("/.")
|
||||
const sortHiddenLast = (items: string[]) => {
|
||||
if (preferHidden) return items
|
||||
const visible: string[] = []
|
||||
const hiddenItems: string[] = []
|
||||
for (const item of items) {
|
||||
const isHidden = hidden(item)
|
||||
if (isHidden) hiddenItems.push(item)
|
||||
if (!isHidden) visible.push(item)
|
||||
const files = await FFF.search({
|
||||
cwd: Instance.directory,
|
||||
query,
|
||||
limit: kind === "all" || kind === "directory" ? limit * 20 : limit,
|
||||
})
|
||||
const set = new Set<string>()
|
||||
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
|
||||
}
|
||||
return [...visible, ...hiddenItems]
|
||||
}
|
||||
const allDirs = Array.from(set)
|
||||
|
||||
if (!query) {
|
||||
if (kind === "file") return result.files.slice(0, limit)
|
||||
return sortHiddenLast(result.dirs.toSorted()).slice(0, limit)
|
||||
const output = kind === "file" ? files.slice(0, limit) : allDirs.toSorted().slice(0, limit)
|
||||
log.info("search", { query, kind, results: output.length })
|
||||
return output
|
||||
}
|
||||
|
||||
const items =
|
||||
kind === "file" ? result.files : kind === "directory" ? result.dirs : [...result.files, ...result.dirs]
|
||||
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 searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit
|
||||
const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((r) => r.target)
|
||||
const output = kind === "directory" ? sortHiddenLast(sorted).slice(0, limit) : sorted
|
||||
if (kind === "file") {
|
||||
const output = files.slice(0, limit)
|
||||
log.info("search", { query, kind, results: output.length })
|
||||
return output
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
log.info("search", { query, kind, results: output.length })
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export const FileRoutes = lazy(() =>
|
||||
z.object({
|
||||
query: z.string(),
|
||||
dirs: z.enum(["true", "false"]).optional(),
|
||||
type: z.enum(["file", "directory"]).optional(),
|
||||
type: z.enum(["file", "directory", "all"]).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).optional(),
|
||||
}),
|
||||
),
|
||||
@@ -76,8 +76,7 @@ export const FileRoutes = lazy(() =>
|
||||
const results = await File.search({
|
||||
query,
|
||||
limit: limit ?? 10,
|
||||
dirs: dirs !== "false",
|
||||
type,
|
||||
type: dirs === "false" ? "file" : type,
|
||||
})
|
||||
return c.json(results)
|
||||
},
|
||||
|
||||
52
packages/opencode/test/file/fff.test.ts
Normal file
52
packages/opencode/test/file/fff.test.ts
Normal file
@@ -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)
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user