Переглянути джерело

fix: stabilize fff file results (#31369)

Shoubhit Dash 2 місяців тому
батько
коміт
4119051077

+ 17 - 9
packages/core/src/filesystem/search.ts

@@ -133,14 +133,22 @@ function item(hit: Fff.Hit): Item {
   }
 }
 
-function collectPaths<T>(items: T[], toPath: (item: T) => string): string[] {
+function collectPaths<T>(items: T[], scores: Array<{ total: number }>, toPath: (item: T) => string): string[] {
+  const rows = items.flatMap((item, index): Array<{ text: string; score: number }> => {
+    const text = toPath(item)
+    if (!text) return []
+    return [{ text, score: scores[index]?.total ?? 0 }]
+  })
+  rows.sort(
+    (a, b) =>
+      b.score - a.score ||
+      a.text.length - b.text.length ||
+      (a.text < b.text ? -1 : a.text > b.text ? 1 : 0),
+  )
+
   return Array.from(
     new Set(
-      items.flatMap((item): string[] => {
-        const text = toPath(item)
-        if (!text) return []
-        return [text]
-      }),
+      rows.map((item) => item.text),
     ),
   )
 }
@@ -156,7 +164,7 @@ function searchFff(
     if (!out.ok) return out
     return {
       ok: true,
-      value: collectPaths(out.value.items, (entry) => normalize(entry.relativePath)),
+      value: collectPaths(out.value.items, out.value.scores, (entry) => normalize(entry.relativePath)),
     }
   }
   if (kind === "all") {
@@ -164,14 +172,14 @@ function searchFff(
     if (!out.ok) return out
     return {
       ok: true,
-      value: collectPaths(out.value.items, (entry) => normalize(entry.item.relativePath)),
+      value: collectPaths(out.value.items, out.value.scores, (entry) => normalize(entry.item.relativePath)),
     }
   }
   const out = pick.fileSearch(query, opts)
   if (!out.ok) return out
   return {
     ok: true,
-    value: collectPaths(out.value.items, (entry) => normalize(entry.relativePath)),
+    value: collectPaths(out.value.items, out.value.scores, (entry) => normalize(entry.relativePath)),
   }
 }
 

+ 20 - 0
packages/core/test/filesystem/search.test.ts

@@ -19,6 +19,8 @@ const tmpdir = (init?: (dir: string) => Effect.Effect<void>) =>
   ).pipe(Effect.tap((dir) => init?.(dir) ?? Effect.void))
 
 const write = (file: string, data: string) => Effect.promise(() => Bun.write(file, data))
+const waitForFileIndex = (search: Search.Interface, cwd: string) =>
+  search.glob({ cwd, pattern: "**/*", limit: 1 }).pipe(Effect.ignore)
 
 describe("file.search", () => {
   it.live("uses fff for Bun-backed grep", () =>
@@ -43,6 +45,7 @@ describe("file.search", () => {
       yield* write(path.join(dir, "README.md"), "hello\n")
 
       const search = yield* Search.Service
+      yield* waitForFileIndex(search, dir)
       const results = yield* search.file({ cwd: dir, query: "rdme", limit: 10 })
 
       expect(results).toContain("README.md")
@@ -57,6 +60,7 @@ describe("file.search", () => {
       yield* write(path.join(dir, "src", "main.ts"), "export const main = true\n")
 
       const search = yield* Search.Service
+      yield* waitForFileIndex(search, dir)
       const results = yield* search.file({ cwd: dir, query: "", limit: 10, kind: "all" })
 
       expect(results).toContain("README.md")
@@ -65,6 +69,21 @@ describe("file.search", () => {
     }),
   )
 
+  it.live("stabilizes equal score file candidates by path length", () =>
+    Effect.gen(function* () {
+      expect(Fff.available()).toBe(true)
+      const dir = yield* tmpdir()
+      yield* write(path.join(dir, "src", "longer-name.ts"), "export const longer = true\n")
+      yield* write(path.join(dir, "a.ts"), "export const shorter = true\n")
+
+      const search = yield* Search.Service
+      yield* waitForFileIndex(search, dir)
+      const results = yield* search.file({ cwd: dir, query: "", limit: 10 })
+
+      expect(results?.slice(0, 2)).toEqual(["a.ts", "src/longer-name.ts"])
+    }),
+  )
+
   it.live("keeps paging grep results without an explicit limit", () =>
     Effect.gen(function* () {
       expect(Fff.available()).toBe(true)
@@ -142,6 +161,7 @@ describe("file.search", () => {
       yield* write(path.join(dir, "alpha-target-two.ts"), "export const two = 2\n")
 
       const search = yield* Search.Service
+      yield* waitForFileIndex(search, dir)
       const results = yield* search.file({ cwd: dir, query: "alpha target two", limit: 10 })
       expect(results).toContain("alpha-target-two.ts")