Files
iris-context/integration/history.go
T
VERSE ffa336196a chore: use go fix to enhance codebase (#29)
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 ./...`
2026-07-04 19:44:38 +07:00

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
}