This pull request modernizes the codebase by adopting Go 1.21+ features, including built-in functions like min and max, standard library iterators such as strings.SplitSeq and strings.FieldsSeq, utility functions like maps.Copy and slices.Contains, and string manipulation helpers like strings.Cut and strings.CutPrefix. Feedback is provided on a test file where the usage of strings.Builder can be further optimized to avoid unnecessary string allocations during concatenation. `go fix -diff ./...`
261 lines
5.4 KiB
Go
261 lines
5.4 KiB
Go
package integration
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/versenilvis/fuzzy"
|
|
"github.com/versenilvis/iris/integration/shell"
|
|
)
|
|
|
|
var (
|
|
historyCache []string
|
|
idMapCache map[string]int
|
|
searcherCache *fuzzy.Searcher
|
|
mu sync.Mutex
|
|
lastModTime int64
|
|
)
|
|
|
|
type HistResult struct {
|
|
ID int
|
|
Cmd string
|
|
FuzzyScore int
|
|
}
|
|
|
|
func init() {
|
|
idMapCache = make(map[string]int)
|
|
}
|
|
|
|
func SearchHistory(query string, aliases map[string]string) ([]HistResult, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
shellName := "bash"
|
|
if shell.Current != nil {
|
|
shellName = shell.Current.GetName()
|
|
}
|
|
|
|
var histFile string
|
|
switch shellName {
|
|
case "zsh":
|
|
histFile = filepath.Join(home, ".zsh_history")
|
|
case "fish":
|
|
histFile = filepath.Join(home, ".local", "share", "fish", "fish_history")
|
|
default:
|
|
histFile = filepath.Join(home, ".bash_history")
|
|
}
|
|
|
|
if info, err := os.Stat(histFile); err == nil {
|
|
if info.ModTime().UnixNano() > lastModTime {
|
|
historyCache = nil // force reload
|
|
idMapCache = make(map[string]int)
|
|
lastModTime = info.ModTime().UnixNano()
|
|
}
|
|
}
|
|
|
|
// lazy load history if cache is empty
|
|
if len(historyCache) == 0 {
|
|
file, err := os.Open(histFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
var allCmds []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
cmd := line
|
|
|
|
if shellName == "zsh" {
|
|
parts := strings.SplitN(line, ";", 2)
|
|
if len(parts) == 2 {
|
|
cmd = parts[1]
|
|
}
|
|
} else if shellName == "bash" {
|
|
if strings.HasPrefix(line, "#") && len(line) > 1 {
|
|
isTimestamp := true
|
|
for _, c := range line[1:] {
|
|
if c < '0' || c > '9' {
|
|
isTimestamp = false
|
|
break
|
|
}
|
|
}
|
|
if isTimestamp {
|
|
continue
|
|
}
|
|
}
|
|
} else if shellName == "fish" {
|
|
if after, ok := strings.CutPrefix(line, "- cmd: "); ok {
|
|
cmd = after
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
cmd = strings.TrimSpace(cmd)
|
|
if cmd != "" {
|
|
allCmds = append(allCmds, cmd)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// build historyCache backwards so newest commands come first
|
|
seen := make(map[string]bool)
|
|
for i := len(allCmds) - 1; i >= 0; i-- {
|
|
cmd := allCmds[i]
|
|
if !seen[cmd] {
|
|
historyCache = append(historyCache, cmd)
|
|
seen[cmd] = true
|
|
// we assign the ID as the original line number (1-indexed based on allCmds length)
|
|
idMapCache[cmd] = i + 1
|
|
}
|
|
}
|
|
|
|
searcherCache = fuzzy.NewPlainSearcher(historyCache)
|
|
}
|
|
|
|
if query == "" {
|
|
var results []HistResult
|
|
limit := min(len(historyCache), 100)
|
|
|
|
for i := 0; i < limit; i++ {
|
|
cmd := historyCache[i]
|
|
results = append(results, HistResult{
|
|
ID: idMapCache[cmd],
|
|
Cmd: cmd,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
var alternativeQueries []string
|
|
for name, target := range aliases {
|
|
if target != "" {
|
|
qLow := strings.ToLower(query)
|
|
tLow := strings.ToLower(target)
|
|
nLow := strings.ToLower(name)
|
|
|
|
if qLow == tLow {
|
|
alternativeQueries = append(alternativeQueries, name)
|
|
} else if strings.HasPrefix(qLow, tLow+" ") {
|
|
suffix := query[len(target):]
|
|
alternativeQueries = append(alternativeQueries, name+suffix)
|
|
}
|
|
|
|
if qLow == nLow {
|
|
alternativeQueries = append(alternativeQueries, target)
|
|
} else if strings.HasPrefix(qLow, nLow+" ") {
|
|
suffix := query[len(name):]
|
|
alternativeQueries = append(alternativeQueries, target+suffix)
|
|
}
|
|
}
|
|
}
|
|
|
|
var results []HistResult
|
|
seenCmds := make(map[string]bool)
|
|
|
|
addMatches := func(q string) {
|
|
qLow := strings.ToLower(q)
|
|
queryFirstWord := ""
|
|
if strings.IndexByte(qLow, ' ') != -1 {
|
|
if fields := strings.Fields(qLow); len(fields) > 0 {
|
|
queryFirstWord = fields[0]
|
|
}
|
|
}
|
|
|
|
matches := searcherCache.SearchWithScores(q, &fuzzy.SearchOptions{Limit: 200})
|
|
for _, m := range matches {
|
|
if seenCmds[m.Str] {
|
|
continue
|
|
}
|
|
|
|
// filter results by command name match
|
|
firstWord := m.Str
|
|
if idx := strings.IndexByte(m.Str, ' '); idx != -1 {
|
|
firstWord = m.Str[:idx]
|
|
}
|
|
firstWordLow := strings.ToLower(firstWord)
|
|
|
|
if queryFirstWord != "" {
|
|
if firstWordLow != queryFirstWord {
|
|
continue
|
|
}
|
|
} else {
|
|
if !strings.HasPrefix(firstWordLow, qLow) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
seenCmds[m.Str] = true
|
|
results = append(results, HistResult{
|
|
ID: idMapCache[m.Str],
|
|
Cmd: m.Str,
|
|
FuzzyScore: m.Score,
|
|
})
|
|
}
|
|
}
|
|
|
|
addMatches(query)
|
|
|
|
for _, altQ := range alternativeQueries {
|
|
addMatches(altQ)
|
|
}
|
|
|
|
getTier := func(cmd, q string) int {
|
|
bestTier := 4
|
|
check := func(ql string) {
|
|
cmdLow := strings.ToLower(cmd)
|
|
qlLow := strings.ToLower(ql)
|
|
tier := 4
|
|
if cmdLow == qlLow {
|
|
tier = 1
|
|
} else if strings.HasPrefix(cmdLow, qlLow) {
|
|
tier = 2
|
|
} else if strings.Contains(cmdLow, qlLow) {
|
|
tier = 3
|
|
}
|
|
if tier < bestTier {
|
|
bestTier = tier
|
|
}
|
|
}
|
|
check(q)
|
|
for _, altQ := range alternativeQueries {
|
|
check(altQ)
|
|
}
|
|
return bestTier
|
|
}
|
|
|
|
tiers := make([]int, len(results))
|
|
for i, r := range results {
|
|
tiers[i] = getTier(r.Cmd, query)
|
|
}
|
|
|
|
sort.SliceStable(results, func(i, j int) bool {
|
|
tI := tiers[i]
|
|
tJ := tiers[j]
|
|
if tI != tJ {
|
|
return tI < tJ
|
|
}
|
|
|
|
if tI == 4 && results[i].FuzzyScore != results[j].FuzzyScore {
|
|
return results[i].FuzzyScore > results[j].FuzzyScore
|
|
}
|
|
|
|
return results[i].ID > results[j].ID
|
|
})
|
|
|
|
return results, nil
|
|
}
|