Skip to content

Commit d6094c4

Browse files
committed
fix: replace fuzzy search with alphabetical tiered matching for autocomplete
Replace fuzzysort-based autocomplete with deterministic alphabetical sorting that: - Tier 1: Commands starting with filter (prefix matches) - Tier 2: Commands containing filter in name (substring matches) - Tier 3: Commands matching only in description/aliases Each tier is sorted alphabetically, with up to 100 results total. Fixes unpredictable ordering where '/wrong-branch' appeared before '/auto-integrate-branches'.
1 parent 455e492 commit d6094c4

File tree

1 file changed

+36
-20
lines changed

1 file changed

+36
-20
lines changed

packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core"
2-
import fuzzysort from "fuzzysort"
32
import { firstBy } from "remeda"
43
import { createMemo, createResource, createEffect, onMount, onCleanup, For, Show, createSignal } from "solid-js"
54
import { createStore } from "solid-js/store"
@@ -62,6 +61,41 @@ export type AutocompleteOption = {
6261
path?: string
6362
}
6463

64+
function tieredMatch(
65+
items: AutocompleteOption[],
66+
needle: string,
67+
prefix: string,
68+
limit: number = 100,
69+
): AutocompleteOption[] {
70+
const lowerNeedle = needle.toLowerCase()
71+
const fullNeedle = (prefix + needle).toLowerCase()
72+
73+
const tier1: AutocompleteOption[] = []
74+
const tier2: AutocompleteOption[] = []
75+
const tier3: AutocompleteOption[] = []
76+
77+
for (const item of items) {
78+
const display = item.display.trimEnd().toLowerCase()
79+
80+
if (display.startsWith(fullNeedle)) {
81+
tier1.push(item)
82+
} else if (display.includes(lowerNeedle)) {
83+
tier2.push(item)
84+
} else {
85+
const descMatch = item.description?.toLowerCase().includes(lowerNeedle)
86+
const aliasMatch = item.aliases?.some((a) => a.toLowerCase().includes(lowerNeedle))
87+
if (descMatch || aliasMatch) {
88+
tier3.push(item)
89+
}
90+
}
91+
}
92+
93+
const sortByDisplay = (a: AutocompleteOption, b: AutocompleteOption) =>
94+
a.display.trimEnd().localeCompare(b.display.trimEnd())
95+
96+
return [...tier1.sort(sortByDisplay), ...tier2.sort(sortByDisplay), ...tier3.sort(sortByDisplay)].slice(0, limit)
97+
}
98+
6599
export function Autocomplete(props: {
66100
value: string
67101
sessionID?: string
@@ -488,25 +522,7 @@ export function Autocomplete(props: {
488522
return prev
489523
}
490524

491-
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
492-
keys: [
493-
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
494-
"description",
495-
(obj) => obj.aliases?.join(" ") ?? "",
496-
],
497-
limit: 10,
498-
scoreFn: (objResults) => {
499-
const displayResult = objResults[0]
500-
let score = objResults.score
501-
if (displayResult && displayResult.target.startsWith(store.visible + currentFilter)) {
502-
score *= 2
503-
}
504-
const frecencyScore = objResults.obj.path ? frecency.getFrecency(objResults.obj.path) : 0
505-
return score * (1 + frecencyScore)
506-
},
507-
})
508-
509-
return result.map((arr) => arr.obj)
525+
return tieredMatch(mixed, currentFilter, store.visible || "/", 100)
510526
})
511527

512528
createEffect(() => {

0 commit comments

Comments
 (0)