Files
iris-context/spec/lookup.go
T
VERSE ebde9ba68f refactor(commands): separate commands and its core into two folders (#32)
- I think it would be better if one for commands only, one for the spec
handling core logic so people won't be confused by the core logic inside
the commands folder (cleaner I guess)
- Regen docs
2026-07-05 11:27:30 +07:00

319 lines
7.6 KiB
Go

package spec
import (
"maps"
"slices"
"strings"
"sync"
"github.com/versenilvis/iris/integration/shell"
"github.com/versenilvis/iris/logger"
)
var (
ShellAliases = map[string]string{}
shellAliasesMu sync.RWMutex
)
func GetAlias(name string) (string, bool) {
shellAliasesMu.RLock()
defer shellAliasesMu.RUnlock()
val, ok := ShellAliases[name]
return val, ok
}
func GetAliasesCopy() map[string]string {
shellAliasesMu.RLock()
defer shellAliasesMu.RUnlock()
cp := make(map[string]string, len(ShellAliases))
maps.Copy(cp, ShellAliases)
return cp
}
// Lookup finds matching suggestions for your input by looking at how many words you typed
// it changes aliases to real commands, finds subcommands inside others, and runs generators for more suggestions
// e.g. Lookup("git che") -> suggests "git checkout"
// e.g. Lookup("git checkout ") -> suggests branch names via generator
func Lookup(input string) []Suggestion {
if shell.Current != nil {
aliases := shell.Current.ScanAliases()
shellAliasesMu.Lock()
ShellAliases = aliases
shellAliasesMu.Unlock()
}
shellAliasesMu.RLock()
aliases := ShellAliases
shellAliasesMu.RUnlock()
if input == "" {
return topLevelSuggestions("", aliases)
}
tokens := Tokenize(input)
if len(tokens) == 1 && tokens[0] == "" {
return topLevelSuggestions("", aliases)
}
// NOTE: remember that wrapper already check if we type nothing, but I just want to make sure
// in the future, maybe we will write unit test or using Lookup in another module
// it will be safer because we maybe forget to check it in that module
scanExternalCommands()
// if you have an alias in your shell config like: alias gca="git commit -a"
// if the first word match it, IRIS will suggest "git commit -a"
if len(tokens) > 1 {
if target, ok := aliases[tokens[0]]; ok {
aliasTokens := Tokenize(target)
if len(aliasTokens) > 0 && aliasTokens[len(aliasTokens)-1] == "" {
aliasTokens = aliasTokens[:len(aliasTokens)-1]
}
tokens = append(aliasTokens, tokens[1:]...)
}
}
if len(tokens) == 1 {
query := tokens[0]
results := topLevelSuggestions(query, aliases)
if spec, exists := Registry[query]; exists {
hasTrailingSpace := query != "" && query[len(query)-1] == ' '
if hasTrailingSpace {
partial := ""
prefix := query
for _, sub := range spec.Subcommands {
results = append(results, Suggestion{
Cmd: strings.TrimSpace(query) + " " + sub.Name, Desc: sub.Description, Icon: query,
})
}
if spec.Generator != nil {
genResults := spec.Generator(tokens, prefix, partial)
for _, g := range genResults {
results = append(results, Suggestion{
Cmd: strings.TrimSpace(query) + " " + g.Cmd, Desc: g.Desc, Icon: query,
})
}
}
}
}
return results
}
rootCmdName := tokens[0]
spec, exists := Registry[rootCmdName]
logger.Debugf("core lookup tokens: %v, registry exists: %v", tokens, exists)
if !exists {
return nil
}
currentSubs, currentOpts, currentGen := spec.Subcommands, spec.Options, spec.Generator
depth := 1
for depth < len(tokens)-1 {
tok := tokens[depth]
if tok == "" || strings.HasPrefix(tok, "-") || strings.Contains(tok, "=") {
depth++
continue
}
found := false
for _, sub := range currentSubs {
match := sub.Name == tok
if !match {
if slices.Contains(sub.Aliases, tok) {
match = true
}
}
if match {
currentSubs, currentOpts, currentGen = sub.Subcommands, sub.Options, sub.Generator
found = true
break
}
}
if found {
depth++
continue
}
// invalid subcommand word typed
if len(currentSubs) > 0 {
return nil
}
break
}
results := []Suggestion{}
currentLimit := spec.MaxArgs
tempSubs := spec.Subcommands
for i := 1; i < depth; i++ {
tok := tokens[i]
for _, sub := range tempSubs {
if sub.Name == tok {
currentLimit = sub.MaxArgs
tempSubs = sub.Subcommands
break
}
}
}
argCount := 0
for i := depth; i < len(tokens)-1; i++ {
t := tokens[i]
if t != "" && !strings.HasPrefix(t, "-") && !strings.Contains(t, "=") {
argCount++
}
}
partial := tokens[len(tokens)-1]
allowMoreArgs := currentLimit <= 0 || argCount < currentLimit
logger.Debugf("core query tokens: %v (partial: '%s')", tokens, partial)
logger.Debugf("core depth: %d, argCount: %d, limit: %d, allowMore: %v", depth, argCount, currentLimit, allowMoreArgs)
prefixBuilder := strings.Builder{}
for i := 0; i < depth; i++ {
if i > 0 {
prefixBuilder.WriteByte(' ')
}
prefixBuilder.WriteString(tokens[i])
}
prefix := prefixBuilder.String()
linePrefixBuilder := strings.Builder{}
for i := 0; i < len(tokens)-1; i++ {
if i > 0 {
linePrefixBuilder.WriteByte(' ')
}
linePrefixBuilder.WriteString(tokens[i])
}
linePrefix := linePrefixBuilder.String()
if currentGen != nil && allowMoreArgs {
genResults := currentGen(tokens, prefix, partial)
for _, g := range genResults {
if partial != "" && !HasPrefix(g.Cmd, partial) && !strings.Contains(g.Cmd, partial) {
continue
}
suggested := g.Cmd
if strings.Contains(suggested, " ") && !strings.HasPrefix(suggested, "\"") {
suggested = "\"" + suggested + "\""
}
// if the suggestion is a full path that includes
// words already in the command line (multi-word support), we replace
// the entire argument part by using prefix only
finalCmd := ""
if len(tokens) > depth+1 && strings.HasPrefix(g.Cmd, tokens[depth]) {
finalCmd = prefix + " " + suggested
} else {
finalCmd = strings.TrimSpace(linePrefix) + " " + suggested
}
newTokens := Tokenize(finalCmd)
if len(newTokens) > 0 {
lastToken := newTokens[len(newTokens)-1]
isDuplicate := false
for i := 0; i < len(tokens)-1; i++ {
if tokens[i] == lastToken {
isDuplicate = true
break
}
}
if isDuplicate {
continue
}
}
results = append(results, Suggestion{
Cmd: finalCmd,
Desc: g.Desc,
Icon: rootCmdName,
})
}
}
if allowMoreArgs {
for _, sub := range currentSubs {
if partial == "" || HasPrefix(sub.Name, partial) {
results = append(results, Suggestion{
Cmd: prefix + " " + sub.Name, Desc: sub.Description, Icon: rootCmdName,
})
}
}
}
if len(partial) > 0 && partial[0] == '-' {
usedOpts := make(map[string]bool)
for _, t := range tokens {
if strings.HasPrefix(t, "-") {
usedOpts[t] = true
}
}
for _, opt := range currentOpts {
if !usedOpts[opt.Name] && (partial == "" || HasPrefix(opt.Name, partial)) {
results = append(results, Suggestion{
Cmd: linePrefix + " " + opt.Name, Desc: opt.Description, Icon: rootCmdName,
})
}
}
}
return results
}
func topLevelSuggestions(query string, aliases map[string]string) []Suggestion {
scanExternalCommands()
results, seen := []Suggestion{}, make(map[string]bool)
for name, target := range aliases {
if !seen[name] && (query == "" || HasPrefix(name, query)) {
results = append(results, Suggestion{
Cmd: name, Desc: target, Icon: "alias",
})
seen[name] = true
}
}
for name, spec := range Registry {
if seen[name] {
continue
}
match := false
if query == "" || HasPrefix(name, query) {
match = true
} else {
for _, a := range spec.Aliases {
if HasPrefix(a, query) {
match = true
break
}
}
}
if match {
icon := spec.Icon
if icon == "" {
icon = name
}
results = append(results, Suggestion{Cmd: name, Desc: spec.Description, Icon: icon})
seen[name] = true
}
}
for name := range pathCmds {
if !seen[name] && (query == "" || HasPrefix(name, query)) {
results = append(results, Suggestion{
Cmd: name, Desc: "system command", Icon: "system",
})
}
}
return results
}