Add an entry point for the completion tool
This commit is contained in:
parent
f4de6d2a10
commit
005a9c7090
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"kitty/tools/cli"
|
||||
"kitty/tools/cmd/at"
|
||||
"kitty/tools/completion"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -18,6 +19,8 @@ func main() {
|
||||
})
|
||||
root.AddCommand(at.EntryPoint(root))
|
||||
|
||||
root.AddCommand(completion.EntryPoint(root))
|
||||
|
||||
cli.Init(root)
|
||||
if err := cli.Execute(root); err != nil {
|
||||
cli.PrintError(err)
|
||||
|
||||
84
tools/completion/main.go
Normal file
84
tools/completion/main.go
Normal file
@ -0,0 +1,84 @@
|
||||
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package completion
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"kitty/tools/cli"
|
||||
"kitty/tools/utils"
|
||||
)
|
||||
|
||||
func json_input_parser(data []byte, shell_state map[string]string) ([]string, error) {
|
||||
ans := make([]string, 0, 32)
|
||||
err := json.Unmarshal(data, &ans)
|
||||
return ans, err
|
||||
}
|
||||
|
||||
func json_output_serializer(completions *Completions, shell_state map[string]string) ([]byte, error) {
|
||||
return json.Marshal(completions)
|
||||
}
|
||||
|
||||
type parser_func func(data []byte, shell_state map[string]string) ([]string, error)
|
||||
type serializer_func func(completions *Completions, shell_state map[string]string) ([]byte, error)
|
||||
|
||||
var input_parsers = make(map[string]parser_func, 4)
|
||||
var output_serializers = make(map[string]serializer_func, 4)
|
||||
|
||||
func init() {
|
||||
input_parsers["json"] = json_input_parser
|
||||
output_serializers["json"] = json_output_serializer
|
||||
}
|
||||
|
||||
func main(args []string) error {
|
||||
output_type := "json"
|
||||
if len(args) > 0 {
|
||||
output_type = args[0]
|
||||
args = args[1:]
|
||||
}
|
||||
shell_state := make(map[string]string, len(args))
|
||||
for _, arg := range args {
|
||||
k, v, found := utils.Cut(arg, "=")
|
||||
if !found {
|
||||
return fmt.Errorf("Invalid shell state specification: %s", arg)
|
||||
}
|
||||
shell_state[k] = v
|
||||
}
|
||||
input_parser := input_parsers[output_type]
|
||||
output_serializer := output_serializers[output_type]
|
||||
if input_parser == nil || output_serializer == nil {
|
||||
return fmt.Errorf("Unknown output type: %s", output_type)
|
||||
}
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argv, err := input_parser(data, shell_state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
completions := GetCompletions(argv)
|
||||
output, err := output_serializer(completions, shell_state)
|
||||
if err == nil {
|
||||
os.Stdout.Write(output)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func EntryPoint(tool_root *cobra.Command) *cobra.Command {
|
||||
complete_command := cli.CreateCommand(&cobra.Command{
|
||||
Use: "__complete__ output_type [shell state...]",
|
||||
Short: "Generate completions for kitty commands",
|
||||
Long: "Generate completion candidates for kitty commands. The command line is read from STDIN. output_type can be one of the supported shells or 'json' for JSON output.",
|
||||
Hidden: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return main(args)
|
||||
},
|
||||
})
|
||||
return complete_command
|
||||
}
|
||||
@ -12,29 +12,36 @@ func (self *Completions) add_group(group *MatchGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *command) find_option(name_including_leading_dash string) *option {
|
||||
q := strings.TrimLeft(name_including_leading_dash, "-")
|
||||
for _, opt := range self.options {
|
||||
for _, alias := range opt.aliases {
|
||||
func (self *Command) find_option(name_including_leading_dash string) *Option {
|
||||
var q string
|
||||
if strings.HasPrefix(name_including_leading_dash, "--") {
|
||||
q = name_including_leading_dash[2:]
|
||||
} else if strings.HasPrefix(name_including_leading_dash, "-") {
|
||||
q = name_including_leading_dash[len(name_including_leading_dash)-1:]
|
||||
} else {
|
||||
q = name_including_leading_dash
|
||||
}
|
||||
for _, opt := range self.Options {
|
||||
for _, alias := range opt.Aliases {
|
||||
if alias == q {
|
||||
return &opt
|
||||
return opt
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Completions) add_options_group(options []option, word string) {
|
||||
func (self *Completions) add_options_group(options []*Option, word string) {
|
||||
group := MatchGroup{Title: "Options"}
|
||||
group.Matches = make([]*Match, 0, 8)
|
||||
seen_flags := make(map[string]bool)
|
||||
if strings.HasPrefix(word, "--") {
|
||||
prefix := word[2:]
|
||||
for _, opt := range options {
|
||||
for _, q := range opt.aliases {
|
||||
for _, q := range opt.Aliases {
|
||||
if len(q) > 1 && strings.HasPrefix(q, prefix) {
|
||||
seen_flags[q] = true
|
||||
group.Matches = append(group.Matches, &Match{Word: "--" + q, Description: opt.description})
|
||||
group.Matches = append(group.Matches, &Match{Word: "--" + q, Description: opt.Description})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -48,10 +55,10 @@ func (self *Completions) add_options_group(options []option, word string) {
|
||||
}
|
||||
group.WordPrefix = word
|
||||
for _, opt := range options {
|
||||
for _, q := range opt.aliases {
|
||||
for _, q := range opt.Aliases {
|
||||
if len(q) == 1 && !seen_flags[q] {
|
||||
seen_flags[q] = true
|
||||
group.Matches = append(group.Matches, &Match{Word: q, FullForm: "-" + q, Description: opt.description})
|
||||
group.Matches = append(group.Matches, &Match{Word: q, FullForm: "-" + q, Description: opt.Description})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,11 +66,11 @@ func (self *Completions) add_options_group(options []option, word string) {
|
||||
self.add_group(&group)
|
||||
}
|
||||
|
||||
func complete_word(word string, completions *Completions, only_args_allowed bool, expecting_arg_for *option, arg_num int) {
|
||||
cmd := Completions.current_cmd
|
||||
func complete_word(word string, completions *Completions, only_args_allowed bool, expecting_arg_for *Option, arg_num int) {
|
||||
cmd := completions.current_cmd
|
||||
if expecting_arg_for != nil {
|
||||
if expecting_arg_for.completion_for_arg != nil {
|
||||
expecting_arg_for.completion_for_arg(completions, word)
|
||||
if expecting_arg_for.Completion_for_arg != nil {
|
||||
expecting_arg_for.Completion_for_arg(completions, word)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -72,27 +79,27 @@ func complete_word(word string, completions *Completions, only_args_allowed bool
|
||||
idx := strings.Index(word, "=")
|
||||
option := cmd.find_option(word[:idx])
|
||||
if option != nil {
|
||||
if option.completion_for_arg != nil {
|
||||
if option.Completion_for_arg != nil {
|
||||
completions.WordPrefix = word[:idx+1]
|
||||
option.completion_for_arg(completions, word[idx+1:])
|
||||
option.Completion_for_arg(completions, word[idx+1:])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completions.add_options_group(cmd.options, word)
|
||||
completions.add_options_group(cmd.Options, word)
|
||||
}
|
||||
return
|
||||
}
|
||||
if arg_num == 1 && len(cmd.subcommands) > 0 {
|
||||
for _, sc := range cmd.subcommands {
|
||||
if strings.HasPrefix(sc.name, word) {
|
||||
title := cmd.subcommands_title
|
||||
if arg_num == 1 && len(cmd.Subcommands) > 0 {
|
||||
for _, sc := range cmd.Subcommands {
|
||||
if strings.HasPrefix(sc.Name, word) {
|
||||
title := cmd.Subcommands_title
|
||||
if title == "" {
|
||||
title = "Sub-commands"
|
||||
}
|
||||
group := MatchGroup{Title: title}
|
||||
group.Matches = make([]*Match, 0, len(cmd.subcommands))
|
||||
if strings.HasPrefix(sc, word) {
|
||||
group.Matches = append(group.Matches, &Match{Word: sc.name, Description: sc.description})
|
||||
group.Matches = make([]*Match, 0, len(cmd.Subcommands))
|
||||
if strings.HasPrefix(sc.Name, word) {
|
||||
group.Matches = append(group.Matches, &Match{Word: sc.Name, Description: sc.Description})
|
||||
}
|
||||
completions.add_group(&group)
|
||||
}
|
||||
@ -100,20 +107,29 @@ func complete_word(word string, completions *Completions, only_args_allowed bool
|
||||
return
|
||||
}
|
||||
|
||||
if cmd.completion_for_arg != nil {
|
||||
cmd.completion_for_arg(completions, word)
|
||||
if cmd.Completion_for_arg != nil {
|
||||
cmd.Completion_for_arg(completions, word)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parse_args(cmd *command, words []string, completions *Completions) {
|
||||
func (self *Command) find_subcommand(name string) *Command {
|
||||
for _, sc := range self.Subcommands {
|
||||
if sc.Name == name {
|
||||
return sc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *Command) parse_args(words []string, completions *Completions) {
|
||||
completions.current_cmd = cmd
|
||||
if len(words) == 0 {
|
||||
complete_word("", completions, false, nil, 0)
|
||||
return
|
||||
}
|
||||
|
||||
var expecting_arg_for *option
|
||||
var expecting_arg_for *Option
|
||||
only_args_allowed := false
|
||||
arg_num := 0
|
||||
|
||||
@ -135,14 +151,17 @@ func parse_args(cmd *command, words []string, completions *Completions) {
|
||||
continue
|
||||
}
|
||||
if !only_args_allowed && strings.HasPrefix(word, "-") {
|
||||
// TODO:
|
||||
// handle single letter multiple options -abcd
|
||||
// handle standalone --long-opt
|
||||
// handle long opt ends with =
|
||||
// handle long opt containing =
|
||||
idx := strings.Index(word, "=")
|
||||
if idx > -1 {
|
||||
continue
|
||||
}
|
||||
option := cmd.find_option(word[:idx])
|
||||
if option != nil {
|
||||
expecting_arg_for = option
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(cmd.subcommands) > 0 && arg_num == 1 {
|
||||
if len(cmd.Subcommands) > 0 && arg_num == 1 {
|
||||
sc := cmd.find_subcommand(word)
|
||||
if sc == nil {
|
||||
only_args_allowed = true
|
||||
@ -152,7 +171,7 @@ func parse_args(cmd *command, words []string, completions *Completions) {
|
||||
cmd = sc
|
||||
arg_num = 0
|
||||
only_args_allowed = false
|
||||
} else if cmd.stop_processing_at_arg > 0 && arg_num >= cmd.stop_processing_at_arg {
|
||||
} else if cmd.Stop_processing_at_arg > 0 && arg_num >= cmd.Stop_processing_at_arg {
|
||||
return
|
||||
} else {
|
||||
only_args_allowed = true
|
||||
|
||||
@ -3,44 +3,62 @@
|
||||
package completion
|
||||
|
||||
type Match struct {
|
||||
Word string
|
||||
FullForm string
|
||||
Description string
|
||||
Word string `json:"word,omitempty"`
|
||||
FullForm string `json:"full_form,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type MatchGroup struct {
|
||||
Title string
|
||||
NoTrailingSpace, IsFiles bool
|
||||
Matches []*Match
|
||||
WordPrefix string
|
||||
Title string `json:"title,omitempty"`
|
||||
NoTrailingSpace bool `json:"no_trailing_space,omitempty"`
|
||||
IsFiles bool `json:"is_files,omitempty"`
|
||||
Matches []*Match `json:"matches,omitempty"`
|
||||
WordPrefix string `json:"word_prefix,omitempty"`
|
||||
}
|
||||
|
||||
type Completions struct {
|
||||
Groups []*MatchGroup
|
||||
WordPrefix string
|
||||
Groups []*MatchGroup `json:"groups,omitempty"`
|
||||
WordPrefix string `json:"word_prefix,omitempty"`
|
||||
|
||||
current_cmd *command
|
||||
current_cmd *Command
|
||||
}
|
||||
|
||||
type completion_func func(completions *Completions, partial_word string)
|
||||
|
||||
type option struct {
|
||||
name string
|
||||
aliases []string
|
||||
description string
|
||||
has_following_arg bool
|
||||
completion_for_arg completion_func
|
||||
type Option struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Description string
|
||||
Has_following_arg bool
|
||||
Completion_for_arg completion_func
|
||||
}
|
||||
|
||||
type command struct {
|
||||
name string
|
||||
description string
|
||||
type Command struct {
|
||||
Name string
|
||||
Description string
|
||||
|
||||
options []option
|
||||
// List of options for this command
|
||||
Options []*Option
|
||||
|
||||
subcommands []command
|
||||
subcommands_title string
|
||||
// List of subcommands
|
||||
Subcommands []*Command
|
||||
// Optional title used as a header when displaying the list of matching sub-commands for a completion
|
||||
Subcommands_title string
|
||||
|
||||
completion_for_arg completion_func
|
||||
stop_processing_at_arg int
|
||||
Completion_for_arg completion_func
|
||||
Stop_processing_at_arg int
|
||||
}
|
||||
|
||||
var Root = Command{Options: make([]*Option, 0), Subcommands: make([]*Command, 0, 32)}
|
||||
|
||||
func GetCompletions(argv []string) *Completions {
|
||||
ans := Completions{Groups: make([]*MatchGroup, 0, 4)}
|
||||
if len(argv) > 0 {
|
||||
exe := argv[0]
|
||||
cmd := Root.find_subcommand(exe)
|
||||
if cmd != nil {
|
||||
cmd.parse_args(argv[1:], &ans)
|
||||
}
|
||||
}
|
||||
return &ans
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user