diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 2 | ||||
-rw-r--r-- | cmd2/cmd2.py | 424 | ||||
-rw-r--r-- | cmd2/utils.py | 2 | ||||
-rw-r--r-- | tasks.py | 2 | ||||
-rw-r--r-- | tests/test_completion.py | 56 |
7 files changed, 269 insertions, 220 deletions
diff --git a/.travis.yml b/.travis.yml index e05281a9..016756fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,7 +44,7 @@ before_script: # stop the build if there are Python syntax errors or undefined names # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide if [[ $TOXENV == py37 ]]; then - flake8 . --count --ignore=E252,W503 --max-complexity=31 --max-line-length=127 --show-source --statistics ; + flake8 . --count --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics ; fi script: diff --git a/CHANGELOG.md b/CHANGELOG.md index abb440dd..e26c60d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Greatly simplified using argparse-based tab completion. The new interface is a complete overhaul that breaks the previous way of specifying completion and choices functions. See header of [argparse_custom.py](https://github.com/python-cmd2/cmd2/blob/master/cmd2/argparse_custom.py) for more information. + * Enabled tab completion on multiline commands * **Renamed Commands Notice** * The following commands were renamed in the last release and have been removed in this release * `load` - replaced by `run_script` diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index c824bf97..a741882c 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -414,7 +414,7 @@ class AutoCompleter(object): """ Supports cmd2's help command in the completion of sub-command names :param tokens: command line tokens - :param text: the string prefix we are attempting to match (all returned matches must begin with it) + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c8f2f9ff..1f07f2cb 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -428,6 +428,9 @@ class Cmd(cmd.Cmd): # Used to keep track of whether a continuation prompt is being displayed self._at_continuation_prompt = False + # The multiline command currently being typed which is used to tab complete multiline commands. + self._multiline_in_progress = '' + # The error that prints when no help information can be found self.help_error = "No help on {}" @@ -716,6 +719,7 @@ class Cmd(cmd.Cmd): self.allow_appended_space = True self.allow_closing_quote = True self.completion_header = '' + self.completion_matches = [] self.display_matches = [] self.matches_delimited = False self.matches_sorted = False @@ -868,7 +872,7 @@ class Cmd(cmd.Cmd): In this case the delimiter would be :: and the user could easily narrow down what they are looking for if they were only shown suggestions in the category they are at in the string. - :param text: the string prefix we are attempting to match (all returned matches must begin with it) + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text @@ -908,7 +912,7 @@ class Cmd(cmd.Cmd): all_else: Union[None, Iterable, Callable] = None) -> List[str]: """Tab completes based on a particular flag preceding the token being completed. - :param text: the string prefix we are attempting to match (all returned matches must begin with it) + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text @@ -950,7 +954,7 @@ class Cmd(cmd.Cmd): all_else: Union[None, Iterable, Callable] = None) -> List[str]: """Tab completes based on a fixed position in the input string. - :param text: the string prefix we are attempting to match (all returned matches must begin with it) + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text @@ -994,7 +998,7 @@ class Cmd(cmd.Cmd): path_filter: Optional[Callable[[str], bool]] = None) -> List[str]: """Performs completion of local file system paths - :param text: the string prefix we are attempting to match (all returned matches must begin with it) + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text @@ -1138,7 +1142,7 @@ class Cmd(cmd.Cmd): complete_blank: bool = False) -> List[str]: """Performs completion of executables either in a user's path or a given path - :param text: the string prefix we are attempting to match (all returned matches must begin with it) + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text @@ -1164,7 +1168,7 @@ class Cmd(cmd.Cmd): It determines if it should tab complete for redirection (|, >, >>) or use the completer function for the current command - :param text: the string prefix we are attempting to match (all returned matches must begin with it) + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text @@ -1344,237 +1348,241 @@ class Cmd(cmd.Cmd): # Display matches using actual display function. This also redraws the prompt and line. orig_pyreadline_display(matches_to_display) - def _complete_worker(self, text: str, state: int) -> Optional[str]: - """The actual worker function for tab completion which is called by complete() and returns - the next possible completion for 'text'. - - If a command has not been entered, then complete against command list. - Otherwise try to call complete_<command> to get list of completions. - - This completer function is called as complete(text, state), for state in 0, 1, 2, …, until it returns a - non-string value. It should return the next possible completion starting with text. - - :param text: the current word that user is typing - :param state: non-negative integer + def _completion_for_command(self, text: str, line: str, begidx: int, + endidx: int, shortcut_to_restore: str) -> None: """ - import functools - if state == 0 and rl_type != RlType.NONE: - unclosed_quote = '' - self._reset_completion_defaults() - - # lstrip the original line - orig_line = readline.get_line_buffer() - line = orig_line.lstrip() - stripped = len(orig_line) - len(line) - - # Calculate new indexes for the stripped line. If the cursor is at a position before the end of a - # line of spaces, then the following math could result in negative indexes. Enforce a max of 0. - begidx = max(readline.get_begidx() - stripped, 0) - endidx = max(readline.get_endidx() - stripped, 0) - - # Shortcuts are not word break characters when tab completing. Therefore shortcuts become part - # of the text variable if there isn't a word break, like a space, after it. We need to remove it - # from text and update the indexes. This only applies if we are at the the beginning of the line. - shortcut_to_restore = '' - if begidx == 0: - for (shortcut, _) in self.statement_parser.shortcuts: - if text.startswith(shortcut): - # Save the shortcut to restore later - shortcut_to_restore = shortcut - - # Adjust text and where it begins - text = text[len(shortcut_to_restore):] - begidx += len(shortcut_to_restore) - break + Helper function for complete() that performs command-specific tab completion - # If begidx is greater than 0, then we are no longer completing the command - if begidx > 0: + :param text: the string prefix we are attempting to match (all matches must begin with it) + :param line: the current input line with leading whitespace removed + :param begidx: the beginning index of the prefix text + :param endidx: the ending index of the prefix text + :param shortcut_to_restore: if not blank, then this shortcut was removed from text and needs to be + prepended to all the matches + """ + unclosed_quote = '' - # Parse the command line - statement = self.statement_parser.parse_command_only(line) - command = statement.command - expanded_line = statement.command_and_args - - # We overwrote line with a properly formatted but fully stripped version - # Restore the end spaces since line is only supposed to be lstripped when - # passed to completer functions according to Python docs - rstripped_len = len(line) - len(line.rstrip()) - expanded_line += ' ' * rstripped_len - - # Fix the index values if expanded_line has a different size than line - if len(expanded_line) != len(line): - diff = len(expanded_line) - len(line) - begidx += diff - endidx += diff - - # Overwrite line to pass into completers - line = expanded_line - - # Get all tokens through the one being completed - tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx) - - # Check if we either had a parsing error or are trying to complete the command token - # The latter can happen if " or ' was entered as the command - if len(tokens) <= 1: - self.completion_matches = [] - return None - - # Text we need to remove from completions later - text_to_remove = '' - - # Get the token being completed with any opening quote preserved - raw_completion_token = raw_tokens[-1] - - # Check if the token being completed has an opening quote - if raw_completion_token and raw_completion_token[0] in constants.QUOTES: - - # Since the token is still being completed, we know the opening quote is unclosed - unclosed_quote = raw_completion_token[0] - - # readline still performs word breaks after a quote. Therefore something like quoted search - # text with a space would have resulted in begidx pointing to the middle of the token we - # we want to complete. Figure out where that token actually begins and save the beginning - # portion of it that was not part of the text readline gave us. We will remove it from the - # completions later since readline expects them to start with the original text. - actual_begidx = line[:endidx].rfind(tokens[-1]) - - if actual_begidx != begidx: - text_to_remove = line[actual_begidx:begidx] - - # Adjust text and where it begins so the completer routines - # get unbroken search text to complete on. - text = text_to_remove + text - begidx = actual_begidx - - # Check if a valid command was entered - if command in self.get_all_commands(): - # Get the completer function for this command - compfunc = getattr(self, 'complete_' + command, None) - - if compfunc is None: - # There's no completer function, next see if the command uses argparser - func = self.cmd_func(command) - if func and hasattr(func, 'argparser'): - compfunc = functools.partial(self._autocomplete_default, - argparser=getattr(func, 'argparser')) - else: - compfunc = self.completedefault + # Parse the command line + statement = self.statement_parser.parse_command_only(line) + command = statement.command + expanded_line = statement.command_and_args - # Check if a macro was entered - elif command in self.macros: - compfunc = self.path_complete + # We overwrote line with a properly formatted but fully stripped version + # Restore the end spaces since line is only supposed to be lstripped when + # passed to completer functions according to Python docs + rstripped_len = len(line) - len(line.rstrip()) + expanded_line += ' ' * rstripped_len - # A valid command was not entered - else: - # Check if this command should be run as a shell command - if self.default_to_shell and command in utils.get_exes_in_path(command): - compfunc = self.path_complete - else: - compfunc = self.completedefault + # Fix the index values if expanded_line has a different size than line + if len(expanded_line) != len(line): + diff = len(expanded_line) - len(line) + begidx += diff + endidx += diff - # Attempt tab completion for redirection first, and if that isn't occurring, - # call the completer function for the current command - self.completion_matches = self._redirect_complete(text, line, begidx, endidx, compfunc) + # Overwrite line to pass into completers + line = expanded_line - if self.completion_matches: + # Get all tokens through the one being completed + tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx) - # Eliminate duplicates - self.completion_matches = utils.remove_duplicates(self.completion_matches) - self.display_matches = utils.remove_duplicates(self.display_matches) + # Check if we either had a parsing error or are trying to complete the command token + # The latter can happen if " or ' was entered as the command + if len(tokens) <= 1: + return - 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) + # Text we need to remove from completions later + text_to_remove = '' + + # Get the token being completed with any opening quote preserved + raw_completion_token = raw_tokens[-1] + + # Check if the token being completed has an opening quote + if raw_completion_token and raw_completion_token[0] in constants.QUOTES: + + # Since the token is still being completed, we know the opening quote is unclosed + unclosed_quote = raw_completion_token[0] + + # readline still performs word breaks after a quote. Therefore something like quoted search + # text with a space would have resulted in begidx pointing to the middle of the token we + # we want to complete. Figure out where that token actually begins and save the beginning + # portion of it that was not part of the text readline gave us. We will remove it from the + # completions later since readline expects them to start with the original text. + actual_begidx = line[:endidx].rfind(tokens[-1]) + + if actual_begidx != begidx: + text_to_remove = line[actual_begidx:begidx] + + # Adjust text and where it begins so the completer routines + # get unbroken search text to complete on. + text = text_to_remove + text + begidx = actual_begidx + + # Check if a valid command was entered + if command in self.get_all_commands(): + # Get the completer function for this command + compfunc = getattr(self, 'complete_' + command, None) + + if compfunc is None: + # There's no completer function, next see if the command uses argparser + func = self.cmd_func(command) + if func and hasattr(func, 'argparser'): + import functools + compfunc = functools.partial(self._autocomplete_default, + argparser=getattr(func, 'argparser')) + else: + compfunc = self.completedefault - # Check if we need to add an opening quote - if not unclosed_quote: + # Check if a macro was entered + elif command in self.macros: + compfunc = self.path_complete - add_quote = False + # A valid command was not entered + else: + # Check if this command should be run as a shell command + if self.default_to_shell and command in utils.get_exes_in_path(command): + compfunc = self.path_complete + else: + compfunc = self.completedefault - # This is the tab completion text that will appear on the command line. - common_prefix = os.path.commonprefix(self.completion_matches) + # Attempt tab completion for redirection first, and if that isn't occurring, + # call the completer function for the current command + self.completion_matches = self._redirect_complete(text, line, begidx, endidx, compfunc) - 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) + if self.completion_matches: - # For delimited matches, we check for a space in what appears before the display - # matches (common_prefix) as well as in the display matches themselves. - if ' ' in common_prefix or (display_prefix - and any(' ' in match for match in self.display_matches)): - add_quote = True + # Eliminate duplicates + self.completion_matches = utils.remove_duplicates(self.completion_matches) + self.display_matches = utils.remove_duplicates(self.display_matches) - # If there is a tab completion and any match has a space, then add an opening quote - elif common_prefix and any(' ' in match for match in self.completion_matches): - add_quote = True + 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) - if add_quote: - # Figure out what kind of quote to add and save it as the unclosed_quote - if any('"' in match for match in self.completion_matches): - unclosed_quote = "'" - else: - unclosed_quote = '"' + # Check if we need to add an opening quote + if not unclosed_quote: - self.completion_matches = [unclosed_quote + match for match in self.completion_matches] + add_quote = False - # Check if we need to remove text from the beginning of tab completions - elif text_to_remove: - self.completion_matches = \ - [match.replace(text_to_remove, '', 1) for match in self.completion_matches] + # This is the tab completion text that will appear on the command line. + common_prefix = os.path.commonprefix(self.completion_matches) - # Check if we need to restore a shortcut in the tab completions - # so it doesn't get erased from the command line - if shortcut_to_restore: - self.completion_matches = \ - [shortcut_to_restore + match for match in self.completion_matches] + 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) - else: - # Complete token against anything a user can run - self.completion_matches = utils.basic_complete(text, line, begidx, endidx, - self._get_commands_aliases_and_macros_for_completion()) + # For delimited matches, we check for a space in what appears before the display + # matches (common_prefix) as well as in the display matches themselves. + if ' ' in common_prefix or (display_prefix + and any(' ' in match for match in self.display_matches)): + add_quote = True - # Handle single result - if len(self.completion_matches) == 1: - str_to_append = '' + # If there is a tab completion and any match has a space, then add an opening quote + elif common_prefix and any(' ' in match for match in self.completion_matches): + add_quote = True - # Add a closing quote if needed and allowed - if self.allow_closing_quote and unclosed_quote: - str_to_append += unclosed_quote + if add_quote: + # Figure out what kind of quote to add and save it as the unclosed_quote + if any('"' in match for match in self.completion_matches): + unclosed_quote = "'" + else: + unclosed_quote = '"' - # If we are at the end of the line, then add a space if allowed - if self.allow_appended_space and endidx == len(line): - str_to_append += ' ' + self.completion_matches = [unclosed_quote + match for match in self.completion_matches] - self.completion_matches[0] += str_to_append + # Check if we need to remove text from the beginning of tab completions + elif text_to_remove: + self.completion_matches = [match.replace(text_to_remove, '', 1) for match in self.completion_matches] - # Sort matches if they haven't already been sorted - if not self.matches_sorted: - self.completion_matches.sort(key=self.default_sort_key) - self.display_matches.sort(key=self.default_sort_key) - self.matches_sorted = True + # Check if we need to restore a shortcut in the tab completions + # so it doesn't get erased from the command line + if shortcut_to_restore: + self.completion_matches = [shortcut_to_restore + match for match in self.completion_matches] - try: - return self.completion_matches[state] - except IndexError: - return None + # If we have one result, then add a closing quote if needed and allowed + if len(self.completion_matches) == 1 and self.allow_closing_quote and unclosed_quote: + self.completion_matches[0] += unclosed_quote def complete(self, text: str, state: int) -> Optional[str]: """Override of cmd2's complete method which returns the next possible completion for 'text' - This method gets called directly by readline. Since readline suppresses any exception raised - in completer functions, they can be difficult to debug. Therefore this function wraps the - actual tab completion logic and prints to stderr any exception that occurs before returning - control to readline. + This completer function is called by readline as complete(text, state), for state in 0, 1, 2, …, + until it returns a non-string value. It should return the next possible completion starting with text. + + Since readline suppresses any exception raised in completer functions, they can be difficult to debug. + Therefore this function wraps the actual tab completion logic and prints to stderr any exception that + occurs before returning control to readline. :param text: the current word that user is typing :param state: non-negative integer + :return: the next possible completion for text or None """ # noinspection PyBroadException try: - return self._complete_worker(text, state) + if state == 0 and rl_type != RlType.NONE: + self._reset_completion_defaults() + + # Check if we are completing a multiline command + if self._at_continuation_prompt: + # lstrip and prepend the previously typed portion of this multiline command + lstripped_previous = self._multiline_in_progress.lstrip() + line = lstripped_previous + readline.get_line_buffer() + + # Increment the indexes to account for the prepended text + begidx = len(lstripped_previous) + readline.get_begidx() + endidx = len(lstripped_previous) + readline.get_endidx() + else: + # lstrip the original line + orig_line = readline.get_line_buffer() + line = orig_line.lstrip() + num_stripped = len(orig_line) - len(line) + + # Calculate new indexes for the stripped line. If the cursor is at a position before the end of a + # line of spaces, then the following math could result in negative indexes. Enforce a max of 0. + begidx = max(readline.get_begidx() - num_stripped, 0) + endidx = max(readline.get_endidx() - num_stripped, 0) + + # Shortcuts are not word break characters when tab completing. Therefore shortcuts become part + # of the text variable if there isn't a word break, like a space, after it. We need to remove it + # from text and update the indexes. This only applies if we are at the the beginning of the line. + shortcut_to_restore = '' + if begidx == 0: + for (shortcut, _) in self.statement_parser.shortcuts: + if text.startswith(shortcut): + # Save the shortcut to restore later + shortcut_to_restore = shortcut + + # Adjust text and where it begins + text = text[len(shortcut_to_restore):] + begidx += len(shortcut_to_restore) + break + + # If begidx is greater than 0, then we are no longer completing the first token (command name) + if begidx > 0: + self._completion_for_command(text, line, begidx, endidx, shortcut_to_restore) + + # Otherwise complete token against anything a user can run + else: + match_against = self._get_commands_aliases_and_macros_for_completion() + self.completion_matches = utils.basic_complete(text, line, begidx, endidx, match_against) + + # If we have one result and we are at the end of the line, then add a space if allowed + if len(self.completion_matches) == 1 and endidx == len(line) and self.allow_appended_space: + self.completion_matches[0] += ' ' + + # Sort matches if they haven't already been sorted + if not self.matches_sorted: + self.completion_matches.sort(key=self.default_sort_key) + self.display_matches.sort(key=self.default_sort_key) + self.matches_sorted = True + + try: + return self.completion_matches[state] + except IndexError: + return None + except Exception as e: # Insert a newline so the exception doesn't print in the middle of the command line being tab completed self.perror('\n', end='') @@ -1854,15 +1862,19 @@ class Cmd(cmd.Cmd): # - a multiline command with unclosed quotation marks try: self._at_continuation_prompt = True - newline = self._pseudo_raw_input(self.continuation_prompt) - if newline == 'eof': + + # Save the command line up to this point for tab completion + self._multiline_in_progress = line + '\n' + + nextline = self._pseudo_raw_input(self.continuation_prompt) + if nextline == 'eof': # they entered either a blank line, or we hit an EOF # for some other reason. Turn the literal 'eof' # into a blank line, which serves as a command # terminator - newline = '\n' - self.poutput(newline) - line = '{}\n{}'.format(statement.raw, newline) + nextline = '\n' + self.poutput(nextline) + line = '{}{}'.format(self._multiline_in_progress, nextline) except KeyboardInterrupt as ex: if self.quit_on_sigint: raise ex diff --git a/cmd2/utils.py b/cmd2/utils.py index 7f357a6c..57d0deee 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -624,7 +624,7 @@ def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against Basic tab completion function that matches against a list of strings without considering line contents or cursor position. The args required by this function are defined in the header of Pythons's cmd.py. - :param text: the string prefix we are attempting to match (all returned matches must begin with it) + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text @@ -239,5 +239,5 @@ namespace.add_task(pypi_test) @invoke.task def flake8(context): "Run flake8 linter and tool for style guide enforcement" - context.run("flake8 --ignore=E252,W503 --max-complexity=31 --max-line-length=127 --show-source --statistics --exclude=.git,__pycache__,.tox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov") + context.run("flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics --exclude=.git,__pycache__,.tox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov") namespace.add_task(flake8) diff --git a/tests/test_completion.py b/tests/test_completion.py index 3cee1955..233783c6 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -61,7 +61,7 @@ class CompletionsExample(cmd2.Cmd): Example cmd2 application used to exercise tab-completion tests """ def __init__(self): - cmd2.Cmd.__init__(self) + cmd2.Cmd.__init__(self, multiline_commands=['test_multiline']) def do_test_basic(self, args): pass @@ -88,6 +88,12 @@ class CompletionsExample(cmd2.Cmd): def complete_test_raise_exception(self, text, line, begidx, endidx): raise IndexError("You are out of bounds!!") + def do_test_multiline(self, args): + pass + + def complete_test_multiline(self, text, line, begidx, endidx): + return utils.basic_complete(text, line, begidx, endidx, sport_item_strs) + def do_test_no_completer(self, args): """Completing this should result in completedefault() being called""" pass @@ -128,7 +134,7 @@ def test_complete_empty_arg(cmd2_app): endidx = len(line) begidx = endidx - len(text) - expected = sorted(cmd2_app.get_visible_commands()) + expected = sorted(cmd2_app.get_visible_commands(), key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected @@ -733,7 +739,8 @@ def test_add_opening_quote_basic_no_text(cmd2_app): # The whole list will be returned with no opening quotes added first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs) + assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs, + key=cmd2_app.default_sort_key) def test_add_opening_quote_basic_nothing_added(cmd2_app): text = 'P' @@ -750,7 +757,7 @@ def test_add_opening_quote_basic_quote_added(cmd2_app): endidx = len(line) begidx = endidx - len(text) - expected = sorted(['"Ham', '"Ham Sandwich']) + expected = sorted(['"Ham', '"Ham Sandwich'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected @@ -771,7 +778,7 @@ def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app): endidx = len(line) begidx = endidx - len(text) - expected = sorted(['"Ham', '"Ham Sandwich']) + expected = sorted(['"Ham', '"Ham Sandwich'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected @@ -783,7 +790,8 @@ def test_add_opening_quote_delimited_no_text(cmd2_app): # The whole list will be returned with no opening quotes added first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs) + assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs, + key=cmd2_app.default_sort_key) def test_add_opening_quote_delimited_nothing_added(cmd2_app): text = '/ho' @@ -791,8 +799,8 @@ def test_add_opening_quote_delimited_nothing_added(cmd2_app): endidx = len(line) begidx = endidx - len(text) - expected_matches = sorted(delimited_strs) - expected_display = sorted(['other user', 'user']) + expected_matches = sorted(delimited_strs, key=cmd2_app.default_sort_key) + expected_display = sorted(['other user', 'user'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ @@ -806,7 +814,7 @@ def test_add_opening_quote_delimited_quote_added(cmd2_app): begidx = endidx - len(text) expected_common_prefix = '"/home/user/file' - expected_display = sorted(['file.txt', 'file space.txt']) + expected_display = sorted(['file.txt', 'file space.txt'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ @@ -821,7 +829,7 @@ def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app): begidx = endidx - len(text) expected_common_prefix = '"/home/user/file' - expected_display = sorted(['file.txt', 'file space.txt']) + expected_display = sorted(['file.txt', 'file space.txt'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ @@ -863,6 +871,34 @@ def test_quote_as_command(cmd2_app): assert first_match is None and not cmd2_app.completion_matches +def test_complete_multiline_on_single_line(cmd2_app): + text = '' + line = 'test_multiline {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + expected = sorted(sport_item_strs, key=cmd2_app.default_sort_key) + first_match = complete_tester(text, line, begidx, endidx, cmd2_app) + + assert first_match is not None and cmd2_app.completion_matches == expected + + +def test_complete_multiline_on_multiple_lines(cmd2_app): + # Set the same variables _complete_statement() sets when a user is entering data at a continuation prompt + cmd2_app._at_continuation_prompt = True + cmd2_app._multiline_in_progress = "test_multiline\n" + + text = 'Ba' + line = '{}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + expected = sorted(['Bat', 'Basket', 'Basketball'], key=cmd2_app.default_sort_key) + first_match = complete_tester(text, line, begidx, endidx, cmd2_app) + + assert first_match is not None and cmd2_app.completion_matches == expected + + # Used by redirect_complete tests class RedirCompType(enum.Enum): SHELL_CMD = 1, |