Skip to content

Commit e69acf4

Browse files
feat: fff tools for file search
Co-Authored-By: Shoubhit Dash <shoubhit2005@gmail.com>
1 parent 0954966 commit e69acf4

25 files changed

Lines changed: 1036 additions & 110 deletions

bun.lock

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bunfig.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
exact = true
33
# Only install newly resolved package versions published at least 3 days ago.
44
minimumReleaseAge = 259200
5-
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid"]
5+
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "@ff-labs/fff-node", "@ff-labs/fff-bun"]
66

77
[test]
88
root = "./do-not-run-tests-from-root"

packages/opencode/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
"node": "./src/storage/db.node.ts",
2929
"default": "./src/storage/db.bun.ts"
3030
},
31+
"#fff": {
32+
"bun": "./src/file/fff.bun.ts",
33+
"node": "./src/file/fff.node.ts",
34+
"default": "./src/file/fff.bun.ts"
35+
},
3136
"#pty": {
3237
"bun": "./src/pty/pty.bun.ts",
3338
"node": "./src/pty/pty.node.ts",
@@ -93,6 +98,8 @@
9398
"@clack/prompts": "1.0.0-alpha.1",
9499
"@effect/opentelemetry": "catalog:",
95100
"@effect/platform-node": "catalog:",
101+
"@ff-labs/fff-bun": "0.8.1",
102+
"@ff-labs/fff-node": "0.8.1",
96103
"@gitlab/opencode-gitlab-auth": "1.3.3",
97104
"@lydell/node-pty": "catalog:",
98105
"@modelcontextprotocol/sdk": "1.27.1",
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Effect } from "effect"
2+
import { Fff } from "#fff"
3+
import { AppRuntime } from "@/effect/app-runtime"
4+
import { Search } from "@/file/search"
5+
import { InstanceStore } from "@/project/instance-store"
6+
7+
const dir = process.cwd()
8+
9+
const FILE_QUERIES = ["fff", "package.json", "tools/ experiment"]
10+
const GREP_QUERIES = ["FileFinder", "import", "grep", "autocomplete"]
11+
const GLOB_QUERIES = ["**/*.test.ts"]
12+
13+
const FILE_LIMIT = 100
14+
const GREP_LIMIT = 50
15+
const GLOB_LIMIT = 50
16+
17+
const run = <A>(effect: Effect.Effect<A, unknown, Search.Service>) =>
18+
AppRuntime.runPromise(
19+
InstanceStore.Service.use((store) => store.provide({ directory: dir }, effect as never)),
20+
) as Promise<A>
21+
22+
// --- raw Fff picker ---
23+
const t0 = performance.now()
24+
const made = Fff.create({ basePath: dir, aiMode: true })
25+
if (!made.ok) {
26+
console.error("Fff.create failed:", made.error)
27+
process.exit(1)
28+
}
29+
const picker = made.value
30+
console.log(`picker create: ${(performance.now() - t0).toFixed(1)}ms`)
31+
32+
const tw = performance.now()
33+
const deadline = tw + 2500
34+
while (picker.isScanning() && performance.now() < deadline) {
35+
await new Promise((resolve) => setTimeout(resolve, 25))
36+
}
37+
console.log(`wait for scan (poll): ${(performance.now() - tw).toFixed(1)}ms`)
38+
39+
// warmup grep to let the content index build
40+
const tWarmup = performance.now()
41+
picker.grep("_warmup_", { mode: "regex", maxMatchesPerFile: 1, timeBudgetMs: 1_500 })
42+
console.log(`grep warmup: ${(performance.now() - tWarmup).toFixed(1)}ms`)
43+
44+
console.log()
45+
console.log("--- raw picker (warm) ---")
46+
47+
for (const q of FILE_QUERIES) {
48+
const t = performance.now()
49+
const r = picker.fileSearch(q, { pageSize: Math.max(FILE_LIMIT, 100) })
50+
const count = r.ok ? r.value.items.length : "err"
51+
console.log(`[picker] fileSearch "${q}": ${(performance.now() - t).toFixed(1)}ms (${count} results)`)
52+
}
53+
54+
for (const q of GREP_QUERIES) {
55+
const t = performance.now()
56+
const r = picker.grep(q, { mode: "regex", pageSize: GREP_LIMIT, timeBudgetMs: 1_500 })
57+
const count = r.ok ? r.value.items.length : "err"
58+
console.log(`[picker] grep "${q}": ${(performance.now() - t).toFixed(1)}ms (${count} matches)`)
59+
}
60+
61+
picker.destroy()
62+
63+
// --- Ripgrep service (via Search with file:["."] to force rg path) ---
64+
console.log()
65+
console.log("--- Ripgrep (via Search service) ---")
66+
67+
// warmup
68+
await run(Search.Service.use((svc) => svc.search({ cwd: dir, pattern: "_warmup_rg_", limit: 1, file: ["."] })))
69+
70+
for (const q of GREP_QUERIES) {
71+
const t = performance.now()
72+
const r = await run(Search.Service.use((svc) => svc.search({ cwd: dir, pattern: q, limit: GREP_LIMIT, file: ["."] })))
73+
console.log(
74+
`[ripgrep] grep "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.items.length} total, limit is per-file not total)`,
75+
)
76+
}
77+
78+
// --- Search service: init breakdown ---
79+
console.log()
80+
81+
// 1) runtime + InstanceState + picker create + scan poll
82+
const tRuntime = performance.now()
83+
await run(Search.Service.use((svc) => svc.file({ cwd: dir, query: "_warmup_file_", limit: 1 })))
84+
console.log(`[Search] init file (runtime + picker + scan): ${(performance.now() - tRuntime).toFixed(1)}ms`)
85+
86+
// 2) grep warmup (content index cold-start inside the Search service picker)
87+
const tGrepWarmup = performance.now()
88+
await run(Search.Service.use((svc) => svc.search({ cwd: dir, pattern: "_warmup_grep_", limit: 1 })))
89+
console.log(`[Search] init grep (content index warmup): ${(performance.now() - tGrepWarmup).toFixed(1)}ms`)
90+
91+
console.log()
92+
console.log("--- Search service (warm) ---")
93+
94+
for (const q of FILE_QUERIES) {
95+
const t = performance.now()
96+
const r = await run(Search.Service.use((svc) => svc.file({ cwd: dir, query: q, limit: FILE_LIMIT })))
97+
console.log(
98+
`[Search.file] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r?.length ?? "undefined (cache fallback)"} results)`,
99+
)
100+
}
101+
102+
for (const q of GREP_QUERIES) {
103+
const t = performance.now()
104+
const r = await run(Search.Service.use((svc) => svc.search({ cwd: dir, pattern: q, limit: GREP_LIMIT })))
105+
console.log(
106+
`[Search.search] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.items.length} matches, engine=${r.engine})`,
107+
)
108+
}
109+
110+
for (const q of GLOB_QUERIES) {
111+
const t = performance.now()
112+
const r = await run(Search.Service.use((svc) => svc.glob({ cwd: dir, pattern: q, limit: GLOB_LIMIT })))
113+
console.log(
114+
`[Search.glob] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.files.length} files, truncated=${r.truncated})`,
115+
)
116+
}
117+
118+
process.exit(0)
119+

packages/opencode/src/cli/cmd/debug/file.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EOL } from "os"
22
import { Effect } from "effect"
33
import { File } from "../../../file"
4-
import { Ripgrep } from "@/file/ripgrep"
4+
import { Search } from "@/file/search"
55
import { effectCmd } from "../../effect-cmd"
66
import { cmd } from "../cmd"
77

@@ -70,7 +70,7 @@ const FileTreeCommand = effectCmd({
7070
default: process.cwd(),
7171
}),
7272
handler: Effect.fn("Cli.debug.file.tree")(function* (args) {
73-
const tree = yield* Effect.orDie(Ripgrep.Service.use((svc) => svc.tree({ cwd: args.dir, limit: 200 })))
73+
const tree = yield* Effect.orDie(Search.Service.use((svc) => svc.tree({ cwd: args.dir, limit: 200 })))
7474
console.log(JSON.stringify(tree, null, 2))
7575
}),
7676
})

packages/opencode/src/cli/cmd/debug/ripgrep.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EOL } from "os"
22
import { Effect, Stream } from "effect"
3-
import { Ripgrep } from "../../../file/ripgrep"
3+
import { Search } from "../../../file/search"
44
import { effectCmd } from "../../effect-cmd"
55
import { cmd } from "../cmd"
66
import { InstanceRef } from "@/effect/instance-ref"
@@ -22,7 +22,7 @@ const TreeCommand = effectCmd({
2222
handler: Effect.fn("Cli.debug.rg.tree")(function* (args) {
2323
const ctx = yield* InstanceRef
2424
if (!ctx) return
25-
const tree = yield* Effect.orDie(Ripgrep.Service.use((svc) => svc.tree({ cwd: ctx.directory, limit: args.limit })))
25+
const tree = yield* Effect.orDie(Search.Service.use((svc) => svc.tree({ cwd: ctx.directory, limit: args.limit })))
2626
process.stdout.write(tree + EOL)
2727
}),
2828
})
@@ -47,8 +47,8 @@ const FilesCommand = effectCmd({
4747
handler: Effect.fn("Cli.debug.rg.files")(function* (args) {
4848
const ctx = yield* InstanceRef
4949
if (!ctx) return
50-
const rg = yield* Ripgrep.Service
51-
const files = yield* rg
50+
const search = yield* Search.Service
51+
const files = yield* search
5252
.files({
5353
cwd: ctx.directory,
5454
glob: args.glob ? [args.glob] : undefined,
@@ -85,7 +85,7 @@ const SearchCommand = effectCmd({
8585
const ctx = yield* InstanceRef
8686
if (!ctx) return
8787
const results = yield* Effect.orDie(
88-
Ripgrep.Service.use((svc) =>
88+
Search.Service.use((svc) =>
8989
svc.search({
9090
cwd: ctx.directory,
9191
pattern: args.pattern,

0 commit comments

Comments
 (0)