Skip to content

Commit 8760840

Browse files
committed
feat(cmd2): improve completion menu and subcommand handling
Switched PromptSession to CompleteStyle.COLUMN and disabled completion while typing to ensure the menu appears on the first Tab press with descriptions. Updated ArgparseCompleter to return CompletionItems for subcommands and main commands, allowing their descriptions to appear in the prompt-toolkit menu instead of a redundant hint table above the prompt. Restored printing of hint tables for other argument types.
1 parent 7c8db71 commit 8760840

File tree

3 files changed

+62
-12
lines changed

3 files changed

+62
-12
lines changed

cmd2/argparse_completer.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,12 @@ def _format_completions(self, arg_state: _ArgumentState, completions: list[str]
538538

539539
# Check if there are too many CompletionItems to display as a table
540540
if len(completions) <= self._cmd2_app.max_completion_items:
541+
# Skip generating hint table for subcommands or the main command completion
542+
if isinstance(arg_state.action, argparse._SubParsersAction) or (
543+
arg_state.action.metavar == "COMMAND" and arg_state.action.dest == "command"
544+
):
545+
return cast(list[str], completions)
546+
541547
# If a metavar was defined, use that instead of the dest field
542548
destination = arg_state.action.metavar if arg_state.action.metavar else arg_state.action.dest
543549

@@ -659,9 +665,27 @@ def _complete_arg(
659665
:raises CompletionError: if the completer or choices function this calls raises one.
660666
"""
661667
# Check if the arg provides choices to the user
662-
arg_choices: list[str] | ChoicesCallable
668+
arg_choices: list[str] | list[CompletionItem] | ChoicesCallable
663669
if arg_state.action.choices is not None:
664-
arg_choices = list(arg_state.action.choices)
670+
if isinstance(arg_state.action, argparse._SubParsersAction):
671+
arg_choices_items: list[CompletionItem] = []
672+
# Map parser object to help text using _choices_actions (which contains canonical commands)
673+
parser_help = {}
674+
for action in arg_state.action._choices_actions:
675+
# Retrieve the parser corresponding to this action
676+
# action.dest is the canonical name
677+
if action.dest in arg_state.action.choices:
678+
subparser = arg_state.action.choices[action.dest]
679+
parser_help[subparser] = action.help if action.help else ''
680+
681+
# Iterate over all choices (including aliases)
682+
for name, subparser in arg_state.action.choices.items():
683+
help_text = parser_help.get(subparser, '')
684+
arg_choices_items.append(CompletionItem(name, [help_text]))
685+
arg_choices = arg_choices_items
686+
else:
687+
arg_choices = list(arg_state.action.choices)
688+
665689
if not arg_choices:
666690
return []
667691

@@ -716,7 +740,7 @@ def _complete_arg(
716740
# Otherwise use basic_complete on the choices
717741
else:
718742
# Check if the choices come from a function
719-
completion_items: list[str] = []
743+
completion_items: list[str] | list[CompletionItem] = []
720744
if isinstance(arg_choices, ChoicesCallable):
721745
if not arg_choices.is_completer:
722746
choices_func = arg_choices.choices_provider

cmd2/cmd2.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,9 @@ def __init__(
445445
self.session: PromptSession[str] = PromptSession(
446446
history=self.history_adapter,
447447
completer=self.completer,
448-
complete_style=CompleteStyle.READLINE_LIKE,
448+
complete_style=CompleteStyle.COLUMN,
449449
complete_in_thread=True,
450+
complete_while_typing=False,
450451
)
451452
except (NoConsoleScreenBufferError, AttributeError, ValueError):
452453
# Fallback to dummy input/output if PromptSession initialization fails.
@@ -457,8 +458,9 @@ def __init__(
457458
completer=self.completer,
458459
input=DummyInput(),
459460
output=DummyOutput(),
460-
complete_style=CompleteStyle.READLINE_LIKE,
461+
complete_style=CompleteStyle.COLUMN,
461462
complete_in_thread=True,
463+
complete_while_typing=False,
462464
)
463465

464466
# Commands to exclude from the history command
@@ -2519,12 +2521,26 @@ def _get_settable_completion_items(self) -> list[CompletionItem]:
25192521

25202522
return results
25212523

2522-
def _get_commands_aliases_and_macros_for_completion(self) -> list[str]:
2524+
def _get_commands_aliases_and_macros_for_completion(self) -> list[CompletionItem]:
25232525
"""Return a list of visible commands, aliases, and macros for tab completion."""
2524-
visible_commands = set(self.get_visible_commands())
2525-
alias_names = set(self.aliases)
2526-
macro_names = set(self.macros)
2527-
return list(visible_commands | alias_names | macro_names)
2526+
results: list[CompletionItem] = []
2527+
2528+
# Add commands
2529+
for command in self.get_visible_commands():
2530+
# Get the command method
2531+
func = getattr(self, constants.COMMAND_FUNC_PREFIX + command)
2532+
description = strip_doc_annotations(func.__doc__).splitlines()[0] if func.__doc__ else ''
2533+
results.append(CompletionItem(command, [description]))
2534+
2535+
# Add aliases
2536+
for name, value in self.aliases.items():
2537+
results.append(CompletionItem(name, [f"Alias for: {value}"]))
2538+
2539+
# Add macros
2540+
for name, macro in self.macros.items():
2541+
results.append(CompletionItem(name, [f"Macro: {macro.value}"]))
2542+
2543+
return results
25282544

25292545
def get_help_topics(self) -> list[str]:
25302546
"""Return a list of help topics."""
@@ -3265,6 +3281,8 @@ def get_prompt() -> Any:
32653281
history=history_to_use,
32663282
input=self.session.input,
32673283
output=self.session.output,
3284+
complete_style=self.session.complete_style,
3285+
complete_while_typing=self.session.complete_while_typing,
32683286
)
32693287

32703288
return temp_session1.prompt(
@@ -3285,6 +3303,8 @@ def get_prompt() -> Any:
32853303
temp_session2: PromptSession[str] = PromptSession(
32863304
input=self.session.input,
32873305
output=self.session.output,
3306+
complete_style=self.session.complete_style,
3307+
complete_while_typing=self.session.complete_while_typing,
32883308
)
32893309
line = temp_session2.prompt(
32903310
prompt,
@@ -3298,6 +3318,8 @@ def get_prompt() -> Any:
32983318
temp_session3: PromptSession[str] = PromptSession(
32993319
input=self.session.input,
33003320
output=self.session.output,
3321+
complete_style=self.session.complete_style,
3322+
complete_while_typing=self.session.complete_while_typing,
33013323
)
33023324
line = temp_session3.prompt(
33033325
bottom_toolbar=self._bottom_toolbar if self.include_bottom_toolbar else None,

cmd2/pt_utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
constants,
2020
utils,
2121
)
22+
from .argparse_custom import CompletionItem
2223

2324
if TYPE_CHECKING:
2425
from .cmd2 import Cmd
@@ -69,7 +70,7 @@ def get_completions(self, document: Document, _complete_event: object) -> Iterab
6970
# Print formatted completions or hints above the prompt if present
7071
if self.cmd_app.formatted_completions:
7172
print_formatted_text(ANSI(self.cmd_app.formatted_completions.rstrip()))
72-
elif self.cmd_app.completion_hint:
73+
if self.cmd_app.completion_hint:
7374
print_formatted_text(ANSI(self.cmd_app.completion_hint.rstrip()))
7475

7576
# Now we iterate over self.cmd_app.completion_matches and self.cmd_app.display_matches
@@ -87,14 +88,17 @@ def get_completions(self, document: Document, _complete_event: object) -> Iterab
8788

8889
for i, match in enumerate(matches):
8990
display = display_matches[i] if use_display_matches else match
91+
display_meta = None
92+
if isinstance(match, CompletionItem) and match.descriptive_data:
93+
display_meta = match.descriptive_data[0]
9094

9195
# prompt_toolkit replaces the word before cursor by default if we use the default Completer?
9296
# No, we yield Completion(text, start_position=...).
9397
# Default start_position is 0 (append).
9498

9599
start_position = -len(text)
96100

97-
yield Completion(match, start_position=start_position, display=display)
101+
yield Completion(match, start_position=start_position, display=display, display_meta=display_meta)
98102

99103

100104
class Cmd2History(History):

0 commit comments

Comments
 (0)