Browse Source

fix: speed up fff file search (#31366)

Shoubhit Dash 3 tháng trước cách đây
mục cha
commit
b1a6c40ad0

+ 1 - 0
packages/app/src/components/dialog-select-file.tsx

@@ -407,6 +407,7 @@ export function DialogSelectFile(props: {
         items={items}
         key={(item) => item.id}
         filterKeys={["title", "description", "category"]}
+        skipFilter={(item) => item.type === "file"}
         groupBy={grouped() ? (item) => item.category : () => ""}
         onMove={handleMove}
         onSelect={handleSelect}

+ 1 - 0
packages/app/src/components/prompt-input.tsx

@@ -658,6 +658,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     },
     key: atKey,
     filterKeys: ["display"],
+    skipFilter: (item) => item.type === "file" && !item.recent,
     groupBy: (item) => {
       if (item.type === "agent") return "agent"
       if (item.recent) return "recent"

+ 15 - 19
packages/core/src/filesystem/search.ts

@@ -134,15 +134,12 @@ function item(hit: Fff.Hit): Item {
 }
 
 function collectPaths<T>(
-  out: { items: T[]; scores: Array<{ total: number }> },
+  items: T[],
   toPath: (item: T) => string,
-  opts?: { includeZeroScore?: boolean },
 ): string[] {
   return Array.from(
     new Set(
-      out.items.flatMap((item, idx): string[] => {
-        const score = out.scores[idx]
-        if (!score || (!opts?.includeZeroScore && score.total <= 0)) return []
+      items.flatMap((item): string[] => {
         const text = toPath(item)
         if (!text) return []
         return [text]
@@ -162,7 +159,7 @@ function searchFff(
     if (!out.ok) return out
     return {
       ok: true,
-      value: collectPaths(out.value, (entry) => normalize(entry.relativePath), { includeZeroScore: !query }),
+      value: collectPaths(out.value.items, (entry) => normalize(entry.relativePath)),
     }
   }
   if (kind === "all") {
@@ -170,14 +167,14 @@ function searchFff(
     if (!out.ok) return out
     return {
       ok: true,
-      value: collectPaths(out.value, (entry) => normalize(entry.item.relativePath), { includeZeroScore: !query }),
+      value: collectPaths(out.value.items, (entry) => normalize(entry.item.relativePath)),
     }
   }
   const out = pick.fileSearch(query, opts)
   if (!out.ok) return out
   return {
     ok: true,
-    value: collectPaths(out.value, (entry) => normalize(entry.relativePath), { includeZeroScore: !query }),
+    value: collectPaths(out.value.items, (entry) => normalize(entry.relativePath)),
   }
 }
 
@@ -241,6 +238,13 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Ripgrep.Service
       // remove before process exit, so use ripgrep instead of native FFF there.
       if (process.env.OPENCODE_TEST_HOME) return undefined
 
+      const dir = FSUtil.resolve(cwd)
+      const existing = state.pick.get(dir)
+      if (existing) return existing
+
+      const pending = state.wait.get(dir)
+      if (pending) return yield* Deferred.await(pending)
+
       const available = yield* fffSync("check availability", () => Fff.available()).pipe(
         Effect.catch((error) => {
           log.warn("fff availability check failed", { error })
@@ -249,13 +253,6 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Ripgrep.Service
       )
       if (!available) return undefined
 
-      const dir = FSUtil.resolve(cwd)
-      const existing = state.pick.get(dir)
-      if (existing) return existing
-
-      const pending = state.wait.get(dir)
-      if (pending) return yield* Deferred.await(pending)
-
       const gate = yield* Deferred.make<Picker, Error>()
       state.wait.set(dir, gate)
       return yield* Effect.gen(function* () {
@@ -353,13 +350,12 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Ripgrep.Service
       const query = input.query.trim()
       const kind = input.kind ?? "file"
 
-      const pick = yield* picker(input.cwd)
-      if (!pick) return undefined
-
+      const entry = yield* acquire(input.cwd).pipe(Effect.catch(() => Effect.succeed<Picker | undefined>(undefined)))
+      if (!entry) return undefined
       const dir = FSUtil.resolve(input.cwd)
       const limit = input.limit ?? 100
       const fffResult = yield* fffSync(`${kind} search`, () =>
-        searchFff(pick, kind, query, {
+        searchFff(entry.pick, kind, query, {
           pageIndex: 0,
           currentFile: input.current, // supports both relative and absolute (relative preferred)
           pageSize: limit,

+ 14 - 14
packages/opencode/src/cli/cmd/run/footer.prompt.tsx

@@ -368,19 +368,6 @@ export function createPromptState(input: PromptInput): PromptState {
       const next = extractLineRange(value)
       const list = await input.findFiles(next.base)
       return list
-        .sort((a, b) => {
-          const dir = Number(b.endsWith("/")) - Number(a.endsWith("/"))
-          if (dir !== 0) {
-            return dir
-          }
-
-          const depth = a.split("/").length - b.split("/").length
-          if (depth !== 0) {
-            return depth
-          }
-
-          return a.localeCompare(b)
-        })
         .map((item): Auto => {
           const url = pathToFileURL(path.resolve(input.directory, item))
           let filename = item
@@ -472,8 +459,21 @@ export function createPromptState(input: PromptInput): PromptState {
       return mixed
     }
 
+    const next = removeLineRange(query())
+    if (mode() === "mention") {
+      return [
+        ...fuzzysort
+          .go(next, agents(), { keys: ["value", "display", "description"] })
+          .map((item) => item.obj),
+        ...files(),
+        ...fuzzysort
+          .go(next, resources(), { keys: ["value", "display", "description"] })
+          .map((item) => item.obj),
+      ]
+    }
+
     return fuzzysort
-      .go(removeLineRange(query()), mixed, {
+      .go(next, mixed, {
         keys: [(item) => (item.kind === "mention" ? item.value : item.name).trimEnd(), "display", "description"],
       })
       .map((item) => item.obj)

+ 27 - 2
packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts

@@ -4,12 +4,15 @@ import { LocationServiceMap } from "@opencode-ai/core/location-layer"
 import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
 import { Search } from "@opencode-ai/core/filesystem/search"
 import { FSUtil } from "@opencode-ai/core/fs-util"
+import { Log } from "@opencode-ai/core/util/log"
 import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
 import { Effect, Layer } from "effect"
 import path from "path"
 import { HttpApiBuilder } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../api"
 
+const log = Log.create({ service: "server.file" })
+
 export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
   Effect.gen(function* () {
     const ripgrep = yield* Ripgrep.Service
@@ -34,11 +37,23 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
       const directory = (yield* InstanceState.context).directory
       const limit = ctx.query.limit ?? 10
       const kind = ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : "all")
+      const started = performance.now()
       // Prefer fff (frecency + fuzzy ranking) and trust its ordering. Fall back
       // to the ripgrep-backed FileSystem.find when fff is unavailable.
       const fff = yield* search.file({ cwd: directory, query: ctx.query.query, limit, kind }).pipe(Effect.orDie)
-      if (fff !== undefined) return fff
-      return (yield* filesystem(
+      if (fff !== undefined) {
+        log.info("find file", {
+          engine: "fff",
+          query: ctx.query.query,
+          kind,
+          directory,
+          limit,
+          results: fff.length,
+          duration: Math.round(performance.now() - started),
+        })
+        return fff
+      }
+      const fallback = (yield* filesystem(
         FileSystem.Service.use((fs) =>
           fs.find({
             query: ctx.query.query,
@@ -47,6 +62,16 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
           }),
         ),
       )).map((item) => item.path)
+      log.info("find file", {
+        engine: "ripgrep",
+        query: ctx.query.query,
+        kind,
+        directory,
+        limit,
+        results: fallback.length,
+        duration: Math.round(performance.now() - started),
+      })
+      return fallback
     })
 
     const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {

+ 1 - 0
packages/ui/src/components/line-comment.tsx

@@ -240,6 +240,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
     },
     key: (item) => item.path,
     filterKeys: ["path"],
+    skipFilter: () => true,
     onSelect: selectMention,
   })
 

+ 9 - 4
packages/ui/src/hooks/use-filtered-list.tsx

@@ -12,6 +12,7 @@ export interface FilteredListProps<T> {
   groupBy?: (x: T) => string
   sortBy?: (a: T, b: T) => number
   sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
+  skipFilter?: (item: T) => boolean
   onSelect?: (value: T | undefined, index: number) => void
   noInitialSelection?: boolean
 }
@@ -35,10 +36,14 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
         all,
         (x) => {
           if (!needle) return x
-          if (!props.filterKeys && Array.isArray(x) && x.every((e) => typeof e === "string")) {
-            return fuzzysort.go(needle, x).map((x) => x.target) as T[]
-          }
-          return fuzzysort.go(needle, x, { keys: props.filterKeys! }).map((x) => x.obj)
+          const skipFilter = props.skipFilter
+          const filterable = skipFilter ? x.filter((item) => !skipFilter(item)) : x
+          const skipped = skipFilter ? x.filter(skipFilter) : []
+          const filtered =
+            !props.filterKeys && Array.isArray(filterable) && filterable.every((e) => typeof e === "string")
+              ? (fuzzysort.go(needle, filterable).map((x) => x.target) as T[])
+              : fuzzysort.go(needle, filterable, { keys: props.filterKeys! }).map((x) => x.obj)
+          return skipped.length ? [...filtered, ...skipped] : filtered
         },
         groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
         entries(),