Use the new completionspec for rc cmds

This commit is contained in:
Kovid Goyal 2022-09-16 15:05:20 +05:30
parent d84efe105c
commit 26d4f5bcc9
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 91 additions and 64 deletions

View File

@ -7,6 +7,7 @@ import shlex
import socket
import sys
from collections import deque
from enum import Enum, auto
from typing import (
Any, Callable, Dict, FrozenSet, Iterator, List, Match, Optional, Sequence,
Tuple, Type, TypeVar, Union, cast
@ -15,8 +16,8 @@ from typing import (
from .cli_stub import CLIOptions
from .conf.utils import resolve_config
from .constants import (
appname, clear_handled_signals, config_dir, defconf, is_macos, str_version,
website_url, default_pager_for_help
appname, clear_handled_signals, config_dir, default_pager_for_help,
defconf, is_macos, str_version, website_url
)
from .fast_data_types import wcswidth
from .options.types import Options as KittyOpts
@ -24,6 +25,55 @@ from .types import run_once
from .typing import BadLineType, TypedDict
class CompletionType(Enum):
file = auto()
directory = auto()
keyword = auto()
none = auto()
class CompletionRelativeTo(Enum):
cwd = auto()
config_dir = auto()
class CompletionSpec:
type: CompletionType = CompletionType.none
kwds: Sequence[str] = ()
extensions: Sequence[str] = ()
mime_patterns: Sequence[str] = ()
group: str = ''
relative_to: CompletionRelativeTo = CompletionRelativeTo.cwd
@staticmethod
def from_string(raw: str) -> 'CompletionSpec':
self = CompletionSpec()
for x in shlex.split(raw):
ck, vv = x.split(':', 1)
if ck == 'type':
self.type = getattr(CompletionType, vv)
elif ck == 'kwds':
self.kwds = tuple(vv.split(','))
elif ck == 'ext':
self.extensions = tuple(vv.split(','))
elif ck == 'group':
self.group = vv
elif ck == 'mime':
self.mime_patterns = tuple(vv.split(','))
elif ck == 'relative':
if vv == 'conf':
self.relative_to = CompletionRelativeTo.config_dir
else:
raise ValueError(f'Unknown completion relative to value: {vv}')
else:
raise KeyError(f'Unknown completion property: {ck}')
return self
def as_go_code(self, go_name: str) -> Iterator[str]:
pass
class OptionDict(TypedDict):
dest: str
name: str
@ -33,7 +83,7 @@ class OptionDict(TypedDict):
type: str
default: Optional[str]
condition: bool
completion: Dict[str, str]
completion: CompletionSpec
def serialize_as_go_string(x: str) -> str:
@ -343,7 +393,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
mpat = re.compile('([a-z]+)=(.+)')
current_cmd: OptionDict = {
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(),
'type': '', 'condition': False, 'default': None, 'completion': {}, 'name': ''
'type': '', 'condition': False, 'default': None, 'completion': CompletionSpec(), 'name': ''
}
empty_cmd = current_cmd
@ -364,7 +414,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
current_cmd = {
'dest': defdest, 'aliases': frozenset(parts), 'help': '',
'choices': frozenset(), 'type': '', 'name': defdest,
'default': None, 'condition': True, 'completion': {}
'default': None, 'condition': True, 'completion': CompletionSpec(),
}
state = METADATA
continue
@ -391,10 +441,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
elif k == 'condition':
current_cmd['condition'] = bool(eval(v))
elif k == 'completion':
cv = current_cmd['completion'] = {}
for x in shlex.split(v):
ck, vv = x.split(':', 1)
cv[ck] = vv
current_cmd['completion'] = CompletionSpec.from_string(v)
elif state is HELP:
if line:
current_indent = indent_of_line(line)

View File

@ -14,12 +14,13 @@ from kittens.runner import (
)
from .cli import (
OptionDict, options_for_completion, parse_option_spec, prettify
CompletionSpec, CompletionType, OptionDict, options_for_completion,
parse_option_spec, prettify
)
from .remote_control import global_options_spec
from .constants import config_dir, shell_integration_dir
from .fast_data_types import truncate_point_for_length, wcswidth
from .rc.base import all_command_names, command_for_name
from .rc.base import all_command_names
from .remote_control import global_options_spec
from .shell import options_for_cmd
from .types import run_once
from .utils import screen_size_function
@ -491,19 +492,7 @@ def global_options_for_remote_cmd() -> Dict[str, OptionDict]:
def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str], new_word: bool) -> None:
aliases, alias_map = options_for_cmd(cmd_name)
try:
args_completion = command_for_name(cmd_name).args.completion
except KeyError:
return
args_completer: CompleteArgsFunc = basic_option_arg_completer
if args_completion:
if 'files' in args_completion:
title, matchers = args_completion['files']
if isinstance(matchers, tuple):
args_completer = remote_files_completer(title, matchers)
elif 'names' in args_completion:
title, q = args_completion['names']
args_completer = remote_args_completer(title, q() if callable(q) else q)
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
@ -566,17 +555,17 @@ def complete_files_and_dirs(
ans.add_match_group(files_group_name, files, is_files=True)
def filter_files_from_completion_spec(spec: Dict[str, str]) -> Callable[['os.DirEntry[str]', str], bool]:
def filter_files_from_completion_spec(spec: CompletionSpec) -> Callable[['os.DirEntry[str]', str], bool]:
if 'ext' in spec:
extensions = frozenset(os.extsep + x.lower() for x in spec['ext'].split(','))
if spec.extensions:
extensions = frozenset(os.extsep + x.lower() for x in spec.extensions)
else:
extensions = frozenset()
if 'mime' in spec:
if spec.mime_patterns:
import re
from fnmatch import translate
mimes = tuple(re.compile(translate(x)) for x in spec['mime'].split(','))
mimes = tuple(re.compile(translate(x)) for x in spec.mime_patterns)
from .guess_mime_type import guess_type
else:
mimes = ()
@ -602,14 +591,12 @@ def filter_files_from_completion_spec(spec: Dict[str, str]) -> Callable[['os.Dir
return check_file
def complete_file_path(ans: Completions, spec: Dict[str, str], prefix: str, only_dirs: bool = False) -> None:
def complete_file_path(ans: Completions, spec: CompletionSpec, prefix: str, only_dirs: bool = False) -> None:
prefix = prefix.replace(r'\ ', ' ')
relative_to = spec.get('relative', '')
if relative_to:
if relative_to == 'conf':
relative_to = config_dir
else:
if spec.relative_to is spec.relative_to.__class__.cwd:
relative_to = os.getcwd()
else:
relative_to = config_dir
src_dir = relative_to
check_against = prefix
prefix_result_with = prefix
@ -641,26 +628,25 @@ def complete_file_path(ans: Completions, spec: Dict[str, str], prefix: str, only
if dirs:
ans.add_match_group('Directories', dirs, trailing_space=False, is_files=True)
if not only_dirs and files:
ans.add_match_group(spec.get('group') or 'Files', files, is_files=True)
ans.add_match_group(spec.group or 'Files', files, is_files=True)
def complete_path(ans: Completions, opt: OptionDict, prefix: str) -> None:
spec = opt['completion']
t = spec['type']
if 'kwds' in spec:
kwds = [x for x in spec['kwds'].split(',') if x.startswith(prefix)]
if spec.kwds:
kwds = [x for x in spec.kwds if x.startswith(prefix)]
if kwds:
ans.add_match_group('Keywords', kwds)
if t == 'file':
if spec.type is CompletionType.file:
complete_file_path(ans, spec, prefix)
elif t == 'directory':
elif spec.type is CompletionType.directory:
complete_file_path(ans, spec, prefix, only_dirs=True)
def complete_basic_option_args(ans: Completions, opt: OptionDict, prefix: str) -> None:
if opt['choices']:
ans.add_match_group(f'Choices for {opt["dest"]}', tuple(k for k in opt['choices'] if k.startswith(prefix)))
elif opt['completion'].get('type') in ('file', 'directory'):
elif opt['completion'].type is not CompletionType.none:
complete_path(ans, opt, prefix)

View File

@ -6,10 +6,10 @@ from contextlib import suppress
from dataclasses import dataclass
from typing import (
TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Iterable, Iterator,
List, NoReturn, Optional, Set, Tuple, Type, Union, cast, Mapping
List, NoReturn, Optional, Set, Tuple, Type, Union, cast
)
from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec
from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec, CompletionSpec
from kitty.cli_stub import RCOptions as R
from kitty.constants import appname, list_kitty_resources, running_in_kitty
from kitty.types import AsyncResponse
@ -88,7 +88,8 @@ CmdGenerator = Iterator[CmdReturnType]
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
PayloadGetType = PayloadGetter
ArgsType = List[str]
ImageCompletion: Dict[str, Tuple[str, Tuple[str, ...]]] = {'files': ('Images', ('*.png', '*.jpg', '*.jpeg', '*.webp', '*.gif', '*.bmp', '*.tiff'))}
ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')
ImageCompletion.extensions = 'png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'
MATCH_WINDOW_OPTION = '''\
@ -184,7 +185,7 @@ class ArgsHandling:
json_field: str = ''
count: Optional[int] = None
spec: str = ''
completion: Optional[Mapping[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None
completion: CompletionSpec = CompletionSpec()
value_if_unspecified: Tuple[str, ...] = ()
minimum_count: int = -1
first_rest: Optional[Tuple[str, str]] = None
@ -198,24 +199,11 @@ class ArgsHandling:
return self.count
def as_go_completion_code(self, go_name: str) -> Iterator[str]:
from ..cli import serialize_as_go_string
c = self.args_count
if c is not None:
yield f'{go_name}.Stop_processing_at_arg = {c}'
if self.completion:
for k, v in self.completion.items():
if k == 'files':
title, pats = v
assert isinstance(pats, tuple)
gpats = (f'"{serialize_as_go_string(x)}"' for x in pats)
yield f'{go_name}.Completion_for_arg = fnmatch_completer("{serialize_as_go_string(title)}", {", ".join(gpats)})'
elif k == 'names':
title, gen = v
assert callable(gen)
gpats = (f'"{serialize_as_go_string(x)}"' for x in gen())
yield f'{go_name}.Completion_for_arg = names_completer("{serialize_as_go_string(title)}", {", ".join(gpats)})'
else:
raise KeyError(f'Unknown args completion type: {k}')
yield from self.completion.as_go_code(go_name)
def as_go_code(self, cmd_name: str, field_types: Dict[str, str], handled_fields: Set[str]) -> Iterator[str]:
c = self.args_count
@ -313,6 +301,7 @@ class StreamInFlight:
class RemoteCommand:
Args = ArgsHandling
CompletionSpec = CompletionSpec
name: str = ''
short_desc: str = ''

View File

@ -30,7 +30,10 @@ class GotoLayout(RemoteCommand):
' You can use special match value :code:`all` to set the layout in all tabs.'
)
options_spec = MATCH_TAB_OPTION
args = RemoteCommand.Args(spec='LAYOUT_NAME', count=1, completion={'names': ('Layouts', layout_names)}, json_field='layout', args_choices=layout_names)
args = RemoteCommand.Args(
spec='LAYOUT_NAME', count=1, json_field='layout',
completion=RemoteCommand.CompletionSpec.from_string('type:keyword group:"Layout" kwds:' + ','.join(layout_names())),
args_choices=layout_names)
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if len(args) != 1:

View File

@ -89,8 +89,8 @@ type=bool-set
Restore all colors to the values they had at kitty startup. Note that if you specify
this option, any color arguments are ignored and :option:`kitty @ set-colors --configured` and :option:`kitty @ set-colors --all` are implied.
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
args = RemoteCommand.Args(spec='COLOR_OR_FILE ...', json_field='colors', special_parse='parse_colors_and_files(args)', completion={
'files': ('CONF files', ('*.conf',))})
args = RemoteCommand.Args(spec='COLOR_OR_FILE ...', json_field='colors', special_parse='parse_colors_and_files(args)',
completion=RemoteCommand.CompletionSpec.from_string('type:file group:"CONF files", ext:conf'))
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
final_colors: Dict[str, Optional[int]] = {}

View File

@ -42,7 +42,9 @@ Change the default enabled layout value so that the new value takes effect for a
as well.
'''
args = RemoteCommand.Args(
spec='LAYOUT ...', minimum_count=1, json_field='layouts', completion={'names': ('Layouts', layout_names)}, args_choices=layout_names)
spec='LAYOUT ...', minimum_count=1, json_field='layouts',
completion=RemoteCommand.CompletionSpec.from_string('type:keyword group:"Layout" kwds:' + ','.join(layout_names())),
args_choices=layout_names)
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if len(args) < 1: