Add $COMPLETION_MODE='menu-complete' to enable readline menu-complete (#3876)

* Add $COMPLETION_MODE='menu-complete' to enablereadline menu-complete -like behavor in completion.

* Tests

Co-authored-by: Bob Hyman <bob.hyman@bobssoftwareworks.com>
This commit is contained in:
Bob Hyman 2020-11-05 13:12:15 -08:00 committed by GitHub
parent 5d4745ca7b
commit b44dc70af7
Failed to generate hash of commit
5 changed files with 165 additions and 0 deletions

View file

@ -0,0 +1,28 @@
**Added:**
* Environment variable ``$COMPLETION_MODE`` controls kind of TAB completion used with prompt-toolkit shell.
``default``, the default, retains prior Xonsh behavior: first TAB displays the common prefix of matching completions,
next TAB selects the first or next available completion.
``menu-complete`` enables TAB behavior like ``readline`` command ``menu-complete``. First TAB selects the first matching
completion, subsequent TABs cycle through available completions till the last one. Next TAB after that displays
the common prefix, then the cycle repeats.
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -85,6 +85,10 @@ from xonsh.tools import (
all_permutations, all_permutations,
register_custom_style, register_custom_style,
simple_random_choice, simple_random_choice,
is_completion_mode,
to_completion_mode,
is_completions_display_value,
to_completions_display_value,
) )
from xonsh.environ import Env from xonsh.environ import Env
@ -1819,3 +1823,79 @@ def test_register_custom_style(name, styles, refrules):
for rule, color in style.styles.items(): for rule, color in style.styles.items():
if str(rule) in refrules: if str(rule) in refrules:
assert refrules[str(rule)] == color assert refrules[str(rule)] == color
@pytest.mark.parametrize(
"val, exp",
[
("default", True),
("menu-complete", True),
("def", False),
("xonsh", False),
("men", False),
],
)
def test_is_completion_mode(val, exp):
assert is_completion_mode(val) is exp
@pytest.mark.parametrize(
"val, exp",
[
("", "default"),
(None, "default"),
("default", "default"),
("DEfaULT", "default"),
("m", "menu-complete"),
("mEnu_COMPlete", "menu-complete"),
("menu-complete", "menu-complete"),
],
)
def test_to_completion_mode(val, exp):
assert to_completion_mode(val) == exp
@pytest.mark.parametrize("val", ["de", "defa_ult", "men_", "menu_",])
def test_to_completion_mode_fail(val):
with pytest.warns(RuntimeWarning):
obs = to_completion_mode(val)
assert obs == "default"
@pytest.mark.parametrize(
"val, exp",
[
("none", True),
("single", True),
("multi", True),
("", False),
(None, False),
("argle", False),
],
)
def test_is_completions_display_value(val, exp):
assert is_completions_display_value(val) == exp
@pytest.mark.parametrize(
"val, exp",
[
("none", "none"),
(False, "none"),
("false", "none"),
("single", "single"),
("readline", "single"),
("multi", "multi"),
(True, "multi"),
("TRUE", "multi"),
],
)
def test_to_completions_display_value(val, exp):
to_completions_display_value(val) == exp
@pytest.mark.parametrize("val", [1, "", "argle"])
def test_to_completions_display_value_fail(val):
with pytest.warns(RuntimeWarning):
obs = to_completions_display_value(val)
assert obs == "multi"

View file

@ -57,6 +57,8 @@ from xonsh.tools import (
is_string_or_callable, is_string_or_callable,
is_completions_display_value, is_completions_display_value,
to_completions_display_value, to_completions_display_value,
is_completion_mode,
to_completion_mode,
is_string_set, is_string_set,
csv_to_set, csv_to_set,
set_to_csv, set_to_csv,
@ -851,6 +853,17 @@ def DEFAULT_VARS():
"``$COMPLETIONS_DISPLAY`` is ``single`` or ``multi``. This only affects the " "``$COMPLETIONS_DISPLAY`` is ``single`` or ``multi``. This only affects the "
"prompt-toolkit shell.", "prompt-toolkit shell.",
), ),
"COMPLETION_MODE": Var(
is_completion_mode,
to_completion_mode,
str,
"default",
"Mode of tab completion in prompt-toolkit shell (only).\n\n"
"'default', the default, selects the common prefix of completions on first TAB,\n"
"then cycles through all completions.\n"
"'menu-complete' selects the first whole completion on the first TAB, \n"
"then cycles through the remaining completions, then the common prefix.",
),
"COMPLETION_QUERY_LIMIT": Var( "COMPLETION_QUERY_LIMIT": Var(
is_int, is_int,
int, int,

View file

@ -109,6 +109,12 @@ def tab_insert_indent():
return bool(before_cursor.isspace()) return bool(before_cursor.isspace())
@Condition
def tab_menu_complete():
"""Checks whether completion mode is `menu-complete`"""
return builtins.__xonsh__.env.get("COMPLETION_MODE") == "menu-complete"
@Condition @Condition
def beginning_of_line(): def beginning_of_line():
"""Check if cursor is at beginning of a line other than the first line in a """Check if cursor is at beginning of a line other than the first line in a
@ -206,6 +212,15 @@ def load_xonsh_bindings() -> KeyBindingsBase:
env = builtins.__xonsh__.env env = builtins.__xonsh__.env
event.cli.current_buffer.insert_text(env.get("INDENT")) event.cli.current_buffer.insert_text(env.get("INDENT"))
@handle(Keys.Tab, filter=~tab_insert_indent & tab_menu_complete)
def menu_complete_select(event):
"""Start completion in menu-complete mode, or tab to next completion"""
b = event.current_buffer
if b.complete_state:
b.complete_next()
else:
b.start_completion(select_first=True)
@handle(Keys.ControlX, Keys.ControlE, filter=~has_selection) @handle(Keys.ControlX, Keys.ControlE, filter=~has_selection)
def open_editor(event): def open_editor(event):
""" Open current buffer in editor """ """ Open current buffer in editor """

View file

@ -1617,10 +1617,12 @@ def ptk2_color_depth_setter(x):
def is_completions_display_value(x): def is_completions_display_value(x):
"""Enumerated values of ``$COMPLETIONS_DISPLAY``"""
return x in {"none", "single", "multi"} return x in {"none", "single", "multi"}
def to_completions_display_value(x): def to_completions_display_value(x):
"""Convert user input to value of ``$COMPLETIONS_DISPLAY``"""
x = str(x).lower() x = str(x).lower()
if x in {"none", "false"}: if x in {"none", "false"}:
x = "none" x = "none"
@ -1636,6 +1638,33 @@ def to_completions_display_value(x):
return x return x
CANONIC_COMPLETION_MODES = frozenset({"default", "menu-complete"})
def is_completion_mode(x):
"""Enumerated values of $COMPLETION_MODE"""
return x in CANONIC_COMPLETION_MODES
def to_completion_mode(x):
"""Convert user input to value of $COMPLETION_MODE"""
y = str(x).casefold().replace("_", "-")
y = (
"default"
if y in ("", "d", "xonsh", "none", "def")
else "menu-complete"
if y in ("m", "menu", "menu-completion")
else y
)
if y not in CANONIC_COMPLETION_MODES:
warnings.warn(
f"'{x}' is not valid for $COMPLETION_MODE, must be one of {CANONIC_COMPLETION_MODES}. Using 'default'.",
RuntimeWarning,
)
y = "default"
return y
def is_str_str_dict(x): def is_str_str_dict(x):
"""Tests if something is a str:str dictionary""" """Tests if something is a str:str dictionary"""
return isinstance(x, dict) and all( return isinstance(x, dict) and all(