Basic completion for options
This commit is contained in:
parent
18c3e46ac6
commit
c5afceb4cb
@ -61,6 +61,8 @@ def generate_completions_for_kitty() -> None:
|
|||||||
print('k := root.add_command("kitty", "")')
|
print('k := root.add_command("kitty", "")')
|
||||||
print('k.First_arg_may_not_be_subcommand = true')
|
print('k.First_arg_may_not_be_subcommand = true')
|
||||||
print('k.Completion_for_arg = complete_kitty')
|
print('k.Completion_for_arg = complete_kitty')
|
||||||
|
for opt in go_options_for_seq(parse_option_spec()[0]):
|
||||||
|
print(opt.as_completion_option('k'))
|
||||||
print('at := k.add_command("@", "Remote control")')
|
print('at := k.add_command("@", "Remote control")')
|
||||||
print('at.Description = "Control kitty using commands"')
|
print('at.Description = "Control kitty using commands"')
|
||||||
for go_name in all_command_names():
|
for go_name in all_command_names():
|
||||||
|
|||||||
29
kitty/cli.py
29
kitty/cli.py
@ -55,12 +55,10 @@ class GoOption:
|
|||||||
self.aliases = []
|
self.aliases = []
|
||||||
if len(flags) > 1 and not flags[0].startswith("--"):
|
if len(flags) > 1 and not flags[0].startswith("--"):
|
||||||
short = flags[0][1:]
|
short = flags[0][1:]
|
||||||
del flags[0]
|
|
||||||
self.short, self.long = short, x['name'].replace('_', '-')
|
self.short, self.long = short, x['name'].replace('_', '-')
|
||||||
for f in flags:
|
for f in flags:
|
||||||
q = f[2:]
|
q = f[2:] if f.startswith('--') else f[1:]
|
||||||
if q != self.long:
|
self.aliases.append(q)
|
||||||
self.aliases.append(q)
|
|
||||||
self.usage = serialize_as_go_string(x['help'].strip())
|
self.usage = serialize_as_go_string(x['help'].strip())
|
||||||
self.type = x['type']
|
self.type = x['type']
|
||||||
self.dest = x['dest']
|
self.dest = x['dest']
|
||||||
@ -96,9 +94,7 @@ class GoOption:
|
|||||||
return f'{base}.StringArrayP("{self.long}", "{self.short}", {defval}, "{self.usage}")'
|
return f'{base}.StringArrayP("{self.long}", "{self.short}", {defval}, "{self.usage}")'
|
||||||
return f'{base}.StringArray("{self.long}", {defval}, "{self.usage}")'
|
return f'{base}.StringArray("{self.long}", {defval}, "{self.usage}")'
|
||||||
elif self.type == 'choices':
|
elif self.type == 'choices':
|
||||||
choices = sorted(self.obj_dict['choices'])
|
choices = self.sorted_choices
|
||||||
choices.remove(self.default or '')
|
|
||||||
choices.insert(0, self.default or '')
|
|
||||||
cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in choices)
|
cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in choices)
|
||||||
if self.short:
|
if self.short:
|
||||||
return f'cli.ChoicesP({base}, "{self.long}", "{self.short}", "{self.usage}", {cx})'
|
return f'cli.ChoicesP({base}, "{self.long}", "{self.short}", "{self.usage}", {cx})'
|
||||||
@ -112,6 +108,25 @@ class GoOption:
|
|||||||
ans += f'\n{struct_name}.{self.go_var_name} = {self.go_var_name}_temp'
|
ans += f'\n{struct_name}.{self.go_var_name} = {self.go_var_name}_temp'
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sorted_choices(self) -> List[str]:
|
||||||
|
choices = sorted(self.obj_dict['choices'])
|
||||||
|
choices.remove(self.default or '')
|
||||||
|
choices.insert(0, self.default or '')
|
||||||
|
return choices
|
||||||
|
|
||||||
|
def as_completion_option(self, command_name: str) -> str:
|
||||||
|
ans = f'{command_name}.add_option(&' 'Option{Name: ' f'"{serialize_as_go_string(self.long)}", '
|
||||||
|
ans += f'Description: "{self.usage}", '
|
||||||
|
aliases = (f'"{serialize_as_go_string(x)}"' for x in self.aliases)
|
||||||
|
ans += 'Aliases: []string{' f'{", ".join(aliases)}' '}, '
|
||||||
|
if self.go_type != 'bool':
|
||||||
|
ans += 'Has_following_arg: true, '
|
||||||
|
if self.type == 'choices':
|
||||||
|
cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in self.sorted_choices)
|
||||||
|
ans += f'Completion_for_arg: names_completer("Choices for {self.long}", {cx}),'
|
||||||
|
return ans + '})'
|
||||||
|
|
||||||
|
|
||||||
def go_options_for_seq(seq: 'OptionSpecSeq') -> Iterator[GoOption]:
|
def go_options_for_seq(seq: 'OptionSpecSeq') -> Iterator[GoOption]:
|
||||||
for x in seq:
|
for x in seq:
|
||||||
|
|||||||
@ -20,25 +20,34 @@ class TestCompletion(BaseTest):
|
|||||||
completion(self, tdir)
|
completion(self, tdir)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_words(result):
|
||||||
|
all_words = set()
|
||||||
|
for group in result.get('groups', ()):
|
||||||
|
for m in group['matches']:
|
||||||
|
all_words.add(m['word'])
|
||||||
|
return all_words
|
||||||
|
|
||||||
|
|
||||||
def has_words(*words):
|
def has_words(*words):
|
||||||
def t(self, result):
|
def t(self, result):
|
||||||
q = set(words)
|
q = set(words)
|
||||||
for group in result.get('groups', ()):
|
missing = q - get_all_words(result)
|
||||||
for m in group['matches']:
|
self.assertFalse(missing, f'Words missing. Command line: {self.current_cmd!r}')
|
||||||
if m['word'] in words:
|
return t
|
||||||
q.discard(m['word'])
|
|
||||||
self.assertFalse(q, f'Command line: {self.current_cmd!r}')
|
|
||||||
|
def does_not_have_words(*words):
|
||||||
|
def t(self, result):
|
||||||
|
q = set(words)
|
||||||
|
all_words = get_all_words(result)
|
||||||
|
self.assertFalse(q & all_words, f'Words unexpectedly present. Command line: {self.current_cmd!r}')
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
def all_words(*words):
|
def all_words(*words):
|
||||||
def t(self, result):
|
def t(self, result):
|
||||||
expected = set(words)
|
expected = set(words)
|
||||||
actual = set()
|
actual = get_all_words(result)
|
||||||
|
|
||||||
for group in result.get('groups', ()):
|
|
||||||
for m in group['matches']:
|
|
||||||
actual.add(m['word'])
|
|
||||||
self.assertEqual(expected, actual, f'Command line: {self.current_cmd!r}')
|
self.assertEqual(expected, actual, f'Command line: {self.current_cmd!r}')
|
||||||
return t
|
return t
|
||||||
|
|
||||||
@ -103,6 +112,13 @@ def completion(self: TestCompletion, tdir: str):
|
|||||||
add('kitty @ set-window-logo e', all_words('exe-not2.jpeg'))
|
add('kitty @ set-window-logo e', all_words('exe-not2.jpeg'))
|
||||||
add('kitty @ set-window-logo e e', all_words())
|
add('kitty @ set-window-logo e e', all_words())
|
||||||
|
|
||||||
|
add('kitty -', has_words('-c', '-1', '--'), does_not_have_words('--config', '--single-instance'))
|
||||||
|
add('kitty -c', all_words('-c'))
|
||||||
|
add('kitty --', has_words('--config', '--single-instance', '--'))
|
||||||
|
add('kitty --c', has_words('--config', '--class'))
|
||||||
|
add('kitty --start-as', all_words('--start-as'))
|
||||||
|
add('kitty --start-as ', all_words('minimized', 'maximized', 'fullscreen', 'normal'))
|
||||||
|
|
||||||
for cmd, tests, result in zip(all_cmds, all_tests, run_tool()):
|
for cmd, tests, result in zip(all_cmds, all_tests, run_tool()):
|
||||||
self.current_cmd = cmd
|
self.current_cmd = cmd
|
||||||
for test in tests:
|
for test in tests:
|
||||||
|
|||||||
@ -4,10 +4,12 @@ package completion
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
|
var _ = os.Getenv
|
||||||
|
|
||||||
func (self *Completions) add_group(group *MatchGroup) {
|
func (self *Completions) add_group(group *MatchGroup) {
|
||||||
if len(group.Matches) > 0 {
|
if len(group.Matches) > 0 {
|
||||||
@ -35,38 +37,44 @@ func (self *Command) find_option(name_including_leading_dash string) *Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 := self.add_match_group("Options")
|
||||||
group.Matches = make([]*Match, 0, 8)
|
|
||||||
seen_flags := make(map[string]bool)
|
|
||||||
if strings.HasPrefix(word, "--") {
|
if strings.HasPrefix(word, "--") {
|
||||||
|
if word == "--" {
|
||||||
|
group.Matches = append(group.Matches, &Match{Word: "--", Description: "End of options"})
|
||||||
|
}
|
||||||
prefix := word[2:]
|
prefix := word[2:]
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
for _, q := range opt.Aliases {
|
for _, q := range opt.Aliases {
|
||||||
if len(q) > 1 && strings.HasPrefix(q, prefix) {
|
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})
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if word == "-" {
|
if word == "-" {
|
||||||
group.Matches = append(group.Matches, &Match{Word: "--"})
|
group.Matches = append(group.Matches, &Match{Word: "--", Description: "End of options"})
|
||||||
} else {
|
for _, opt := range options {
|
||||||
for _, letter := range []rune(word[1:]) {
|
for _, q := range opt.Aliases {
|
||||||
seen_flags[string(letter)] = true
|
if len(q) == 1 {
|
||||||
|
group.add_match("-"+q, opt.Description)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
group.WordPrefix = word
|
runes := []rune(word)
|
||||||
for _, opt := range options {
|
last_letter := string(runes[len(runes)-1])
|
||||||
for _, q := range opt.Aliases {
|
for _, opt := range options {
|
||||||
if len(q) == 1 && !seen_flags[q] {
|
for _, q := range opt.Aliases {
|
||||||
seen_flags[q] = true
|
if q == last_letter {
|
||||||
group.add_match(q, opt.Description).FullForm = "-" + q
|
group.add_match(word, opt.Description)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.add_group(&group)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func complete_word(word string, completions *Completions, only_args_allowed bool, expecting_arg_for *Option, arg_num int) {
|
func complete_word(word string, completions *Completions, only_args_allowed bool, expecting_arg_for *Option, arg_num int) {
|
||||||
@ -147,13 +155,11 @@ func (cmd *Command) parse_args(words []string, completions *Completions) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !only_args_allowed && strings.HasPrefix(word, "-") {
|
if !only_args_allowed && strings.HasPrefix(word, "-") {
|
||||||
idx := strings.Index(word, "=")
|
if !strings.Contains(word, "=") {
|
||||||
if idx > -1 {
|
option := cmd.find_option(word)
|
||||||
continue
|
if option != nil && option.Has_following_arg {
|
||||||
}
|
expecting_arg_for = option
|
||||||
option := cmd.find_option(word[:idx])
|
}
|
||||||
if option != nil && option.Has_following_arg {
|
|
||||||
expecting_arg_for = option
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import "strings"
|
|||||||
|
|
||||||
type Match struct {
|
type Match struct {
|
||||||
Word string `json:"word,omitempty"`
|
Word string `json:"word,omitempty"`
|
||||||
FullForm string `json:"full_form,omitempty"`
|
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +122,10 @@ func (self *Command) has_subcommands() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *Command) add_option(opt *Option) {
|
||||||
|
self.Options = append(self.Options, opt)
|
||||||
|
}
|
||||||
|
|
||||||
func (self *Command) GetCompletions(argv []string) *Completions {
|
func (self *Command) GetCompletions(argv []string) *Completions {
|
||||||
ans := Completions{Groups: make([]*MatchGroup, 0, 4)}
|
ans := Completions{Groups: make([]*MatchGroup, 0, 4)}
|
||||||
if len(argv) > 0 {
|
if len(argv) > 0 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user