mirror of
https://github.com/wavetermdev/inshellisense.git
synced 2026-08-05 13:43:30 -07:00
feat: bug fixes & add support for recommendations on command arg (w/ duplicate recommendation after acceptance)
Signed-off-by: cpendery <cpendery@vt.edu>
This commit is contained in:
@@ -7,4 +7,8 @@ clean:
|
||||
cp autocomplete/specs/specs_.go autocomplete/
|
||||
rm -r autocomplete/specs
|
||||
mkdir autocomplete/specs
|
||||
mv autocomplete/specs_.go autocomplete/specs/
|
||||
mv autocomplete/specs_.go autocomplete/specs/
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -o clac main.go
|
||||
+70
-47
@@ -1,6 +1,7 @@
|
||||
package autocomplete
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
|
||||
@@ -8,11 +9,6 @@ import (
|
||||
"github.com/cpendery/clac/autocomplete/specs"
|
||||
)
|
||||
|
||||
type Suggestion struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
var (
|
||||
cmdDelimiter = regexp.MustCompile(`(\|\|)|(&&)|(;)`)
|
||||
lastSuggestionCmd = ""
|
||||
@@ -20,6 +16,11 @@ var (
|
||||
lastCmdRunes = 0
|
||||
)
|
||||
|
||||
type Suggestion struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
func getOption(token string, options []model.Option) *model.Option {
|
||||
for _, option := range options {
|
||||
for _, optionName := range option.Name {
|
||||
@@ -73,48 +74,66 @@ func getShortName(names []string) string {
|
||||
return shortestName
|
||||
}
|
||||
|
||||
func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions []model.Option, partialCmd *commandToken) []Suggestion {
|
||||
suggestions := []Suggestion{}
|
||||
func getSubcommandDrivenRecommendation(spec model.Subcommand, persistentOptions []model.Option, partialCmd *commandToken, onlyRecommendSubcommands bool) []model.TermSuggestion {
|
||||
log.Println("sub rec")
|
||||
suggestions := []model.TermSuggestion{}
|
||||
allOptions := append(spec.Options, persistentOptions...)
|
||||
|
||||
if onlyRecommendSubcommands {
|
||||
getSubcommandDrivenRecommendations(spec, &suggestions)
|
||||
} else {
|
||||
if len(spec.Args) != 0 {
|
||||
activeArg := spec.Args[0]
|
||||
getSuggestionDrivenRecommendations(activeArg.Suggestions, &suggestions)
|
||||
getTemplateDrivenRecommendations(activeArg.Templates, &suggestions)
|
||||
}
|
||||
getSubcommandDrivenRecommendations(spec, &suggestions)
|
||||
getOptionDrivenRecommendations(allOptions, &suggestions)
|
||||
}
|
||||
|
||||
if partialCmd != nil {
|
||||
switch spec.FilterStrategy {
|
||||
case model.FilterStrategyFuzzy:
|
||||
return append(
|
||||
fuzzyMatchSubcommands(partialCmd.token, spec.Subcommands),
|
||||
fuzzyMatchOptions(partialCmd.token, allOptions)...,
|
||||
)
|
||||
getFuzzyFilteredRecommendations(partialCmd.token, &suggestions)
|
||||
case model.FilterStrategyPrefix, model.FilterStrategyEmpty:
|
||||
return append(
|
||||
prefixMatchSubcommands(partialCmd.token, spec.Subcommands),
|
||||
prefixMatchOptions(partialCmd.token, allOptions)...,
|
||||
)
|
||||
getPrefixFilteredRecommendations(partialCmd.token, &suggestions)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sub := range spec.Subcommands {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Name: getLongName(sub.Name),
|
||||
Description: sub.Description,
|
||||
})
|
||||
}
|
||||
for _, op := range allOptions {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Name: getShortName(op.Name),
|
||||
Description: op.Description,
|
||||
})
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func getArgDrivenRecommendation(args []model.Arg, spec model.Subcommand, persistentOptions []model.Option) []Suggestion {
|
||||
return []Suggestion{}
|
||||
func getArgDrivenRecommendation(args []model.Arg, spec model.Subcommand, persistentOptions []model.Option, partialCmd *commandToken) []model.TermSuggestion {
|
||||
log.Println("arg rec")
|
||||
suggestions := []model.TermSuggestion{}
|
||||
activeArg := args[0]
|
||||
allOptions := append(spec.Options, persistentOptions...)
|
||||
|
||||
getSuggestionDrivenRecommendations(activeArg.Suggestions, &suggestions)
|
||||
getTemplateDrivenRecommendations(activeArg.Templates, &suggestions)
|
||||
|
||||
if activeArg.IsOptional {
|
||||
getSubcommandDrivenRecommendations(spec, &suggestions)
|
||||
getOptionDrivenRecommendations(allOptions, &suggestions)
|
||||
}
|
||||
|
||||
if partialCmd != nil {
|
||||
switch spec.FilterStrategy {
|
||||
case model.FilterStrategyFuzzy:
|
||||
getFuzzyFilteredRecommendations(partialCmd.token, &suggestions)
|
||||
case model.FilterStrategyPrefix, model.FilterStrategyEmpty:
|
||||
getPrefixFilteredRecommendations(partialCmd.token, &suggestions)
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func handleSubcommand(tokens []commandToken, spec model.Subcommand, persistentOptions []model.Option) (suggestions []Suggestion) {
|
||||
func handleSubcommand(tokens []commandToken, spec model.Subcommand, persistentOptions []model.Option, argsDepleted bool) (suggestions []model.TermSuggestion) {
|
||||
if len(tokens) == 0 {
|
||||
return getSubcommandDrivenRecommendation(spec, persistentOptions, nil)
|
||||
return getSubcommandDrivenRecommendation(spec, persistentOptions, nil, argsDepleted)
|
||||
} else if !tokens[0].complete {
|
||||
return getSubcommandDrivenRecommendation(spec, persistentOptions, &tokens[0])
|
||||
return getSubcommandDrivenRecommendation(spec, persistentOptions, &tokens[0], argsDepleted)
|
||||
}
|
||||
for _, option := range spec.Options {
|
||||
if option.IsPersistent {
|
||||
@@ -129,30 +148,30 @@ func handleSubcommand(tokens []commandToken, spec model.Subcommand, persistentOp
|
||||
return
|
||||
}
|
||||
if subcommand := getSubcommand(activeCmd.token, spec); subcommand != nil {
|
||||
return handleSubcommand(tokens[1:], *subcommand, persistentOptions)
|
||||
return handleSubcommand(tokens[1:], *subcommand, persistentOptions, false)
|
||||
}
|
||||
|
||||
return handleArg(tokens, spec.Args, spec, persistentOptions)
|
||||
}
|
||||
|
||||
func handleOption(tokens []commandToken, option model.Option, spec model.Subcommand, persistentOptions []model.Option) (suggestions []Suggestion) {
|
||||
func handleOption(tokens []commandToken, option model.Option, spec model.Subcommand, persistentOptions []model.Option) (suggestions []model.TermSuggestion) {
|
||||
if len(tokens) == 0 {
|
||||
slog.Error("invalid state reached, option with no tokens")
|
||||
return
|
||||
}
|
||||
if len(option.Args) == 0 {
|
||||
return handleSubcommand(tokens[1:], spec, persistentOptions)
|
||||
return handleSubcommand(tokens[1:], spec, persistentOptions, false)
|
||||
}
|
||||
return handleArg(tokens[1:], option.Args, spec, persistentOptions)
|
||||
}
|
||||
|
||||
func handleArg(tokens []commandToken, args []model.Arg, spec model.Subcommand, persistentOptions []model.Option) (suggestions []Suggestion) {
|
||||
if len(tokens) == 0 {
|
||||
return getArgDrivenRecommendation(args, spec, persistentOptions)
|
||||
func handleArg(tokens []commandToken, args []model.Arg, spec model.Subcommand, persistentOptions []model.Option) (suggestions []model.TermSuggestion) {
|
||||
if len(args) == 0 {
|
||||
return handleSubcommand(tokens, spec, persistentOptions, true)
|
||||
} else if len(tokens) == 0 {
|
||||
return getArgDrivenRecommendation(args, spec, persistentOptions, nil)
|
||||
} else if !tokens[0].complete {
|
||||
return getArgDrivenRecommendation(args, spec, persistentOptions)
|
||||
} else if len(args) == 0 {
|
||||
return handleSubcommand(tokens, spec, persistentOptions)
|
||||
return getArgDrivenRecommendation(args, spec, persistentOptions, &tokens[0])
|
||||
}
|
||||
|
||||
activeCmd := tokens[0]
|
||||
@@ -165,27 +184,27 @@ func handleArg(tokens []commandToken, args []model.Arg, spec model.Subcommand, p
|
||||
}
|
||||
subcommand := getSubcommand(activeCmd.token, spec)
|
||||
if subcommand != nil {
|
||||
return handleSubcommand(tokens[1:], *subcommand, persistentOptions)
|
||||
return handleSubcommand(tokens[1:], *subcommand, persistentOptions, false)
|
||||
}
|
||||
}
|
||||
|
||||
activeArg := args[0]
|
||||
if activeArg.IsVariadic {
|
||||
return []Suggestion{{Name: activeArg.Name}}
|
||||
return handleArg(tokens[1:], args, spec, persistentOptions)
|
||||
} else if activeArg.IsCommand {
|
||||
if len(tokens) <= 1 {
|
||||
return
|
||||
}
|
||||
activeCmd = tokens[1]
|
||||
if subcommand := getSubcommand(activeCmd.token, spec); subcommand != nil {
|
||||
return handleSubcommand(tokens[2:], *subcommand, persistentOptions)
|
||||
return handleSubcommand(tokens[2:], *subcommand, persistentOptions, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
return handleArg(tokens[1:], args[1:], spec, persistentOptions)
|
||||
}
|
||||
|
||||
func loadSuggestions(cmd string) (suggestions []Suggestion, charsInLastCmd int) {
|
||||
func loadSuggestions(cmd string) (suggestions []model.TermSuggestion, charsInLastCmd int) {
|
||||
activeCmd := ParseCommand(cmd)
|
||||
if len(activeCmd) <= 0 {
|
||||
return
|
||||
@@ -200,7 +219,7 @@ func loadSuggestions(cmd string) (suggestions []Suggestion, charsInLastCmd int)
|
||||
charsInLastCmd = 0
|
||||
}
|
||||
if spec, ok := specs.Specs[rootToken.token]; ok {
|
||||
return handleSubcommand(activeCmd[1:], spec, []model.Option{}), charsInLastCmd
|
||||
return handleSubcommand(activeCmd[1:], spec, []model.Option{}, false), charsInLastCmd
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -209,7 +228,11 @@ func LoadSuggestions(cmd string) ([]Suggestion, int) {
|
||||
if cmd == lastSuggestionCmd {
|
||||
return lastSuggestion, lastCmdRunes
|
||||
}
|
||||
suggestions, lastRunes := loadSuggestions(cmd)
|
||||
termSuggestions, lastRunes := loadSuggestions(cmd)
|
||||
suggestions := []Suggestion{}
|
||||
for _, suggestion := range termSuggestions {
|
||||
suggestions = append(suggestions, Suggestion{Name: suggestion.Name, Description: suggestion.Description})
|
||||
}
|
||||
lastSuggestionCmd, lastSuggestion, lastCmdRunes = cmd, suggestions, lastRunes
|
||||
return suggestions, lastRunes
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package generators
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/cpendery/clac/autocomplete/model"
|
||||
)
|
||||
|
||||
// * - filepaths: show folders and filepaths. Allow autoexecute on filepaths
|
||||
// * - folders: show folders only. Allow autoexecute on folders
|
||||
// * - history: show suggestions for all items in history matching this pattern
|
||||
// * - help: show subcommands. Only includes the 'siblings' of the nearest 'parent' subcommand
|
||||
|
||||
const (
|
||||
directory = "Directory"
|
||||
file = "File"
|
||||
)
|
||||
|
||||
func walk(includeFiles bool) []model.TermSuggestion {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
slog.Error("unable to get working dir", slog.String("error", err.Error()))
|
||||
return []model.TermSuggestion{}
|
||||
}
|
||||
dirItems, err := os.ReadDir(wd)
|
||||
if err != nil {
|
||||
slog.Error("unable to read files from working dir", slog.String("error", err.Error()))
|
||||
return []model.TermSuggestion{}
|
||||
}
|
||||
suggestions := []model.TermSuggestion{}
|
||||
for _, dirItem := range dirItems {
|
||||
if !dirItem.IsDir() && !includeFiles {
|
||||
continue
|
||||
}
|
||||
description := file
|
||||
if dirItem.IsDir() {
|
||||
description = directory
|
||||
}
|
||||
suggestions = append(suggestions, model.TermSuggestion{
|
||||
Name: dirItem.Name(),
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func Filepaths() []model.TermSuggestion {
|
||||
return walk(true)
|
||||
}
|
||||
|
||||
func Folders() []model.TermSuggestion {
|
||||
return walk(false)
|
||||
}
|
||||
|
||||
// TODO: implement history template
|
||||
func History() []model.TermSuggestion {
|
||||
return []model.TermSuggestion{}
|
||||
}
|
||||
|
||||
// TODO: implement help template
|
||||
func Help() []model.TermSuggestion {
|
||||
return []model.TermSuggestion{}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package autocomplete
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cpendery/clac/autocomplete/model"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
)
|
||||
|
||||
type matchable interface {
|
||||
GetName() []string
|
||||
GetDescription() string
|
||||
}
|
||||
|
||||
func fuzzyMatch[M matchable](input string, targets []M) []Suggestion {
|
||||
type match struct {
|
||||
name string
|
||||
rank int
|
||||
item M
|
||||
}
|
||||
matchers := []match{}
|
||||
for _, item := range targets {
|
||||
bestName := ""
|
||||
bestNameRank := -1
|
||||
for _, n := range item.GetName() {
|
||||
rank := fuzzy.RankMatch(input, n)
|
||||
if rank > bestNameRank {
|
||||
bestName = n
|
||||
bestNameRank = rank
|
||||
}
|
||||
}
|
||||
if bestNameRank != -1 {
|
||||
matchers = append(matchers, match{
|
||||
name: bestName,
|
||||
item: item,
|
||||
rank: bestNameRank,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(matchers, func(i, j int) bool {
|
||||
return matchers[i].rank > matchers[j].rank
|
||||
})
|
||||
results := []Suggestion{}
|
||||
for _, m := range matchers {
|
||||
results = append(results, Suggestion{
|
||||
Name: m.name,
|
||||
Description: m.item.GetDescription(),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func fuzzyMatchSubcommands(input string, subcommands []model.Subcommand) []Suggestion {
|
||||
return fuzzyMatch[model.Subcommand](input, subcommands)
|
||||
}
|
||||
|
||||
func fuzzyMatchOptions(input string, options []model.Option) []Suggestion {
|
||||
return fuzzyMatch[model.Option](input, options)
|
||||
}
|
||||
|
||||
func prefixMatch[M matchable](input string, subcommands []M) []Suggestion {
|
||||
results := []Suggestion{}
|
||||
for _, sub := range subcommands {
|
||||
for _, n := range sub.GetName() {
|
||||
if strings.HasPrefix(n, input) {
|
||||
results = append(results, Suggestion{
|
||||
Name: n,
|
||||
Description: sub.GetDescription(),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func prefixMatchSubcommands(input string, subcommands []model.Subcommand) []Suggestion {
|
||||
return prefixMatch[model.Subcommand](input, subcommands)
|
||||
}
|
||||
|
||||
func prefixMatchOptions(input string, options []model.Option) []Suggestion {
|
||||
return prefixMatch[model.Option](input, options)
|
||||
}
|
||||
@@ -50,6 +50,19 @@ type Suggestion struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
type TermSuggestion struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (t TermSuggestion) GetName() []string {
|
||||
return []string{t.Name}
|
||||
}
|
||||
|
||||
func (t TermSuggestion) GetDescription() string {
|
||||
return t.Description
|
||||
}
|
||||
|
||||
type Generator struct {
|
||||
Script string
|
||||
Function func() string
|
||||
@@ -75,5 +88,5 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
Templates = []Template{TemplateFilepaths, TemplateFolders, TemplateHistory}
|
||||
Templates = []Template{TemplateFilepaths, TemplateFolders, TemplateHistory, TemplateHelp}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package autocomplete
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cpendery/clac/autocomplete/generators"
|
||||
"github.com/cpendery/clac/autocomplete/model"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
)
|
||||
|
||||
type matchable interface {
|
||||
GetName() []string
|
||||
GetDescription() string
|
||||
}
|
||||
|
||||
func fuzzyMatch[M matchable](input string, targets []M, suggestions *[]model.TermSuggestion) {
|
||||
type match struct {
|
||||
name string
|
||||
rank int
|
||||
item M
|
||||
}
|
||||
matchers := []match{}
|
||||
for _, item := range targets {
|
||||
bestName := ""
|
||||
bestNameRank := -1
|
||||
for _, n := range item.GetName() {
|
||||
rank := fuzzy.RankMatch(input, n)
|
||||
if rank > bestNameRank {
|
||||
bestName = n
|
||||
bestNameRank = rank
|
||||
}
|
||||
}
|
||||
if bestNameRank != -1 {
|
||||
matchers = append(matchers, match{
|
||||
name: bestName,
|
||||
item: item,
|
||||
rank: bestNameRank,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(matchers, func(i, j int) bool {
|
||||
return matchers[i].rank > matchers[j].rank
|
||||
})
|
||||
for _, m := range matchers {
|
||||
*suggestions = append(*suggestions, model.TermSuggestion{
|
||||
Name: m.name,
|
||||
Description: m.item.GetDescription(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getFuzzyFilteredRecommendations(input string, suggestions *[]model.TermSuggestion) {
|
||||
results := []model.TermSuggestion{}
|
||||
fuzzyMatch[model.TermSuggestion](input, *suggestions, &results)
|
||||
*suggestions = results
|
||||
}
|
||||
|
||||
func prefixMatch[M matchable](input string, subcommands []M, suggestions *[]model.TermSuggestion) {
|
||||
for _, sub := range subcommands {
|
||||
for _, n := range sub.GetName() {
|
||||
if strings.HasPrefix(n, input) {
|
||||
*suggestions = append(*suggestions, model.TermSuggestion{
|
||||
Name: n,
|
||||
Description: sub.GetDescription(),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPrefixFilteredRecommendations(input string, suggestions *[]model.TermSuggestion) {
|
||||
results := []model.TermSuggestion{}
|
||||
prefixMatch[model.TermSuggestion](input, *suggestions, &results)
|
||||
*suggestions = results
|
||||
}
|
||||
|
||||
func getTemplateDrivenRecommendations(templates []model.Template, suggestions *[]model.TermSuggestion) {
|
||||
for _, template := range templates {
|
||||
switch template {
|
||||
case model.TemplateFilepaths:
|
||||
*suggestions = append(*suggestions, generators.Filepaths()...)
|
||||
case model.TemplateFolders:
|
||||
if slices.Contains(templates, model.TemplateFilepaths) {
|
||||
continue
|
||||
}
|
||||
*suggestions = append(*suggestions, generators.Folders()...)
|
||||
case model.TemplateHelp:
|
||||
*suggestions = append(*suggestions, generators.Help()...)
|
||||
case model.TemplateHistory:
|
||||
*suggestions = append(*suggestions, generators.History()...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSuggestionDrivenRecommendations(suggestionSet []model.Suggestion, suggestions *[]model.TermSuggestion) {
|
||||
for _, suggestion := range suggestionSet {
|
||||
*suggestions = append(*suggestions, model.TermSuggestion{
|
||||
Name: getLongName(suggestion.Name),
|
||||
Description: suggestion.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getSubcommandDrivenRecommendations(spec model.Subcommand, suggestions *[]model.TermSuggestion) {
|
||||
for _, sub := range spec.Subcommands {
|
||||
*suggestions = append(*suggestions, model.TermSuggestion{
|
||||
Name: getLongName(sub.Name),
|
||||
Description: sub.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getOptionDrivenRecommendations(options []model.Option, suggestions *[]model.TermSuggestion) {
|
||||
for _, op := range options {
|
||||
*suggestions = append(*suggestions, model.TermSuggestion{
|
||||
Name: getShortName(op.Name),
|
||||
Description: op.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,10 @@ func (m Model) ActiveSuggestion() (string, int) {
|
||||
return m.suggestions[m.cursor].Name, m.runesToRemove
|
||||
}
|
||||
|
||||
func (m *Model) ResetCursor() {
|
||||
m.cursor = 0
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg, command string, userInputCursorLocation int) Model {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
|
||||
Reference in New Issue
Block a user