diff --git a/gen-go-code.py b/gen-go-code.py index 958e9c8e4..cd0637cc8 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -61,6 +61,8 @@ def generate_completions_for_kitty() -> None: print('k := root.add_command("kitty", "")') print('k.First_arg_may_not_be_subcommand = true') 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.Description = "Control kitty using commands"') for go_name in all_command_names(): diff --git a/kitty/cli.py b/kitty/cli.py index 3eebc859e..261ed5536 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -55,12 +55,10 @@ class GoOption: self.aliases = [] if len(flags) > 1 and not flags[0].startswith("--"): short = flags[0][1:] - del flags[0] self.short, self.long = short, x['name'].replace('_', '-') for f in flags: - q = f[2:] - if q != self.long: - self.aliases.append(q) + q = f[2:] if f.startswith('--') else f[1:] + self.aliases.append(q) self.usage = serialize_as_go_string(x['help'].strip()) self.type = x['type'] self.dest = x['dest'] @@ -96,9 +94,7 @@ class GoOption: return f'{base}.StringArrayP("{self.long}", "{self.short}", {defval}, "{self.usage}")' return f'{base}.StringArray("{self.long}", {defval}, "{self.usage}")' elif self.type == 'choices': - choices = sorted(self.obj_dict['choices']) - choices.remove(self.default or '') - choices.insert(0, self.default or '') + choices = self.sorted_choices cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in choices) if self.short: 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' 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]: for x in seq: diff --git a/kitty_tests/completion.py b/kitty_tests/completion.py index ce4f2a7a7..1a9c5600b 100644 --- a/kitty_tests/completion.py +++ b/kitty_tests/completion.py @@ -20,25 +20,34 @@ class TestCompletion(BaseTest): 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 t(self, result): q = set(words) - for group in result.get('groups', ()): - for m in group['matches']: - if m['word'] in words: - q.discard(m['word']) - self.assertFalse(q, f'Command line: {self.current_cmd!r}') + missing = q - get_all_words(result) + self.assertFalse(missing, f'Words missing. Command line: {self.current_cmd!r}') + return t + + +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 def all_words(*words): def t(self, result): expected = set(words) - actual = set() - - for group in result.get('groups', ()): - for m in group['matches']: - actual.add(m['word']) + actual = get_all_words(result) self.assertEqual(expected, actual, f'Command line: {self.current_cmd!r}') 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 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()): self.current_cmd = cmd for test in tests: diff --git a/tools/completion/parse-args.go b/tools/completion/parse-args.go index 2296268e6..755a103e8 100644 --- a/tools/completion/parse-args.go +++ b/tools/completion/parse-args.go @@ -4,10 +4,12 @@ package completion import ( "fmt" + "os" "strings" ) var _ = fmt.Print +var _ = os.Getenv func (self *Completions) add_group(group *MatchGroup) { 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) { - group := MatchGroup{Title: "Options"} - group.Matches = make([]*Match, 0, 8) - seen_flags := make(map[string]bool) + group := self.add_match_group("Options") if strings.HasPrefix(word, "--") { + if word == "--" { + group.Matches = append(group.Matches, &Match{Word: "--", Description: "End of options"}) + } prefix := word[2:] for _, opt := range options { 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}) + break } } } } else { if word == "-" { - group.Matches = append(group.Matches, &Match{Word: "--"}) - } else { - for _, letter := range []rune(word[1:]) { - seen_flags[string(letter)] = true + group.Matches = append(group.Matches, &Match{Word: "--", Description: "End of options"}) + for _, opt := range options { + for _, q := range opt.Aliases { + if len(q) == 1 { + group.add_match("-"+q, opt.Description) + break + } + } } - } - group.WordPrefix = word - for _, opt := range options { - for _, q := range opt.Aliases { - if len(q) == 1 && !seen_flags[q] { - seen_flags[q] = true - group.add_match(q, opt.Description).FullForm = "-" + q + } else { + runes := []rune(word) + last_letter := string(runes[len(runes)-1]) + for _, opt := range options { + for _, q := range opt.Aliases { + if q == last_letter { + 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) { @@ -147,13 +155,11 @@ func (cmd *Command) parse_args(words []string, completions *Completions) { continue } if !only_args_allowed && strings.HasPrefix(word, "-") { - idx := strings.Index(word, "=") - if idx > -1 { - continue - } - option := cmd.find_option(word[:idx]) - if option != nil && option.Has_following_arg { - expecting_arg_for = option + if !strings.Contains(word, "=") { + option := cmd.find_option(word) + if option != nil && option.Has_following_arg { + expecting_arg_for = option + } } continue } diff --git a/tools/completion/types.go b/tools/completion/types.go index c1601906f..2383acf03 100644 --- a/tools/completion/types.go +++ b/tools/completion/types.go @@ -6,7 +6,6 @@ import "strings" type Match struct { Word string `json:"word,omitempty"` - FullForm string `json:"full_form,omitempty"` Description string `json:"description,omitempty"` } @@ -123,6 +122,10 @@ func (self *Command) has_subcommands() bool { return false } +func (self *Command) add_option(opt *Option) { + self.Options = append(self.Options, opt) +} + func (self *Command) GetCompletions(argv []string) *Completions { ans := Completions{Groups: make([]*MatchGroup, 0, 4)} if len(argv) > 0 {