fix(history): substring fuzzy search (#74)

closes #73

<img width="1434" height="302" alt="image"
src="https://github.com/user-attachments/assets/87bb8431-40c7-4ee8-ad58-79c4fea6efa7"
/>

I used use prefix match
imagine you have a long command ENOUGH that you used to use (like
image), with fuzzy search + substring, it will always show suggestion
about that command in the list but in the bottom
and I see this very annoying

but now I limit the score from fuzzy search, if the command is too long
(which means fuzzy score is extremely weak), skip it right away

<img width="1296" height="437" alt="image"
src="https://github.com/user-attachments/assets/77d807a2-ddce-4c37-9f5b-a02cd44de325"
/>

it now has substring support
This commit is contained in:
VERSE
2026-07-30 21:11:34 +07:00
committed by GitHub
parent 0049256844
commit 37182f7212
3 changed files with 76 additions and 77 deletions
+25 -75
View File
@@ -208,52 +208,33 @@ func SearchHistory(query string, aliases map[string]string) ([]HistResult, error
var results []HistResult
seenCmds := make(map[string]bool)
addMatches := func(q string, subcmdFilter bool) {
addMatches := func(q string) {
qLow := strings.ToLower(q)
queryFirstWord := ""
querySecondWord := ""
if strings.IndexByte(qLow, ' ') != -1 {
if fields := strings.Fields(qLow); len(fields) > 0 {
queryFirstWord = fields[0]
// find first non-flag token after the command as the subcommand
for _, f := range fields[1:] {
if !strings.HasPrefix(f, "-") {
querySecondWord = f
break
}
}
}
}
// extract pure prefix matches based strictly on recency order (historyCache is newest-first)
// extract pure substring matches (all words present) based strictly on recency order (historyCache is newest-first)
// this ensures that long commands with exact substrings are never truncated by the fuzzy searcher's limit
strictMatches := 0
words := strings.Fields(qLow)
if len(words) == 0 {
words = []string{qLow}
}
for _, cmd := range historyCache {
if seenCmds[cmd] {
continue
}
fields := strings.Fields(cmd)
firstWordLow := ""
if len(fields) > 0 {
firstWordLow = strings.ToLower(fields[0])
cmdLow := strings.ToLower(cmd)
matchAll := true
for _, w := range words {
if !strings.Contains(cmdLow, w) {
matchAll = false
break
}
}
if queryFirstWord != "" {
if firstWordLow != queryFirstWord {
continue
}
if subcmdFilter && querySecondWord != "" {
if len(fields) < 2 {
continue
}
secondWordLow := strings.ToLower(fields[1])
if !strings.HasPrefix(secondWordLow, querySecondWord) {
continue
}
}
} else {
if !strings.HasPrefix(firstWordLow, qLow) {
continue
}
if !matchAll {
continue
}
seenCmds[cmd] = true
@@ -273,33 +254,11 @@ func SearchHistory(query string, aliases map[string]string) ([]HistResult, error
if seenCmds[m.Str] {
continue
}
// filter results by command name match
fields := strings.Fields(m.Str)
firstWord := m.Str
if len(fields) > 0 {
firstWord = fields[0]
}
firstWordLow := strings.ToLower(firstWord)
if queryFirstWord != "" {
if firstWordLow != queryFirstWord {
continue
}
// when query has a non-flag second token, filter by subcommand prefix
if subcmdFilter && querySecondWord != "" {
if len(fields) < 2 {
continue
}
secondWordLow := strings.ToLower(fields[1])
if !strings.HasPrefix(secondWordLow, querySecondWord) {
continue
}
}
} else {
if !strings.HasPrefix(firstWordLow, qLow) {
continue
}
// filter out extremely weak fuzzy matches (e.g. random garbage typing that
// loosely matches across a very long command)
if len(q) > 0 && m.Score/len(q) < 150 {
continue
}
seenCmds[m.Str] = true
@@ -311,18 +270,9 @@ func SearchHistory(query string, aliases map[string]string) ([]HistResult, error
}
}
addMatches(query, true)
addMatches(query)
for _, altQ := range alternativeQueries {
addMatches(altQ, true)
}
// fallback: if subcommand filter produced nothing, retry without it
// so typos like "git chckout" still surface fuzzy matches
if len(results) == 0 {
addMatches(query, false)
for _, altQ := range alternativeQueries {
addMatches(altQ, false)
}
addMatches(altQ)
}
getTier := func(cmd, q string) int {