diff options
| author | kmvanbrunt <kmvanbrunt@gmail.com> | 2018-03-31 00:34:39 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-03-31 00:34:39 -0400 |
| commit | ad463db10b27c62bf86d2cfc9bb6327cc5950eec (patch) | |
| tree | 86dd173944b42ffb35016c861eea04557130a691 | |
| parent | 4c2f4074616bc7b2a31cdaa940ac24a0ef426448 (diff) | |
| parent | fcccd8801edcf39b44ba5a18694b246ac11ed0d8 (diff) | |
| download | cmd2-git-ad463db10b27c62bf86d2cfc9bb6327cc5950eec.tar.gz | |
Merge pull request #332 from python-cmd2/display_length_fix
Display length fix
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rwxr-xr-x | README.md | 3 | ||||
| -rwxr-xr-x | cmd2.py | 156 | ||||
| -rwxr-xr-x | setup.py | 2 | ||||
| -rw-r--r-- | tox.ini | 5 |
5 files changed, 81 insertions, 86 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d3b0c19..d405796c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Added more control over tab completion behavior including the following flags. The use of these flags is documented in cmd2.py * ``allow_appended_space`` * ``allow_closing_quote`` + * Due to the tab completion changes, non-Windows platforms now depend on [wcwidth](https://pypi.python.org/pypi/wcwidth). * Attribute Changes (Breaks backward compatibility) * ``exclude_from_help`` is now called ``hidden_commands`` since these commands are hidden from things other than help, including tab completion @@ -61,7 +61,8 @@ pip install -U cmd2 cmd2 works with Python 2.7 and Python 3.4+ on Windows, macOS, and Linux. It is pure Python code with the only 3rd-party dependencies being on [six](https://pypi.python.org/pypi/six), [pyparsing](http://pyparsing.wikispaces.com), and [pyperclip](https://github.com/asweigart/pyperclip). -Windows has an additional dependency on [pyreadline](https://pypi.python.org/pypi/pyreadline) and Python +Windows has an additional dependency on [pyreadline](https://pypi.python.org/pypi/pyreadline). Non-Windows platforms +have an additional dependency on [wcwidth](https://pypi.python.org/pypi/wcwidth). Finally, Python 3.4 and earlier have an additional dependency on [contextlib2](https://pypi.python.org/pypi/contextlib2). For information on other installation options, see @@ -153,6 +153,9 @@ if 'pyreadline' in sys.modules: elif 'gnureadline' in sys.modules or 'readline' in sys.modules: rl_type = RlType.GNU + # We need wcswidth to calculate display width of tab completions + from wcwidth import wcswidth + # Load the readline lib so we can make changes to it import ctypes readline_lib = ctypes.CDLL(readline.__file__) @@ -1268,55 +1271,6 @@ class Cmd(cmd.Cmd): self.allow_closing_quote = True self.display_matches = [] - @staticmethod - def display_match_list_gnu_readline(substitution, matches, longest_match_length): - """ - Prints a match list using GNU readline's rl_display_match_list() - :param substitution: str - the substitution written to the command line - :param matches: list[str] - the tab completion matches to display - :param longest_match_length: int - longest printed length of the matches - """ - if rl_type == RlType.GNU: - # We will use readline's display function (rl_display_match_list()), so we - # need to encode our string as bytes to place in a C array. - if six.PY3: - encoded_substitution = bytes(substitution, encoding='utf-8') - encoded_matches = [bytes(cur_match, encoding='utf-8') for cur_match in matches] - else: - encoded_substitution = bytes(substitution) - encoded_matches = [bytes(cur_match) for cur_match in matches] - - # rl_display_match_list() expects matches to be in argv format where - # substitution is the first element, followed by the matches, and then a NULL. - # noinspection PyCallingNonCallable,PyTypeChecker - strings_array = (ctypes.c_char_p * (1 + len(encoded_matches) + 1))() - - # Copy in the encoded strings and add a NULL to the end - strings_array[0] = encoded_substitution - strings_array[1:-1] = encoded_matches - strings_array[-1] = None - - # 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) - - # rl_forced_update_display() is the proper way to redraw the prompt and line, but we - # have to use ctypes to do it since Python's readline API does not wrap the function - readline_lib.rl_forced_update_display() - - # Since we updated the display, readline asks that rl_display_fixed be set for efficiency - display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed") - display_fixed.value = 1 - - @staticmethod - def display_match_list_pyreadline(matches): - """ - Prints a match list using pyreadline's _display_completions() - :param matches: list[str] - the tab completion matches to display - """ - if rl_type == RlType.PYREADLINE: - orig_pyreadline_display(matches) - def tokens_for_completion(self, line, begidx, endidx): """ Used by tab completion functions to get all tokens through the one being completed @@ -1548,7 +1502,7 @@ class Cmd(cmd.Cmd): if flag in flag_dict: match_against = flag_dict[flag] - # Perform tab completion using an Collection. These matches are already sorted. + # Perform tab completion using a Collection. These matches are already sorted. if isinstance(match_against, Collection): completions_matches = self.basic_complete(text, line, begidx, endidx, match_against) @@ -1592,7 +1546,7 @@ class Cmd(cmd.Cmd): else: match_against = all_else - # Perform tab completion using an Collection. These matches are already sorted. + # Perform tab completion using a Collection. These matches are already sorted. if isinstance(match_against, Collection): matches = self.basic_complete(text, line, begidx, endidx, match_against) @@ -1817,55 +1771,87 @@ class Cmd(cmd.Cmd): def _display_matches_gnu_readline(self, substitution, matches, longest_match_length): """ - cmd2's default GNU readline function that prints tab-completion matches to the screen - This exists to allow the printing of self.display_matches if it has data. Otherwise matches prints. - The actual printing is done by display_match_list_gnu_readline(). - - If you need a custom match display function for a particular completion type, then set it by calling - readline.set_completion_display_matches_hook() during the completer routine. - Your custom display function should ultimately call display_match_list_gnu_readline() to print. + Prints a match list using GNU readline's rl_display_match_list() + This exists to print self.display_matches if it has data. Otherwise matches prints. :param substitution: str - the substitution written to the command line :param matches: list[str] - the tab completion matches to display :param longest_match_length: int - longest printed length of the matches """ - if len(self.display_matches) > 0: - matches_to_display = self.display_matches - else: - matches_to_display = matches + if rl_type == RlType.GNU: + + # Check if we should show display_matches + if len(self.display_matches) > 0: + matches_to_display = self.display_matches + + # Recalculate longest_match_length for display_matches + longest_match_length = 0 + + for cur_match in matches_to_display: + cur_length = wcswidth(cur_match) + if cur_length > longest_match_length: + longest_match_length = cur_length + else: + matches_to_display = matches + + # Eliminate duplicates and sort + matches_to_display_set = set(matches_to_display) + matches_to_display = list(matches_to_display_set) + matches_to_display.sort() + + # We will use readline's display function (rl_display_match_list()), so we + # need to encode our string as bytes to place in a C array. + if six.PY3: + encoded_substitution = bytes(substitution, encoding='utf-8') + encoded_matches = [bytes(cur_match, encoding='utf-8') for cur_match in matches_to_display] + else: + encoded_substitution = bytes(substitution) + encoded_matches = [bytes(cur_match) for cur_match in matches_to_display] + + # rl_display_match_list() expects matches to be in argv format where + # substitution is the first element, followed by the matches, and then a NULL. + # noinspection PyCallingNonCallable,PyTypeChecker + strings_array = (ctypes.c_char_p * (1 + len(encoded_matches) + 1))() - # Eliminate duplicates and sort - matches_to_display_set = set(matches_to_display) - matches_to_display = list(matches_to_display_set) - matches_to_display.sort() + # Copy in the encoded strings and add a NULL to the end + strings_array[0] = encoded_substitution + strings_array[1:-1] = encoded_matches + strings_array[-1] = None + + # 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) + + # rl_forced_update_display() is the proper way to redraw the prompt and line, but we + # have to use ctypes to do it since Python's readline API does not wrap the function + readline_lib.rl_forced_update_display() - # Display the matches - self.display_match_list_gnu_readline(substitution, matches_to_display, longest_match_length) + # Since we updated the display, readline asks that rl_display_fixed be set for efficiency + display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed") + display_fixed.value = 1 def _display_matches_pyreadline(self, matches): """ - cmd2's default pyreadline function that prints tab-completion matches to the screen - This exists to allow the printing of self.display_matches if it has data. Otherwise matches prints. - The actual printing is done by display_match_list_pyreadline(). - - If you need a custom match display function for a particular completion type, then set - readline.rl.mode._display_completions to that function during the completer routine. - Your custom display function should ultimately call display_match_list_pyreadline() to print. + Prints a match list using pyreadline's _display_completions() + This exists to print self.display_matches if it has data. Otherwise matches prints. :param matches: list[str] - the tab completion matches to display """ - if len(self.display_matches) > 0: - matches_to_display = self.display_matches - else: - matches_to_display = matches + if rl_type == RlType.PYREADLINE: - # Eliminate duplicates and sort - matches_to_display_set = set(matches_to_display) - matches_to_display = list(matches_to_display_set) - matches_to_display.sort() + # Check if we should show display_matches + if len(self.display_matches) > 0: + matches_to_display = self.display_matches + else: + matches_to_display = matches + + # Eliminate duplicates and sort + matches_to_display_set = set(matches_to_display) + matches_to_display = list(matches_to_display_set) + matches_to_display.sort() - # Display the matches - self.display_match_list_pyreadline(matches_to_display) + # Display the matches + orig_pyreadline_display(matches_to_display) def _handle_completion_token_quote(self, raw_completion_token): """ @@ -81,6 +81,8 @@ if int(setuptools.__version__.split('.')[0]) < 18: EXTRAS_REQUIRE = {} if sys.platform.startswith('win'): INSTALL_REQUIRES.append('pyreadline') + else: + INSTALL_REQUIRES.append('wcwidth') if sys.version_info < (3, 5): INSTALL_REQUIRES.append('contextlib2') if sys.version_info < (3, 4): @@ -22,6 +22,7 @@ deps = pytest-xdist six subprocess32 + wcwidth commands = py.test {posargs: -n 2} --cov=cmd2 --cov-report=term-missing --forked codecov @@ -52,6 +53,7 @@ deps = pytest-forked pytest-xdist six + wcwidth commands = py.test -v -n2 --forked [testenv:py35] @@ -63,6 +65,7 @@ deps = pytest-forked pytest-xdist six + wcwidth commands = py.test -v -n2 --forked [testenv:py35-win] @@ -87,6 +90,7 @@ deps = pytest-forked pytest-xdist six + wcwidth commands = py.test {posargs: -n 2} --cov=cmd2 --cov-report=term-missing --forked codecov @@ -111,5 +115,6 @@ deps = pytest-forked pytest-xdist six + wcwidth commands = py.test -v -n2 --forked |
