Hilbish/readline/completers/tab-completer.go

290 lines
9.6 KiB
Go

package completers
import (
"errors"
"fmt"
"strings"
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
)
// CommandCompleter - A completer using a github.com/jessevdk/go-flags Command Parser, in order
// to build completions for commands, arguments, options and their arguments as well.
// This completer needs to be instantiated with its constructor, in order to ensure the parser is not nil.
type CommandCompleter struct {
parser *flags.Parser
}
// NewCommandCompleter - Instantiate a new tab completer using a github.com/jessevdk/go-flags Command Parser.
func NewCommandCompleter(parser *flags.Parser) (completer *CommandCompleter, err error) {
if parser == nil {
return nil, errors.New("command completer was instantiated with a nil parser")
}
return &CommandCompleter{parser: parser}, nil
}
// TabCompleter - A default tab completer working with a github.com/jessevdk/go-flags parser.
func (c *CommandCompleter) TabCompleter(line []rune, pos int, dtc readline.DelayedTabContext) (lastWord string, completions []*readline.CompletionGroup) {
// Format and sanitize input
// @args => All items of the input line
// @last => The last word detected in input line as []rune
// @lastWord => The last word detected in input as string
args, last, lastWord := formatInput(line)
// Detect base command automatically
var command = c.detectedCommand(args)
// Propose commands
if noCommandOrEmpty(args, last, command) {
return c.completeMenuCommands(lastWord, pos)
}
// Check environment variables
if envVarAsked(args, lastWord) {
completeEnvironmentVariables(lastWord)
}
// Base command has been identified
if commandFound(command) {
// Check environment variables again
if envVarAsked(args, lastWord) {
return completeEnvironmentVariables(lastWord)
}
// If options are asked for root command, return commpletions.
if len(command.Groups()) > 0 {
for _, grp := range command.Groups() {
if opt, yes := optionArgRequired(args, last, grp); yes {
return completeOptionArguments(command, opt, lastWord)
}
}
}
// Then propose subcommands. We don't return from here, otherwise it always skips the next steps.
if hasSubCommands(command, args) {
completions = completeSubCommands(args, lastWord, command)
}
// Handle subcommand if found (maybe we should rewrite this function and use it also for base command)
if sub, ok := subCommandFound(lastWord, args, command); ok {
return handleSubCommand(line, pos, sub)
}
// If user asks for completions with "-" / "--", show command options.
// We ask this here, after having ensured there is no subcommand invoked.
// This prevails over command arguments, even if they are required.
if commandOptionsAsked(args, lastWord, command) {
return completeCommandOptions(args, lastWord, command)
}
// Propose argument completion before anything, and if needed
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
return completeCommandArguments(command, arg, lastWord)
}
}
return
}
// [ Main Completion Functions ] -----------------------------------------------------------------------------------------------------------------
// completeMenuCommands - Selects all commands available in a given context and returns them as suggestions
// Many categories, all from command parsers.
func (c *CommandCompleter) completeMenuCommands(lastWord string, pos int) (prefix string, completions []*readline.CompletionGroup) {
prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.
// Check their namespace (which should be their "group" (like utils, core, Jobs, etc))
for _, cmd := range c.parser.Commands() {
// If command matches readline input
if strings.HasPrefix(cmd.Name, lastWord) {
// Check command group: add to existing group if found
var found bool
for _, grp := range completions {
if grp.Name == cmd.Aliases[0] {
found = true
grp.Suggestions = append(grp.Suggestions, cmd.Name)
grp.Descriptions[cmd.Name] = readline.Dim(cmd.ShortDescription)
}
}
// Add a new group if not found
if !found {
grp := &readline.CompletionGroup{
Name: cmd.Aliases[0],
Suggestions: []string{cmd.Name},
Descriptions: map[string]string{
cmd.Name: readline.Dim(cmd.ShortDescription),
},
}
completions = append(completions, grp)
}
}
}
// Make adjustments to the CompletionGroup list: set maxlength depending on items, check descriptions, etc.
for _, grp := range completions {
// If the length of suggestions is too long and we have
// many groups, use grid display.
if len(completions) >= 10 && len(grp.Suggestions) >= 7 {
grp.DisplayType = readline.TabDisplayGrid
} else {
// By default, we use a map of command to descriptions
grp.DisplayType = readline.TabDisplayList
}
}
return
}
// completeSubCommands - Takes subcommands and gives them as suggestions
// One category, from one source (a parent command).
func completeSubCommands(args []string, lastWord string, command *flags.Command) (completions []*readline.CompletionGroup) {
group := &readline.CompletionGroup{
Name: command.Name,
Suggestions: []string{},
Descriptions: map[string]string{},
DisplayType: readline.TabDisplayList,
}
for _, sub := range command.Commands() {
if strings.HasPrefix(sub.Name, lastWord) {
group.Suggestions = append(group.Suggestions, sub.Name)
group.Descriptions[sub.Name] = readline.DIM + sub.ShortDescription + readline.RESET
}
}
completions = append(completions, group)
return
}
// handleSubCommand - Handles completion for subcommand options and arguments, + any option value related completion
// Many categories, from many sources: this function calls the same functions as the ones previously called for completing its parent command.
func handleSubCommand(line []rune, pos int, command *flags.Command) (lastWord string, completions []*readline.CompletionGroup) {
args, last, lastWord := formatInput(line)
// Check environment variables
if envVarAsked(args, lastWord) {
completeEnvironmentVariables(lastWord)
}
// Check argument options
if len(command.Groups()) > 0 {
for _, grp := range command.Groups() {
if opt, yes := optionArgRequired(args, last, grp); yes {
return completeOptionArguments(command, opt, lastWord)
}
}
}
// If user asks for completions with "-" or "--". This must take precedence on arguments.
if subCommandOptionsAsked(args, lastWord, command) {
return completeCommandOptions(args, lastWord, command)
}
// If command has non-filled arguments, propose them first
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
return completeCommandArguments(command, arg, lastWord)
}
return
}
// completeCommandOptions - Yields completion for options of a command, with various decorators
// Many categories, from one source (a command)
func completeCommandOptions(args []string, lastWord string, cmd *flags.Command) (prefix string, completions []*readline.CompletionGroup) {
prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.
// Get all (root) option groups.
groups := cmd.Groups()
// Append command options not gathered in groups
groups = append(groups, cmd.Group)
// For each group, build completions
for _, grp := range groups {
_, comp := completeOptionGroup(lastWord, grp, "")
// No need to add empty groups, will screw the completion system.
if len(comp.Suggestions) > 0 {
completions = append(completions, comp)
}
}
// Do the same for global options, which are not part of any group "per-se"
_, gcomp := completeOptionGroup(lastWord, cmd.Group, "global options")
if len(gcomp.Suggestions) > 0 {
completions = append(completions, gcomp)
}
return
}
// completeOptionGroup - make completions for a single group of options. Title is optional, not used if empty.
func completeOptionGroup(lastWord string, grp *flags.Group, title string) (prefix string, compGrp *readline.CompletionGroup) {
compGrp = &readline.CompletionGroup{
Name: grp.ShortDescription,
Descriptions: map[string]string{},
DisplayType: readline.TabDisplayList,
Aliases: map[string]string{},
}
// An optional title for this comp group.
// Used by global flag options, added to all commands.
if title != "" {
compGrp.Name = title
}
// Add each option to completion group
for _, opt := range grp.Options() {
// Check if option is already set, next option if yes
// if optionNotRepeatable(opt) && optionIsAlreadySet(args, lastWord, opt) {
// continue
// }
// Depending on the current last word, either build a group with option longs only, or with shorts
if strings.HasPrefix("--"+opt.LongName, lastWord) {
optName := "--" + opt.LongName
compGrp.Suggestions = append(compGrp.Suggestions, optName)
// Add short if there is, and that the prefix is only one dash
if strings.HasPrefix("-", lastWord) {
if opt.ShortName != 0 {
compGrp.Aliases[optName] = "-" + string(opt.ShortName)
}
}
// Option default value if any
var def string
if len(opt.Default) > 0 {
def = " (default:"
for _, d := range opt.Default {
def += " " + d + ","
}
def = strings.TrimSuffix(def, ",")
def += ")"
}
desc := fmt.Sprintf(" -- %s%s%s", opt.Description, def, readline.RESET)
compGrp.Descriptions[optName] = desc
}
}
return
}
// RecursiveGroupCompletion - Handles recursive completion for nested option groups
// Many categories, one source (a command's root option group). Called by the function just above.
func RecursiveGroupCompletion(args []string, last []rune, group *flags.Group) (lastWord string, completions []*readline.CompletionGroup) {
return
}