diff --git a/kittens/ssh/completion.py b/kittens/ssh/completion.py index 9f15a8109..9d82b2e17 100644 --- a/kittens/ssh/completion.py +++ b/kittens/ssh/completion.py @@ -168,7 +168,7 @@ def complete_choices(ans: Completions, prefix: str, title: str, key: str, comma_ q = line.strip() if q.startswith(prefix): choices[q] = '' - ans.match_groups[title] = choices + ans.add_match_group(title, choices) def complete_arg(ans: Completions, option_flag: str, prefix: str = '') -> None: @@ -185,17 +185,17 @@ def complete_arg(ans: Completions, option_flag: str, prefix: str = '') -> None: def complete_destination(ans: Completions, prefix: str = '') -> None: - result = {k: '' for k in known_hosts() if k.startswith(prefix)} - ans.match_groups['remote host name'] = result + result = (k for k in known_hosts() if k.startswith(prefix)) + ans.add_match_group('remote host name', result) def complete_option(ans: Completions, prefix: str = '-') -> None: hm = option_help_map() if len(prefix) <= 1: result = {k: v for k, v in hm.items() if k.startswith(prefix)} - ans.match_groups['option'] = result + ans.add_match_group('option', result) else: - ans.match_groups['option'] = {prefix: ''} + ans.add_match_group('option', {prefix: ''}) def complete(ans: Completions, words: Sequence[str], new_word: bool) -> None: diff --git a/kitty/complete.py b/kitty/complete.py index 2867d6a61..0a505c642 100644 --- a/kitty/complete.py +++ b/kitty/complete.py @@ -6,8 +6,8 @@ import os import shlex import sys from typing import ( - Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Set, - Tuple + Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple, + Union ) from kittens.runner import ( @@ -45,7 +45,23 @@ them into something your shell will understand. parsers: Dict[str, Callable] = {} serializers: Dict[str, Callable] = {} -MatchGroup = Dict[str, str] + + +class MatchGroup: + + def __init__(self, x: Union[Dict[str, str], Iterable[str]], trailing_space: bool = True, is_files: bool = False): + self.mdict = x if isinstance(x, dict) else dict.fromkeys(x, '') + self.trailing_space = trailing_space + self.is_files = is_files + + def __iter__(self) -> Iterator[str]: + return iter(self.mdict) + + def items(self) -> Iterator[Tuple[str, str]]: + return iter(self.mdict.items()) + + def values(self) -> Iterator[str]: + return iter(self.mdict.values()) def debug(*a: Any, **kw: Any) -> None: @@ -76,10 +92,12 @@ class Completions: def __init__(self) -> None: self.match_groups: Dict[str, MatchGroup] = {} - self.no_space_groups: Set[str] = set() - self.files_groups: Set[str] = set() self.delegate: Delegate = Delegate() + def add_match_group(self, name: str, x: Union[Dict[str, str], Iterable[str]], trailing_space: bool = True, is_files: bool = False) -> MatchGroup: + self.match_groups[name] = m = MatchGroup(x, trailing_space, is_files) + return m + @run_once def remote_control_command_names() -> Tuple[str, ...]: @@ -191,14 +209,14 @@ def zsh_output_serializer(ans: Completions) -> str: for description, matches in ans.match_groups.items(): cmd = ['compadd', '-U', '-J', shlex.quote(description), '-X', shlex.quote('%B' + description + '%b')] - if description in ans.no_space_groups: + if not matches.trailing_space: cmd += ['-S', '""'] - if description in ans.files_groups: + if matches.is_files: cmd.append('-f') common_prefix = os.path.commonprefix(tuple(matches)) if common_prefix: cmd.extend(('-p', shlex.quote(common_prefix))) - matches = {k[len(common_prefix):]: v for k, v in matches.items()} + matches = MatchGroup({k[len(common_prefix):]: v for k, v in matches.items()}) has_descriptions = any(matches.values()) if has_descriptions: lines.append('compdescriptions=(') @@ -231,9 +249,8 @@ def zsh_output_serializer(ans: Completions) -> str: def bash_output_serializer(ans: Completions) -> str: lines = [] for description, matches in ans.match_groups.items(): - needs_space = description not in ans.no_space_groups for word in matches: - if needs_space: + if matches.trailing_space: word += ' ' lines.append('COMPREPLY+=({})'.format(shlex.quote(word))) # debug('\n'.join(lines)) @@ -253,11 +270,11 @@ def fish_output_serializer(ans: Completions) -> str: def completions_for_first_word(ans: Completions, prefix: str, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> None: cmds = ['@' + c for c in remote_control_command_names()] - ans.match_groups['Entry points'] = { + ans.add_match_group('Entry points', { k: '' for k in list(entry_points) + cmds + ['+' + k for k in namespaced_entry_points] if not prefix or k.startswith(prefix) - } + }) if prefix: ans.delegate = Delegate([prefix], 0) @@ -270,7 +287,7 @@ def kitty_cli_opts(ans: Completions, prefix: Optional[str] = None) -> None: aliases = frozenset(x for x in opt['aliases'] if x.startswith(prefix)) if prefix else opt['aliases'] for alias in aliases: matches[alias] = opt['help'].strip() - ans.match_groups['Options'] = matches + ans.add_match_group('Options', matches) def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None: @@ -283,8 +300,7 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix: if dest == 'override': from kitty.config import option_names_for_completion k = 'Config directives' - ans.match_groups[k] = {k+'=': '' for k in option_names_for_completion() if k.startswith(prefix)} - ans.no_space_groups.add(k) + ans.add_match_group(k, {k+'=': '' for k in option_names_for_completion() if k.startswith(prefix)}, trailing_space=False) elif dest == 'config': def is_conf_file(x: str) -> bool: @@ -301,13 +317,11 @@ def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix: complete_files_and_dirs(ans, prefix, files_group_name='Directories', predicate=os.path.isdir) elif dest == 'start_as': k = 'Start as' - ans.match_groups[k] = {x: x for x in 'normal,fullscreen,maximized,minimized'.split(',') if x.startswith(prefix)} - ans.no_space_groups.add(k) + ans.add_match_group(k, {x: x for x in 'normal,fullscreen,maximized,minimized'.split(',') if x.startswith(prefix)}, trailing_space=False) elif dest == 'listen_on': if ':' not in prefix: k = 'Address type' - ans.match_groups[k] = {x: x for x in ('unix:', 'tcp:') if x.startswith(prefix)} - ans.no_space_groups.add(k) + ans.add_match_group(k, {x: x for x in ('unix:', 'tcp:') if x.startswith(prefix)}, trailing_space=False) elif prefix.startswith('unix:') and not prefix.startswith('@'): complete_files_and_dirs(ans, prefix[len('unix:'):], files_group_name='UNIX sockets', add_prefix='unix:') @@ -336,7 +350,7 @@ def complete_alias_map( opt = option_map.get(w) if w is last_word and not new_word: if w.startswith('-'): - ans.match_groups['Options'] = {k: opt['help'] for k, opt in option_map.items() if k.startswith(last_word)} + ans.add_match_group('Options', {k: opt['help'] for k, opt in option_map.items() if k.startswith(last_word)}) else: if complete_args is not None: complete_args(ans, None, last_word, Delegate(words, i)) @@ -353,7 +367,7 @@ def complete_alias_map( prefix = '' if new_word else last_word if complete_args is not None: complete_args(ans, None, prefix, Delegate()) - ans.match_groups['Options'] = {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)} + ans.add_match_group('Options', {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)}) def complete_cli( @@ -432,12 +446,9 @@ def complete_files_and_dirs( files = (add_prefix + x for x in files) if dirs: - ans.match_groups['Directories'] = dict.fromkeys(dirs) - ans.files_groups.add('Directories') - ans.no_space_groups.add('Directories') + ans.add_match_group('Directories', dirs, trailing_space=False, is_files=True) if files: - ans.match_groups[files_group_name] = dict.fromkeys(files) - ans.files_groups.add(files_group_name) + ans.add_match_group(files_group_name, files, is_files=True) def complete_icat_args(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None: @@ -474,7 +485,7 @@ def remote_args_completer(title: str, words: Iterable[str]) -> CompleteArgsFunc: def complete_names_for_arg(ans: Completions, opt: Optional[OptionDict], prefix: str, unknown_args: Delegate) -> None: if opt is None: - ans.match_groups[title] = {c: '' for c in items if c.startswith(prefix)} + ans.add_match_group(title, {c: '' for c in items if c.startswith(prefix)}) return complete_names_for_arg @@ -530,24 +541,24 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl if words[0] == '@': if len(words) == 1 or (len(words) == 2 and not new_word): prefix = words[1] if len(words) > 1 else '' - ans.match_groups['Remote control commands'] = {c: '' for c in remote_control_command_names() if c.startswith(prefix)} + ans.add_match_group('Remote control commands', {c: '' for c in remote_control_command_names() if c.startswith(prefix)}) else: complete_remote_command(ans, words[1], words[2:], new_word) return ans if words[0].startswith('@'): if len(words) == 1 and not new_word: prefix = words[0] - ans.match_groups['Remote control commands'] = {'@' + c: '' for c in remote_control_command_names() if c.startswith(prefix)} + ans.add_match_group('Remote control commands', {'@' + c: '' for c in remote_control_command_names() if c.startswith(prefix)}) else: complete_remote_command(ans, words[0][1:], words[1:], new_word) if words[0] == '+': if len(words) == 1 or (len(words) == 2 and not new_word): prefix = words[1] if len(words) > 1 else '' - ans.match_groups['Entry points'] = {c: '' for c in namespaced_entry_points if c.startswith(prefix)} + ans.add_match_group('Entry points', {c: '' for c in namespaced_entry_points if c.startswith(prefix)}) else: if words[1] == 'kitten': if len(words) == 2 or (len(words) == 3 and not new_word): - ans.match_groups['Kittens'] = dict.fromkeys(k for k in all_kitten_names() if k.startswith('' if len(words) == 2 else words[2])) + ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith('' if len(words) == 2 else words[2]))) else: complete_kitten(ans, words[2], words[3:], new_word) return ans @@ -555,13 +566,13 @@ def find_completions(words: Sequence[str], new_word: bool, entry_points: Iterabl if len(words) == 1: if new_word: if words[0] == '+kitten': - ans.match_groups['Kittens'] = dict.fromkeys(all_kitten_names()) + ans.add_match_group('Kittens', all_kitten_names()) else: prefix = words[0] - ans.match_groups['Entry points'] = {c: '' for c in namespaced_entry_points if c.startswith(prefix)} + ans.add_match_group('Entry points', (c for c in namespaced_entry_points if c.startswith(prefix))) else: if len(words) == 2 and not new_word: - ans.match_groups['Kittens'] = dict.fromkeys(k for k in all_kitten_names() if k.startswith(words[1])) + ans.add_match_group('Kittens', (k for k in all_kitten_names() if k.startswith(words[1]))) else: if words[0] == '+kitten': complete_kitten(ans, words[1], words[2:], new_word)