Argparser/color+completion (#4391)

* feat: add colored help formatter and alias builder

closes #4284

* feat: add auto-completion support to argparser

* test: update test for completer

* fix: getting doc from params that have annotation

* refactor: use filter-function for checking alias completions

* doc: add discussion abount check for alias having parser

see discussion on
https://github.com/xonsh/xonsh/pull/4267

* type fix

* refactor: use function based completer

* test: fix failing argparser test

* docs: update news item

* update completion for argparser sub-commands to append_space

from comment on
https://github.com/xonsh/xonsh/pull/4267#discussion_r676044508

* docs: update docstring typo

* refactor: move inspect import to top

* feat: support option strings before positionals

and add env setting for showing completions for options by default

* test: update tests after adding new $ALIAS_COMPLETIONS_OPTIONS_BY_DEFAULT

* add suggested completion_context_parse fixture

* docs: add suggested doc for dispatch function

* refactor: use try/except for import of typing.annotated

* refactor: move complete_argparser_aliases to completers/aliases.py

* refactor: move argparser completer to its own module

* style:

* refactor: rename completer to not clash with argparse

* fix: expand option's descriptions

* fix: add completer/argparser to amalgam
This commit is contained in:
Noorhteen Raja NJ 2021-08-26 04:02:13 +05:30 committed by GitHub
parent abc98c1bb3
commit 54f5ae7bb2
Failed to generate hash of commit
11 changed files with 690 additions and 137 deletions

View file

@ -0,0 +1,24 @@
**Added:**
* added new utility classes ``xonsh.cli_utils.ArgParserAlias``, ``xonsh.cli_utils.ArgCompleter``.
These are helper classes, that add coloring and auto-completion support to the alias-commands.
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -1,34 +1,56 @@
from xonsh.parsers.completion_context import (
CommandArg,
CommandContext,
CompletionContext,
import pytest
@pytest.fixture
def xsh_with_aliases(xession, monkeypatch):
from xonsh.aliases import Aliases, make_default_aliases
xsh = xession
monkeypatch.setattr(xsh, "aliases", Aliases(make_default_aliases()))
return xsh
@pytest.fixture
def mock_completer(monkeypatch, xsh_with_aliases):
xsh = xsh_with_aliases
monkeypatch.setattr(xsh, "completers", {"one": 1, "two": 2})
monkeypatch.setattr(xsh, "ctx", {"three": lambda: 1, "four": lambda: 2})
return xsh
@pytest.mark.parametrize(
"args, positionals, options",
[
("completer", {"add", "remove", "rm", "list", "ls"}, {"--help", "-h"}),
(
"completer add",
set(),
{"--help", "-h"},
),
(
"completer add newcompleter",
{"three", "four"},
{"--help", "-h"},
),
(
"completer add newcompleter three",
{"<one", ">two", ">one", "<two", "end", "start"},
{"--help", "-h"},
),
(
"completer remove",
{"one", "two"},
{"--help", "-h"},
),
(
"completer list",
set(),
{"--help", "-h"},
),
],
)
from xonsh.completers.completer import complete_completer
def test_completer_command(args, positionals, options, mock_completer, check_completer):
assert check_completer(args) == positionals
def test_options():
assert (
complete_completer(
CompletionContext(
CommandContext(
args=(CommandArg("completer"),),
arg_index=1,
)
)
)
== {"add", "remove", "list", "help"}
)
def test_help_options():
assert (
complete_completer(
CompletionContext(
CommandContext(
args=(CommandArg("completer"), CommandArg("help")),
arg_index=2,
)
)
)
== {"add", "remove", "list"}
)
mock_completer.env["ALIAS_COMPLETIONS_OPTIONS_BY_DEFAULT"] = True
assert check_completer(args) == positionals.union(options)

View file

@ -9,11 +9,17 @@ import pytest
from xonsh.aliases import Aliases
from xonsh.built_ins import XonshSession, XSH
from xonsh.completers._aliases import complete_argparser_aliases
from xonsh.execer import Execer
from xonsh.jobs import tasks
from xonsh.events import events
from xonsh.platform import ON_WINDOWS
from xonsh.parsers.completion_context import CompletionContextParser
from xonsh.parsers.completion_context import (
CommandArg,
CommandContext,
CompletionContext,
)
from xonsh import commands_cache
from tools import DummyShell, sp, DummyEnv, DummyHistory
@ -140,6 +146,19 @@ def completion_context_parse():
return CompletionContextParser().parse
@pytest.fixture
def check_completer(xession, completion_context_parse):
def _factory(args, **kwargs):
cmds = tuple(CommandArg(i) for i in args.split(" "))
arg_index = len(cmds)
completions = complete_argparser_aliases(
CompletionContext(CommandContext(args=cmds, arg_index=arg_index, **kwargs))
)
return {getattr(i, "value", i) for i in completions}
return _factory
@pytest.fixture
def ptk_shell(xonsh_execer):
from prompt_toolkit.input import create_pipe_input

View file

@ -31,5 +31,22 @@ def test_get_doc_param():
]
assert cli_utils.get_doc(func_with_doc, "multi").splitlines() == [
"param doc",
" multi line",
"multi line",
]
def test_generated_parser():
from xonsh.completers._aliases import CompleterAlias
alias = CompleterAlias()
assert alias.parser.description
positionals = alias.parser._get_positional_actions()
add_cmd = positionals[0].choices["add"]
assert "Add a new completer" in add_cmd.description
assert (
alias.parser.format_usage()
== "usage: completer [-h] {add,remove,rm,list,ls} ...\n"
)
assert add_cmd.format_usage() == "usage: completer add [-h] name func [pos]\n"

View file

@ -1,11 +1,61 @@
"""
small functions to create argparser CLI from functions.
helper functions and classes to create argparse CLI from functions.
Examples
please see :py:class:`xonsh.completers.completer.CompleterAlias` class
"""
import argparse as ap
import inspect
import os
import sys
import typing as tp
TYPING_ANNOTATED_AVAILABLE = False
"""One can import ``Annotated`` from this module
which adds a stub when it is not available in ``typing``/``typing_extensions`` modules."""
try:
from typing import Annotated # noqa
TYPING_ANNOTATED_AVAILABLE = True
except ImportError:
try:
from typing_extensions import Annotated # type: ignore
TYPING_ANNOTATED_AVAILABLE = True
except ImportError:
T = tp.TypeVar("T") # Declare type variable
class _AnnotatedMeta(type):
def __getitem__(self, item: tp.Tuple[T, tp.Any]) -> T:
if tp.TYPE_CHECKING:
return item[0]
return item[1]
class Annotated(metaclass=_AnnotatedMeta): # type: ignore
pass
class ArgCompleter:
"""Gives a structure to the argparse completers"""
def __call__(self, **kwargs):
"""return dynamic completers for the given action."""
raise NotImplementedError
def Arg(
*args: str,
completer: tp.Union[ArgCompleter, tp.Callable[..., tp.Iterator[str]]] = None,
**kwargs,
):
# converting to tuple because of limitation with hashing args in py3.6
# after dropping py36 support, the dict can be returned
kwargs["completer"] = completer
return args, tuple(kwargs.items())
def _get_func_doc(doc: str) -> str:
lines = doc.splitlines()
@ -15,34 +65,33 @@ def _get_func_doc(doc: str) -> str:
return os.linesep.join(lines)
def _from_index_of(container: tp.Sequence[str], key: str):
if key in container:
idx = container.index(key)
if idx + 1 < len(container):
return container[idx + 1 :]
return []
def _get_param_doc(doc: str, param: str) -> tp.Iterator[str]:
section_title = "\nParameters"
if section_title not in doc:
return
_, doc = doc.split(section_title)
started = False
for lin in doc.splitlines():
if not lin:
continue
if lin.startswith(param):
started = True
continue
if not started:
continue
def _get_param_doc(doc: str, param: str) -> str:
lines = tuple(doc.splitlines())
if "Parameters" not in lines:
return ""
par_doc = []
for lin in _from_index_of(lines, param):
if lin and not lin.startswith(" "):
if not lin.startswith(" "): # new section/parameter
break
par_doc.append(lin)
return os.linesep.join(par_doc).strip()
yield lin
def get_doc(func: tp.Callable, parameter: str = None):
def get_doc(func: tp.Union[tp.Callable, str], parameter: str = None):
"""Parse the function docstring and return its help content
Parameters
----------
func
a callable object that holds docstring
a callable/object that holds docstring
parameter
name of the function parameter to parse doc for
@ -51,58 +100,294 @@ def get_doc(func: tp.Callable, parameter: str = None):
str
doc of the parameter/function
"""
import inspect
if isinstance(func, str):
return func
doc = inspect.getdoc(func) or ""
if parameter:
return _get_param_doc(doc, parameter)
par_doc = os.linesep.join(_get_param_doc(doc, parameter))
return inspect.cleandoc(par_doc).strip()
else:
return _get_func_doc(doc)
return _get_func_doc(doc).strip()
_FUNC_NAME = "_func_"
def _get_args_kwargs(annot: tp.Any) -> tp.Tuple[tp.Sequence[str], tp.Dict[str, tp.Any]]:
args, kwargs = [], {}
if isinstance(annot, tuple):
args, kwargs = annot
elif TYPING_ANNOTATED_AVAILABLE and "Annotated[" in str(annot):
if hasattr(annot, "__metadata__"):
args, kwargs = annot.__metadata__[0]
else:
from typing_extensions import get_args
_, (args, kwargs) = get_args(annot)
if isinstance(kwargs, tuple):
kwargs = dict(kwargs)
return args, kwargs
def add_args(parser: ap.ArgumentParser, func: tp.Callable, allowed_params=None) -> None:
"""Using the function's annotation add arguments to the parser
param:Arg(*args, **kw) -> parser.add_argument(*args, *kw)
"""
# call this function when this sub-command is selected
parser.set_defaults(**{_FUNC_NAME: func})
sign = inspect.signature(func)
for name, param in sign.parameters.items():
if name.startswith("_") or (
allowed_params is not None and name not in allowed_params
):
continue
args, kwargs = _get_args_kwargs(param.annotation)
if args: # optional argument. eg. --option
kwargs.setdefault("dest", name)
else: # positional argument
args = [name]
if inspect.Parameter.empty != param.default:
kwargs.setdefault("default", param.default)
# help can be set by passing help argument otherwise inferred from docstring
kwargs.setdefault("help", get_doc(func, name))
completer = kwargs.pop("completer", None)
action = parser.add_argument(*args, **kwargs)
if completer:
action.completer = completer # type: ignore
action.help = action.help or ""
if action.default and "%(default)s" not in action.help:
action.help += os.linesep + " (default: %(default)s)"
if action.type and "%(type)s" not in action.help:
action.help += " (type: %(type)s)"
def make_parser(
func: tp.Callable,
subparser: ap._SubParsersAction = None,
params: tp.Dict[str, tp.Dict[str, tp.Any]] = None,
**kwargs
) -> "ap.ArgumentParser":
func: tp.Union[tp.Callable, str],
empty_help=True,
**kwargs,
) -> "ArgParser":
"""A bare-bones argparse builder from functions"""
doc = get_doc(func)
kwargs.setdefault("formatter_class", ap.RawTextHelpFormatter)
if subparser is None:
kwargs.setdefault("description", doc)
parser = ap.ArgumentParser(**kwargs)
if "description" not in kwargs:
kwargs["description"] = get_doc(func)
parser = ArgParser(**kwargs)
if empty_help:
parser.set_defaults(
**{_FUNC_NAME: lambda stdout: parser.print_help(file=stdout)}
**{_FUNC_NAME: lambda stdout=None: parser.print_help(file=stdout)}
)
return parser
else:
parser = subparser.add_parser(
kwargs.pop("prog", func.__name__),
help=doc,
**kwargs,
)
parser.set_defaults(**{_FUNC_NAME: func})
return parser
if params:
for par, args in params.items():
args.setdefault("help", get_doc(func, par))
parser.add_argument(par, **args)
class RstHelpFormatter(ap.RawTextHelpFormatter):
"""Highlight help string as rst"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from pygments.formatters.terminal import TerminalFormatter
self.formatter = TerminalFormatter()
def start_section(self, heading) -> None:
from pygments.token import Generic
heading = self.colorize((Generic.Heading, heading))
return super().start_section(heading)
def _get_help_string(self, action) -> str:
return self.markup_rst(action.help)
def colorize(self, *tokens: tuple) -> str:
from pygments import format
return format(tokens, self.formatter)
def markup_rst(self, text):
from pygments import highlight
from pygments.lexers.markup import RstLexer
return highlight(text, RstLexer(), self.formatter)
def _format_text(self, text):
text = super()._format_text(text)
if text:
text = self.markup_rst(text)
return text
def _format_usage(self, usage, actions, groups, prefix):
from pygments.token import Name, Generic
text = super()._format_usage(usage, actions, groups, prefix)
parts = text.split(self._prog, maxsplit=1)
if len(parts) == 2 and all(parts):
text = self.colorize(
(Generic.Heading, parts[0]),
(Name.Function, self._prog),
(Name.Attribute, parts[1]), # from _format_actions_usage
)
return text
def _format_action_invocation(self, action):
from pygments.token import Name
text = super()._format_action_invocation(action)
return self.colorize((Name.Attribute, text))
def get_argparse_formatter_class():
from xonsh.built_ins import XSH
from xonsh.platform import HAS_PYGMENTS
if (
hasattr(sys, "stderr")
and sys.stderr.isatty()
and XSH.env.get("XONSH_INTERACTIVE")
and HAS_PYGMENTS
):
return RstHelpFormatter
return ap.RawTextHelpFormatter
class ArgParser(ap.ArgumentParser):
"""Sub-class of ArgumentParser with special methods to nest commands"""
def __init__(self, **kwargs):
if "formatter_class" not in kwargs:
kwargs["formatter_class"] = get_argparse_formatter_class()
super().__init__(**kwargs)
self.commands = None
def add_command(
self, func: tp.Callable, args: tp.Optional[tp.Iterable[str]] = None, **kwargs
):
"""
create a sub-parser and call this function during dispatch
Parameters
----------
func
a type-annotated function that will be used to create ArgumentParser instance.
All parameters that start with ``_`` will not be added to parser arguments.
Use _stdout, _stack ... to receive them from callable-alias/commands.
Use _parser to get the generated parser instance.
Use _args to get what is passed from sys.argv
Use _parsed to get result of ``parser.parse_args``
args
if given only add these arguments to the parser.
Otherwise all parameters to the function without `_` prefixed
in their name gets added to the parser.
kwargs
passed to ``subparser.add_parser`` call
Returns
-------
result from ``subparser.add_parser``
"""
if not self.commands:
self.commands = self.add_subparsers(title="commands", dest="command")
doc = get_doc(func)
kwargs.setdefault("description", doc)
kwargs.setdefault("help", doc)
parser = self.commands.add_parser(kwargs.pop("prog", func.__name__), **kwargs)
add_args(parser, func, allowed_params=args)
return parser
def dispatch(**ns):
def dispatch(parser: ap.ArgumentParser, args=None, **ns):
"""call the sub-command selected by user"""
import inspect
parsed = parser.parse_args(args)
ns["_parsed"] = parsed
ns.update(vars(parsed))
func = ns[_FUNC_NAME]
sign = inspect.signature(func)
kwargs = {}
for name, _ in sign.parameters.items():
kwargs[name] = ns[name]
for name, param in sign.parameters.items():
default = None
# sometimes the args are skipped in the parser.
# like ones having _ prefix(private to the function), or some special cases like exclusive group.
# it is better to fill the defaults from paramspec when available.
if param.default != inspect.Parameter.empty:
default = param.default
kwargs[name] = ns.get(name, default)
return func(**kwargs)
class ArgParserAlias:
"""Provides a structure to the Alias. The parser is lazily loaded.
can help create ``argparse.ArgumentParser`` parser from function
signature and dispatch the functions.
Examples
---------
For usage please check ``xonsh.completers.completer.py`` module.
"""
def __init__(self, threadable=True, **kwargs) -> None:
if not threadable:
from xonsh.tools import unthreadable
unthreadable(self)
self._parser = None
self.kwargs = kwargs
def build(self):
"""Sub-classes should return constructed ArgumentParser"""
if self.kwargs:
return self.create_parser(**self.kwargs)
raise NotImplementedError
@property
def parser(self):
if self._parser is None:
self._parser = self.build()
return self._parser
def create_parser(self, func=None, has_args=False, allowed_params=None, **kwargs):
"""create root parser"""
func = func or self
has_args = has_args or bool(allowed_params)
if has_args:
kwargs.setdefault("empty_help", False)
parser = make_parser(func, **kwargs)
if has_args:
add_args(parser, func, allowed_params=allowed_params)
return parser
def __call__(
self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
):
return dispatch(
self.parser,
args,
_parser=self.parser,
_args=args,
_stdin=stdin,
_stdout=stdout,
_stderr=stderr,
_spec=spec,
_stack=stack,
)
__all__ = (
"Arg",
"ArgParserAlias",
"Annotated",
"ArgParser",
"make_parser",
"add_args",
"get_doc",
"dispatch",
)

View file

@ -13,6 +13,8 @@ else:
_sys.modules["xonsh.completers.bash_completion"] = __amalgam__
tools = __amalgam__
_sys.modules["xonsh.completers.tools"] = __amalgam__
argparser = __amalgam__
_sys.modules["xonsh.completers.argparser"] = __amalgam__
commands = __amalgam__
_sys.modules["xonsh.completers.commands"] = __amalgam__
completer = __amalgam__

View file

@ -8,6 +8,12 @@ from xonsh.completers.completer import (
remove_completer,
add_one_completer,
)
from xonsh.completers.argparser import complete_argparser
from xonsh.completers.tools import (
contextual_command_completer,
get_filter_function,
)
from xonsh.parsers.completion_context import CommandContext
# for backward compatibility
_add_one_completer = add_one_completer
@ -18,8 +24,30 @@ def _remove_completer(args):
return remove_completer(args[0])
def _register_completer(name: str, func: str, pos="start", stack=None):
"""adds a new completer to xonsh
def complete_func_name_choices(xsh, **_):
"""Return all callable names in the current context"""
for i, j in xsh.ctx.items():
if callable(j):
yield i
def complete_completer_pos_choices(xsh, **_):
"""Compute possible positions for the new completer"""
yield from {"start", "end"}
for k in xsh.completers.keys():
yield ">" + k
yield "<" + k
def _register_completer(
name: str,
func: xcli.Annotated[str, xcli.Arg(completer=complete_func_name_choices)],
pos: xcli.Annotated[
str, xcli.Arg(completer=complete_completer_pos_choices, nargs="?")
] = "start",
_stack=None,
):
"""Add a new completer to xonsh
Parameters
----------
@ -48,7 +76,6 @@ def _register_completer(name: str, func: str, pos="start", stack=None):
be added after the completer named KEY
* "<KEY", where KEY is a pre-existing name, indicates that this should
be added before the completer named KEY
(Default value: "start")
"""
err = None
func_name = func
@ -61,7 +88,7 @@ def _register_completer(name: str, func: str, pos="start", stack=None):
if not callable(func):
err = f"{func_name} is not callable"
else:
for frame_info in stack:
for frame_info in _stack:
frame = frame_info[0]
if func_name in frame.f_locals:
func = frame.f_locals[func_name]
@ -104,8 +131,41 @@ def _parser() -> ap.ArgumentParser:
return parser
def completer_alias(args, stdin=None, stdout=None, stderr=None, spec=None, stack=None):
class CompleterAlias(xcli.ArgParserAlias):
"""CLI to add/remove/list xonsh auto-complete functions"""
ns = _parser.parse_args(args)
kwargs = vars(ns)
return xcli.dispatch(**kwargs, stdin=stdin, stdout=stdout, stack=stack)
def build(self):
parser = self.create_parser(prog="completer")
parser.add_command(_register_completer, prog="add")
parser.add_command(remove_completer, prog="remove", aliases=["rm"])
parser.add_command(list_completers, prog="list", aliases=["ls"])
return parser
completer_alias = CompleterAlias()
@contextual_command_completer
def complete_argparser_aliases(command: CommandContext):
"""Completer for any alias command that has ``argparser`` in ``parser`` attribute"""
if not command.args:
return
cmd = command.args[0].value
alias = XSH.aliases.get(cmd) # type: ignore
# todo: checking isinstance(alias, ArgParserAlias) fails when amalgamated.
# see https://github.com/xonsh/xonsh/pull/4267#discussion_r676066853
if not hasattr(alias, "parser"):
return
if command.suffix:
# completing in a middle of a word
# (e.g. "completer some<TAB>thing")
return
possible = complete_argparser(alias.parser, command=command, alias=alias)
fltr = get_filter_function()
for comp in possible:
if fltr(comp, command.prefix):
yield comp

View file

@ -0,0 +1,149 @@
import argparse as ap
import typing as tp
from xonsh.built_ins import XSH
from xonsh.completers.tools import RichCompletion
from xonsh.parsers.completion_context import CommandContext
class ArgparseCompleter:
"""A completer function for ArgParserAlias commands"""
def __init__(self, parser: ap.ArgumentParser, command: CommandContext, **kwargs):
args = tuple(c.value for c in command.args[: command.arg_index])
self.parser, self.remaining_args = self.get_parser(parser, args[1:])
self.command = command
kwargs["command"] = command
self.kwargs = kwargs
"""will be sent to completer function"""
@staticmethod
def get_parser(parser, args) -> tp.Tuple[ap.ArgumentParser, tp.Tuple[str, ...]]:
"""Check for sub-parsers"""
sub_parsers = {}
for act in parser._get_positional_actions():
if act.nargs == ap.PARSER:
sub_parsers = act.choices # there should be only one subparser
if sub_parsers:
for idx, pos in enumerate(args):
if pos in sub_parsers:
# get the correct parser
return ArgparseCompleter.get_parser(
sub_parsers[pos], args[idx + 1 :]
)
# base scenario
return parser, args
def filled(self, act: ap.Action) -> int:
"""Consume remaining_args for the given action"""
args_len = 0
for arg in self.remaining_args:
if arg and arg[0] in self.parser.prefix_chars:
# stop when other --option explicitly given
break
args_len += 1
nargs = (
act.nargs
if isinstance(act.nargs, int)
else args_len + 1
if act.nargs in {ap.ONE_OR_MORE, ap.ZERO_OR_MORE}
else 1
)
if len(self.remaining_args) >= nargs:
# consume n-number of args
self.remaining_args = self.remaining_args[nargs:]
# complete for next action
return True
return False
def _complete(self, act: ap.Action, **kwargs):
if act.choices:
yield from act.choices
elif hasattr(act, "completer") and callable(act.completer): # type: ignore
# call the completer function
from xonsh.built_ins import XSH
kwargs.update(self.kwargs)
yield from act.completer(xsh=XSH, action=act, completer=self, **kwargs) # type: ignore
def _complete_pos(self, act):
if isinstance(act.choices, dict): # sub-parsers
for choice, sub_parser in act.choices.items():
yield RichCompletion(
choice,
description=sub_parser.description or "",
append_space=True,
)
else:
yield from self._complete(act)
def complete(self):
# options will come before/after positionals
options = {act: None for act in self.parser._get_optional_actions()}
# remove options that are already filled
opt_completions = self._complete_options(options)
if opt_completions:
yield from opt_completions
return
for act in self.parser._get_positional_actions():
# number of arguments it consumes
if self.filled(act):
continue
yield from self._complete_pos(act)
# close after a valid positional arg completion
break
opt_completions = self._complete_options(options)
if opt_completions:
yield from opt_completions
return
# complete remaining options only if requested or enabled
show_opts = XSH.env.get("ALIAS_COMPLETIONS_OPTIONS_BY_DEFAULT", False)
if not show_opts:
if not (
self.command.prefix
and self.command.prefix[0] in self.parser.prefix_chars
):
return
# in the end after positionals show remaining unfilled options
for act in options:
for flag in act.option_strings:
desc = ""
if act.help:
formatter = self.parser._get_formatter()
try:
desc = formatter._expand_help(act)
except KeyError:
desc = act.help
yield RichCompletion(flag, description=desc)
def _complete_options(self, options):
while self.remaining_args:
arg = self.remaining_args[0]
act_res = self.parser._parse_optional(arg)
if not act_res:
# it is not a option string: pass
break
# it is a valid option and advance
self.remaining_args = self.remaining_args[1:]
act, _, value = act_res
# remove the found option
# todo: not remove if append/extend
options.pop(act, None)
if self.filled(act):
continue
# stop suggestion until current option is complete
return self._complete(act)
def complete_argparser(parser, command: CommandContext, **kwargs):
completer = ArgparseCompleter(parser, command=command, **kwargs)
yield from completer.complete()

View file

@ -1,53 +1,14 @@
import collections
from xonsh.parsers.completion_context import CommandContext
from xonsh.built_ins import XSH
from xonsh.cli_utils import Arg, Annotated, get_doc
from xonsh.completers.tools import (
contextual_command_completer_for,
justify,
is_exclusive_completer,
RichCompletion,
)
@contextual_command_completer_for("completer")
def complete_completer(command: CommandContext):
"""
Completion for "completer"
"""
if command.suffix:
# completing in a middle of a word
# (e.g. "completer some<TAB>thing")
return None
curix = command.arg_index
compnames = set(XSH.completers.keys())
if curix == 1:
possible = {"list", "help", "add", "remove"}
elif curix == 2:
first_arg = command.args[1].value
if first_arg == "help":
possible = {"list", "add", "remove"}
elif first_arg == "remove":
possible = compnames
else:
raise StopIteration
else:
if command.args[1].value != "add":
raise StopIteration
if curix == 3:
possible = {i for i, j in XSH.ctx.items() if callable(j)}
elif curix == 4:
possible = (
{"start", "end"}
| {">" + n for n in compnames}
| {"<" + n for n in compnames}
)
else:
raise StopIteration
return {i for i in possible if i.startswith(command.prefix)}
def add_one_completer(name, func, loc="end"):
new = collections.OrderedDict()
if loc == "start":
@ -108,8 +69,16 @@ def list_completers():
return o + "\n".join(_strs) + "\n"
def remove_completer(name: str):
"""removes a completer from xonsh
def complete_completer_names(xsh, **_):
"""Complete all loaded completer names"""
for name, comp in xsh.completers.items():
yield RichCompletion(name, description=get_doc(comp))
def remove_completer(
name: Annotated[str, Arg(completer=complete_completer_names)],
):
"""Removes a completer from xonsh
Parameters
----------

View file

@ -16,8 +16,8 @@ from xonsh.completers.commands import (
complete_end_proc_tokens,
complete_end_proc_keywords,
)
from xonsh.completers.completer import complete_completer
from xonsh.completers.xompletions import complete_xonfig, complete_xontrib
from xonsh.completers._aliases import complete_argparser_aliases
from xonsh.completers.environment import complete_environment_vars
@ -31,8 +31,8 @@ def default_completers():
("environment_vars", complete_environment_vars),
# exclusive completers:
("base", complete_base),
("completer", complete_completer),
("skip", complete_skipper),
("argparser_aliases", complete_argparser_aliases),
("pip", complete_pip),
("cd", complete_cd),
("rmdir", complete_rmdir),

View file

@ -1577,6 +1577,12 @@ class AsyncPromptSetting(PTKSetting):
class AutoCompletionSetting(Xettings):
"""Tab-completion behavior."""
ALIAS_COMPLETIONS_OPTIONS_BY_DEFAULT = Var.with_default(
doc="If True, Argparser based alias completions will show options (e.g. -h, ...) without "
"requesting explicitly with option prefix (-).",
default=False,
type_str="bool",
)
BASH_COMPLETIONS = Var.with_default(
doc="This is a list (or tuple) of strings that specifies where the "
"``bash_completion`` script may be found. "