summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--bun.lock1
-rw-r--r--packages/opencode/package.json1
-rw-r--r--packages/opencode/src/cli/cmd/debug/file.ts23
-rw-r--r--packages/opencode/src/cli/cmd/debug/ripgrep.ts10
-rw-r--r--packages/opencode/src/file/index.ts47
-rw-r--r--packages/opencode/src/file/ripgrep.ts46
-rw-r--r--packages/opencode/src/server/server.ts5
-rw-r--r--packages/opencode/src/session/prompt.ts18
-rw-r--r--packages/opencode/src/tool/glob.ts2
-rw-r--r--packages/opencode/src/tool/ls.ts6
10 files changed, 132 insertions, 27 deletions
diff --git a/bun.lock b/bun.lock
index d1b83367a..8e803cfff 100644
--- a/bun.lock
+++ b/bun.lock
@@ -148,6 +148,7 @@
"chokidar": "4.0.3",
"decimal.js": "10.5.0",
"diff": "8.0.2",
+ "fuzzysort": "3.1.0",
"gray-matter": "4.0.3",
"hono": "catalog:",
"hono-openapi": "1.0.7",
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index d88615487..6dba841a8 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -42,6 +42,7 @@
"chokidar": "4.0.3",
"decimal.js": "10.5.0",
"diff": "8.0.2",
+ "fuzzysort": "3.1.0",
"gray-matter": "4.0.3",
"hono": "catalog:",
"hono-openapi": "1.0.7",
diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts
index 898b7d390..7be304737 100644
--- a/packages/opencode/src/cli/cmd/debug/file.ts
+++ b/packages/opencode/src/cli/cmd/debug/file.ts
@@ -2,6 +2,22 @@ import { File } from "../../../file"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
+const FileSearchCommand = cmd({
+ command: "search <query>",
+ builder: (yargs) =>
+ yargs.positional("query", {
+ type: "string",
+ demandOption: true,
+ description: "Search query",
+ }),
+ async handler(args) {
+ await bootstrap(process.cwd(), async () => {
+ const results = await File.search({ query: args.query })
+ console.log(results.join("\n"))
+ })
+ },
+})
+
const FileReadCommand = cmd({
command: "read <path>",
builder: (yargs) =>
@@ -48,6 +64,11 @@ const FileListCommand = cmd({
export const FileCommand = cmd({
command: "file",
builder: (yargs) =>
- yargs.command(FileReadCommand).command(FileStatusCommand).command(FileListCommand).demandCommand(),
+ yargs
+ .command(FileReadCommand)
+ .command(FileStatusCommand)
+ .command(FileListCommand)
+ .command(FileSearchCommand)
+ .demandCommand(),
async handler() {},
})
diff --git a/packages/opencode/src/cli/cmd/debug/ripgrep.ts b/packages/opencode/src/cli/cmd/debug/ripgrep.ts
index ec3f9cb83..884b291b5 100644
--- a/packages/opencode/src/cli/cmd/debug/ripgrep.ts
+++ b/packages/opencode/src/cli/cmd/debug/ripgrep.ts
@@ -40,12 +40,14 @@ const FilesCommand = cmd({
}),
async handler(args) {
await bootstrap(process.cwd(), async () => {
- const files = await Ripgrep.files({
+ const files: string[] = []
+ for await (const file of Ripgrep.files({
cwd: Instance.directory,
- query: args.query,
glob: args.glob ? [args.glob] : undefined,
- limit: args.limit,
- })
+ })) {
+ files.push(file)
+ if (args.limit && files.length >= args.limit) break
+ }
console.log(files.join("\n"))
})
},
diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts
index 8e142cf2b..20e1f604c 100644
--- a/packages/opencode/src/file/index.ts
+++ b/packages/opencode/src/file/index.ts
@@ -7,6 +7,8 @@ import fs from "fs"
import ignore from "ignore"
import { Log } from "../util/log"
import { Instance } from "../project/instance"
+import { Ripgrep } from "./ripgrep"
+import fuzzysort from "fuzzysort"
export namespace File {
const log = Log.create({ service: "file" })
@@ -74,6 +76,43 @@ export namespace File {
),
}
+ const state = Instance.state(async () => {
+ type Entry = { files: string[]; dirs: string[] }
+ let cache: Entry = { files: [], dirs: [] }
+ let fetching = false
+ const fn = async (result: Entry) => {
+ fetching = true
+ const set = new Set<string>()
+ for await (const file of Ripgrep.files({ cwd: Instance.directory })) {
+ result.files.push(file)
+ let current = file
+ while (true) {
+ const dir = path.dirname(current)
+ if (dir === current) break
+ current = dir
+ if (set.has(dir)) continue
+ set.add(dir)
+ result.dirs.push(dir + "/")
+ }
+ }
+ cache = result
+ fetching = false
+ }
+ fn(cache)
+
+ return {
+ async files() {
+ if (!fetching) {
+ fn({
+ files: [],
+ dirs: [],
+ })
+ }
+ return cache
+ },
+ }
+ })
+
export async function status() {
const project = Instance.project
if (project.vcs !== "git") return []
@@ -201,4 +240,12 @@ export namespace File {
return a.name.localeCompare(b.name)
})
}
+
+ export async function search(input: { query: string; limit?: number }) {
+ const limit = input.limit ?? 100
+ const result = await state().then((x) => x.files())
+ const items = input.query ? [...result.files, ...result.dirs] : [...result.dirs]
+ const sorted = fuzzysort.go(input.query, items, { limit: limit }).map((r) => r.target)
+ return sorted
+ }
}
diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts
index 1cbf6b8a8..d023f47c3 100644
--- a/packages/opencode/src/file/ripgrep.ts
+++ b/packages/opencode/src/file/ripgrep.ts
@@ -6,7 +6,7 @@ import z from "zod/v4"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
-import { Fzf } from "./fzf"
+
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
export namespace Ripgrep {
@@ -203,24 +203,48 @@ export namespace Ripgrep {
return filepath
}
- export async function files(input: { cwd: string; query?: string; glob?: string[]; limit?: number }) {
- const commands = [`${$.escape(await filepath())} --files --follow --hidden --glob='!.git/*'`]
-
+ export async function* files(input: { cwd: string; glob?: string[] }) {
+ const args = [await filepath(), "--files", "--follow", "--hidden", "--glob=!.git/*"]
if (input.glob) {
for (const g of input.glob) {
- commands[0] += ` --glob='${g}'`
+ args.push(`--glob=${g}`)
}
}
- if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
- if (input.limit) commands.push(`head -n ${input.limit}`)
- const joined = commands.join(" | ")
- const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
- return result.split("\n").filter(Boolean)
+ const proc = Bun.spawn(args, {
+ cwd: input.cwd,
+ stdout: "pipe",
+ stderr: "ignore",
+ maxBuffer: 1024 * 1024 * 20,
+ })
+
+ const reader = proc.stdout.getReader()
+ const decoder = new TextDecoder()
+ let buffer = ""
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ buffer += decoder.decode(value, { stream: true })
+ const lines = buffer.split("\n")
+ buffer = lines.pop() || ""
+
+ for (const line of lines) {
+ if (line) yield line
+ }
+ }
+
+ if (buffer) yield buffer
+ } finally {
+ reader.releaseLock()
+ await proc.exited
+ }
}
export async function tree(input: { cwd: string; limit?: number }) {
- const files = await Ripgrep.files({ cwd: input.cwd })
+ const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd }))
interface Node {
path: string[]
children: Node[]
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 544e58b38..7020a2aaa 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -956,12 +956,11 @@ export namespace Server {
),
async (c) => {
const query = c.req.valid("query").query
- const result = await Ripgrep.files({
- cwd: Instance.directory,
+ const results = await File.search({
query,
limit: 10,
})
- return c.json(result)
+ return c.json(results)
},
)
.get(
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 6ebae336b..8e7cf57f0 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -581,9 +581,15 @@ export namespace SessionPrompt {
}
break
case "file:":
+ log.info("file", { mime: part.mime })
// have to normalize, symbol search returns absolute paths
// Decode the pathname since URL constructor doesn't automatically decode it
- const filePath = decodeURIComponent(url.pathname)
+ const filepath = decodeURIComponent(url.pathname)
+ const stat = await Bun.file(filepath).stat()
+
+ if (stat.isDirectory()) {
+ part.mime = "application/x-directory"
+ }
if (part.mime === "text/plain") {
let offset: number | undefined = undefined
@@ -620,7 +626,7 @@ export namespace SessionPrompt {
limit = end - offset
}
}
- const args = { filePath, offset, limit }
+ const args = { filePath: filepath, offset, limit }
const result = await ReadTool.init().then((t) =>
t.execute(args, {
sessionID: input.sessionID,
@@ -658,7 +664,7 @@ export namespace SessionPrompt {
}
if (part.mime === "application/x-directory") {
- const args = { path: filePath }
+ const args = { path: filepath }
const result = await ListTool.init().then((t) =>
t.execute(args, {
sessionID: input.sessionID,
@@ -695,15 +701,15 @@ export namespace SessionPrompt {
]
}
- const file = Bun.file(filePath)
- FileTime.read(input.sessionID, filePath)
+ const file = Bun.file(filepath)
+ FileTime.read(input.sessionID, filepath)
return [
{
id: Identifier.ascending("part"),
messageID: info.id,
sessionID: input.sessionID,
type: "text",
- text: `Called the Read tool with the following input: {\"filePath\":\"${filePath}\"}`,
+ text: `Called the Read tool with the following input: {\"filePath\":\"${filepath}\"}`,
synthetic: true,
},
{
diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts
index dbbe88680..7553a5aa5 100644
--- a/packages/opencode/src/tool/glob.ts
+++ b/packages/opencode/src/tool/glob.ts
@@ -23,7 +23,7 @@ export const GlobTool = Tool.define("glob", {
const limit = 100
const files = []
let truncated = false
- for (const file of await Ripgrep.files({
+ for await (const file of Ripgrep.files({
cwd: search,
glob: [params.pattern],
})) {
diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts
index 819e6fdea..b80f668a5 100644
--- a/packages/opencode/src/tool/ls.ts
+++ b/packages/opencode/src/tool/ls.ts
@@ -44,7 +44,11 @@ export const ListTool = Tool.define("list", {
const searchPath = path.resolve(Instance.directory, params.path || ".")
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
- const files = await Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs, limit: LIMIT })
+ const files = []
+ for await (const file of Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs })) {
+ files.push(file)
+ if (files.length >= LIMIT) break
+ }
// Build directory structure
const dirs = new Set<string>()