diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-06-07 16:57:30 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-07 16:57:30 -0700 |
commit | eb8181e7d3f900cecd1455efd75f3b5c5f6918cd (patch) | |
tree | 97aa7c7151cc8f5c6923d0d965d650af75d7115e | |
parent | d0e71c85190b81bb269cc18bf9380a142e18d707 (diff) | |
parent | e4d13c745b7b983eb444e97f467b572a885d220c (diff) | |
download | cmd2-git-eb8181e7d3f900cecd1455efd75f3b5c5f6918cd.tar.gz |
Merge pull request #433 from python-cmd2/autocompleter
Autocompleter
-rwxr-xr-x | cmd2/argparse_completer.py | 38 | ||||
-rw-r--r-- | cmd2/cmd2.py | 44 | ||||
-rwxr-xr-x | examples/tab_autocompletion.py | 18 |
3 files changed, 84 insertions, 16 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index d98a6eac..904fb7b3 100755 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -59,6 +59,7 @@ Released under MIT license, see LICENSE file import argparse from colorama import Fore +import os import sys from typing import List, Dict, Tuple, Callable, Union @@ -78,6 +79,15 @@ ACTION_ARG_CHOICES = 'arg_choices' ACTION_SUPPRESS_HINT = 'suppress_hint' +class CompletionItem(str): + def __new__(cls, o, desc='', *args, **kwargs): + return str.__new__(cls, o, *args, **kwargs) + + # noinspection PyMissingConstructor,PyUnusedLocal + def __init__(self, o, desc='', *args, **kwargs): + self.description = desc + + class _RangeAction(object): def __init__(self, nargs: Union[int, str, Tuple[int, int], None]): self.nargs_min = None @@ -413,6 +423,8 @@ class AutoCompleter(object): completion_results = self._complete_for_arg(flag_action, text, line, begidx, endidx, consumed) if not completion_results: self._print_action_help(flag_action) + elif len(completion_results) > 1: + completion_results = self._format_completions(flag_action, completion_results) # ok, we're not a flag, see if there's a positional argument to complete else: @@ -422,9 +434,35 @@ class AutoCompleter(object): completion_results = self._complete_for_arg(pos_action, text, line, begidx, endidx, consumed) if not completion_results: self._print_action_help(pos_action) + elif len(completion_results) > 1: + completion_results = self._format_completions(pos_action, completion_results) return completion_results + def _format_completions(self, action, completions: List[Union[str, CompletionItem]]): + if completions and len(completions) > 1 and isinstance(completions[0], CompletionItem): + token_width = len(action.dest) + completions_with_desc = [] + + for item in completions: + if len(item) > token_width: + token_width = len(item) + + term_size = os.get_terminal_size() + fill_width = int(term_size.columns * .6) - (token_width + 2) + for item in completions: + entry = '{: <{token_width}}{: <{fill_width}}'.format(item, item.description, + token_width=token_width+2, + fill_width=fill_width) + completions_with_desc.append(entry) + + header = '\n{: <{token_width}}{}'.format(action.dest.upper(), action.desc_header, token_width=token_width+2) + + self._cmd2_app.completion_header = header + self._cmd2_app.display_matches = completions_with_desc + + return completions + def complete_command_help(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]: """Supports the completion of sub-commands for commands through the cmd2 help command.""" for idx, token in enumerate(tokens): diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 289c44ae..bb4ac4ad 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -33,6 +33,7 @@ import argparse import cmd import collections from colorama import Fore +import copy import glob import os import platform @@ -494,13 +495,19 @@ class Cmd(cmd.Cmd): # will be added if there is an unmatched opening quote self.allow_closing_quote = True - # Use this list if you are completing strings that contain a common delimiter and you only want to - # display the final portion of the matches as the tab-completion suggestions. The full matches - # still must be returned from your completer function. For an example, look at path_complete() - # which uses this to show only the basename of paths as the suggestions. delimiter_complete() also - # populates this list. + # An optional header that prints above the tab-completion suggestions + self.completion_header = '' + + # If the tab-completion suggestions should be displayed in a way that is different than the actual match values, + # then place those results in this list. The full matches still must be returned from your completer function. + # For an example, look at path_complete() which uses this to show only the basename of paths as the + # suggestions. delimiter_complete() also populates this list. self.display_matches = [] + # Used by functions like path_complete() and delimiter_complete() to properly + # quote matches that are completed in a delimited fashion + self.matches_delimited = False + # ----- Methods related to presenting output to the user ----- @property @@ -657,7 +664,9 @@ class Cmd(cmd.Cmd): """ self.allow_appended_space = True self.allow_closing_quote = True + self.completion_header = '' self.display_matches = [] + self.matches_delimited = False if rl_type == RlType.GNU: readline.set_completion_display_matches_hook(self._display_matches_gnu_readline) @@ -683,7 +692,6 @@ class Cmd(cmd.Cmd): On Failure Both items are None """ - import copy unclosed_quote = '' quotes_to_try = copy.copy(constants.QUOTES) @@ -836,6 +844,8 @@ class Cmd(cmd.Cmd): # Display only the portion of the match that's being completed based on delimiter if matches: + # Set this to True for proper quoting of matches with spaces + self.matches_delimited = True # Get the common beginning for the matches common_prefix = os.path.commonprefix(matches) @@ -1037,6 +1047,9 @@ class Cmd(cmd.Cmd): search_str = os.path.join(os.getcwd(), search_str) cwd_added = True + # Set this to True for proper quoting of paths with spaces + self.matches_delimited = True + # Find all matching path completions matches = glob.glob(search_str) @@ -1245,6 +1258,10 @@ class Cmd(cmd.Cmd): strings_array[1:-1] = encoded_matches strings_array[-1] = None + # Print the header if one exists + if self.completion_header: + sys.stdout.write('\n' + self.completion_header) + # Call readline's display function # rl_display_match_list(strings_array, number of completion matches, longest match length) readline_lib.rl_display_match_list(strings_array, len(encoded_matches), longest_match_length) @@ -1270,6 +1287,10 @@ class Cmd(cmd.Cmd): # Add padding for visual appeal matches_to_display, _ = self._pad_matches_to_display(matches_to_display) + # Print the header if one exists + if self.completion_header: + readline.rl.mode.console.write('\n' + self.completion_header) + # Display matches using actual display function. This also redraws the prompt and line. orig_pyreadline_display(matches_to_display) @@ -1414,17 +1435,10 @@ class Cmd(cmd.Cmd): display_matches_set = set(self.display_matches) self.display_matches = list(display_matches_set) - # Check if display_matches has been used. If so, then matches - # on delimited strings like paths was done. - if self.display_matches: - matches_delimited = True - else: - matches_delimited = False - + if not self.display_matches: # Since self.display_matches is empty, set it to self.completion_matches # before we alter them. That way the suggestions will reflect how we parsed # the token being completed and not how readline did. - import copy self.display_matches = copy.copy(self.completion_matches) # Check if we need to add an opening quote @@ -1435,7 +1449,7 @@ class Cmd(cmd.Cmd): # This is the tab completion text that will appear on the command line. common_prefix = os.path.commonprefix(self.completion_matches) - if matches_delimited: + if self.matches_delimited: # Check if any portion of the display matches appears in the tab completion display_prefix = os.path.commonprefix(self.display_matches) diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py index d1726841..817ceb72 100755 --- a/examples/tab_autocompletion.py +++ b/examples/tab_autocompletion.py @@ -109,6 +109,18 @@ class TabCompleteExample(cmd2.Cmd): """Simulating a function that queries and returns a completion values""" return actors + def instance_query_movie_ids(self) -> List[str]: + """Demonstrates showing tabular hinting of tab completion information""" + completions_with_desc = [] + + for movie_id, movie_entry in self.MOVIE_DATABASE.items(): + completions_with_desc.append(argparse_completer.CompletionItem(movie_id, movie_entry['title'])) + + setattr(self.vid_delete_movie_id, 'desc_header', 'Title') + setattr(self.movies_delete_movie_id, 'desc_header', 'Title') + + return completions_with_desc + # This demonstrates a number of customizations of the AutoCompleter version of ArgumentParser # - The help output will separately group required vs optional flags # - The help output for arguments with multiple flags or with append=True is more concise @@ -253,6 +265,8 @@ class TabCompleteExample(cmd2.Cmd): ('path_complete', [False, False])) vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete') + vid_delete_movie_id = vid_movies_delete_parser.add_argument('movie_id', help='Movie ID') + setattr(vid_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, instance_query_movie_ids) vid_shows_parser = video_types_subparsers.add_parser('shows') vid_shows_parser.set_defaults(func=_do_vid_media_shows) @@ -328,6 +342,8 @@ class TabCompleteExample(cmd2.Cmd): movies_add_parser.add_argument('actor', help='Actors', nargs='*') movies_delete_parser = movies_commands_subparsers.add_parser('delete') + movies_delete_movie_id = movies_delete_parser.add_argument('movie_id', help='Movie ID') + setattr(movies_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_movie_ids') movies_load_parser = movies_commands_subparsers.add_parser('load') movie_file_action = movies_load_parser.add_argument('movie_file', help='Movie database') @@ -362,7 +378,7 @@ class TabCompleteExample(cmd2.Cmd): 'director': TabCompleteExample.static_list_directors, # static list 'movie_file': (self.path_complete, [False, False]) } - completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, arg_choices=choices) + completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, arg_choices=choices, cmd2_app=self) tokens, _ = self.tokens_for_completion(line, begidx, endidx) results = completer.complete_command(tokens, text, line, begidx, endidx) |