Skip to content

Commit 2f4198e

Browse files
committed
Fixed bug where display width was not being calculated for display_matches
1 parent 4c2f407 commit 2f4198e

File tree

3 files changed

+76
-83
lines changed

3 files changed

+76
-83
lines changed

cmd2.py

Lines changed: 69 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ class RlType(Enum):
153153
elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
154154
rl_type = RlType.GNU
155155

156+
# We need wcswidth to calculate display width of tab completions
157+
from wcwidth import wcswidth
158+
156159
# Load the readline lib so we can make changes to it
157160
import ctypes
158161
readline_lib = ctypes.CDLL(readline.__file__)
@@ -1268,55 +1271,6 @@ def set_completion_defaults(self):
12681271
self.allow_closing_quote = True
12691272
self.display_matches = []
12701273

1271-
@staticmethod
1272-
def display_match_list_gnu_readline(substitution, matches, longest_match_length):
1273-
"""
1274-
Prints a match list using GNU readline's rl_display_match_list()
1275-
:param substitution: str - the substitution written to the command line
1276-
:param matches: list[str] - the tab completion matches to display
1277-
:param longest_match_length: int - longest printed length of the matches
1278-
"""
1279-
if rl_type == RlType.GNU:
1280-
# We will use readline's display function (rl_display_match_list()), so we
1281-
# need to encode our string as bytes to place in a C array.
1282-
if six.PY3:
1283-
encoded_substitution = bytes(substitution, encoding='utf-8')
1284-
encoded_matches = [bytes(cur_match, encoding='utf-8') for cur_match in matches]
1285-
else:
1286-
encoded_substitution = bytes(substitution)
1287-
encoded_matches = [bytes(cur_match) for cur_match in matches]
1288-
1289-
# rl_display_match_list() expects matches to be in argv format where
1290-
# substitution is the first element, followed by the matches, and then a NULL.
1291-
# noinspection PyCallingNonCallable,PyTypeChecker
1292-
strings_array = (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()
1293-
1294-
# Copy in the encoded strings and add a NULL to the end
1295-
strings_array[0] = encoded_substitution
1296-
strings_array[1:-1] = encoded_matches
1297-
strings_array[-1] = None
1298-
1299-
# Call readline's display function
1300-
# rl_display_match_list(strings_array, number of completion matches, longest match length)
1301-
readline_lib.rl_display_match_list(strings_array, len(encoded_matches), longest_match_length)
1302-
1303-
# rl_forced_update_display() is the proper way to redraw the prompt and line, but we
1304-
# have to use ctypes to do it since Python's readline API does not wrap the function
1305-
readline_lib.rl_forced_update_display()
1306-
1307-
# Since we updated the display, readline asks that rl_display_fixed be set for efficiency
1308-
display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
1309-
display_fixed.value = 1
1310-
1311-
@staticmethod
1312-
def display_match_list_pyreadline(matches):
1313-
"""
1314-
Prints a match list using pyreadline's _display_completions()
1315-
:param matches: list[str] - the tab completion matches to display
1316-
"""
1317-
if rl_type == RlType.PYREADLINE:
1318-
orig_pyreadline_display(matches)
1319-
13201274
def tokens_for_completion(self, line, begidx, endidx):
13211275
"""
13221276
Used by tab completion functions to get all tokens through the one being completed
@@ -1817,55 +1771,87 @@ def _redirect_complete(self, text, line, begidx, endidx, compfunc):
18171771

18181772
def _display_matches_gnu_readline(self, substitution, matches, longest_match_length):
18191773
"""
1820-
cmd2's default GNU readline function that prints tab-completion matches to the screen
1821-
This exists to allow the printing of self.display_matches if it has data. Otherwise matches prints.
1822-
The actual printing is done by display_match_list_gnu_readline().
1823-
1824-
If you need a custom match display function for a particular completion type, then set it by calling
1825-
readline.set_completion_display_matches_hook() during the completer routine.
1826-
Your custom display function should ultimately call display_match_list_gnu_readline() to print.
1774+
Prints a match list using GNU readline's rl_display_match_list()
1775+
This exists to print self.display_matches if it has data. Otherwise matches prints.
18271776
18281777
:param substitution: str - the substitution written to the command line
18291778
:param matches: list[str] - the tab completion matches to display
18301779
:param longest_match_length: int - longest printed length of the matches
18311780
"""
1832-
if len(self.display_matches) > 0:
1833-
matches_to_display = self.display_matches
1834-
else:
1835-
matches_to_display = matches
1781+
if rl_type == RlType.GNU:
1782+
1783+
# Check if we should show display_matches
1784+
if len(self.display_matches) > 0:
1785+
matches_to_display = self.display_matches
1786+
1787+
# Recalculate longest_match_length for display_matches
1788+
longest_match_length = 0
1789+
1790+
for cur_match in matches_to_display:
1791+
cur_length = wcswidth(cur_match)
1792+
if cur_length > longest_match_length:
1793+
longest_match_length = cur_length
1794+
else:
1795+
matches_to_display = matches
1796+
1797+
# Eliminate duplicates and sort
1798+
matches_to_display_set = set(matches_to_display)
1799+
matches_to_display = list(matches_to_display_set)
1800+
matches_to_display.sort()
1801+
1802+
# We will use readline's display function (rl_display_match_list()), so we
1803+
# need to encode our string as bytes to place in a C array.
1804+
if six.PY3:
1805+
encoded_substitution = bytes(substitution, encoding='utf-8')
1806+
encoded_matches = [bytes(cur_match, encoding='utf-8') for cur_match in matches_to_display]
1807+
else:
1808+
encoded_substitution = bytes(substitution)
1809+
encoded_matches = [bytes(cur_match) for cur_match in matches_to_display]
1810+
1811+
# rl_display_match_list() expects matches to be in argv format where
1812+
# substitution is the first element, followed by the matches, and then a NULL.
1813+
# noinspection PyCallingNonCallable,PyTypeChecker
1814+
strings_array = (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()
18361815

1837-
# Eliminate duplicates and sort
1838-
matches_to_display_set = set(matches_to_display)
1839-
matches_to_display = list(matches_to_display_set)
1840-
matches_to_display.sort()
1816+
# Copy in the encoded strings and add a NULL to the end
1817+
strings_array[0] = encoded_substitution
1818+
strings_array[1:-1] = encoded_matches
1819+
strings_array[-1] = None
1820+
1821+
# Call readline's display function
1822+
# rl_display_match_list(strings_array, number of completion matches, longest match length)
1823+
readline_lib.rl_display_match_list(strings_array, len(encoded_matches), longest_match_length)
1824+
1825+
# rl_forced_update_display() is the proper way to redraw the prompt and line, but we
1826+
# have to use ctypes to do it since Python's readline API does not wrap the function
1827+
readline_lib.rl_forced_update_display()
18411828

1842-
# Display the matches
1843-
self.display_match_list_gnu_readline(substitution, matches_to_display, longest_match_length)
1829+
# Since we updated the display, readline asks that rl_display_fixed be set for efficiency
1830+
display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
1831+
display_fixed.value = 1
18441832

18451833
def _display_matches_pyreadline(self, matches):
18461834
"""
1847-
cmd2's default pyreadline function that prints tab-completion matches to the screen
1848-
This exists to allow the printing of self.display_matches if it has data. Otherwise matches prints.
1849-
The actual printing is done by display_match_list_pyreadline().
1850-
1851-
If you need a custom match display function for a particular completion type, then set
1852-
readline.rl.mode._display_completions to that function during the completer routine.
1853-
Your custom display function should ultimately call display_match_list_pyreadline() to print.
1835+
Prints a match list using pyreadline's _display_completions()
1836+
This exists to print self.display_matches if it has data. Otherwise matches prints.
18541837
18551838
:param matches: list[str] - the tab completion matches to display
18561839
"""
1857-
if len(self.display_matches) > 0:
1858-
matches_to_display = self.display_matches
1859-
else:
1860-
matches_to_display = matches
1840+
if rl_type == RlType.PYREADLINE:
18611841

1862-
# Eliminate duplicates and sort
1863-
matches_to_display_set = set(matches_to_display)
1864-
matches_to_display = list(matches_to_display_set)
1865-
matches_to_display.sort()
1842+
# Check if we should show display_matches
1843+
if len(self.display_matches) > 0:
1844+
matches_to_display = self.display_matches
1845+
else:
1846+
matches_to_display = matches
1847+
1848+
# Eliminate duplicates and sort
1849+
matches_to_display_set = set(matches_to_display)
1850+
matches_to_display = list(matches_to_display_set)
1851+
matches_to_display.sort()
18661852

1867-
# Display the matches
1868-
self.display_match_list_pyreadline(matches_to_display)
1853+
# Display the matches
1854+
orig_pyreadline_display(matches_to_display)
18691855

18701856
def _handle_completion_token_quote(self, raw_completion_token):
18711857
"""

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181
EXTRAS_REQUIRE = {}
8282
if sys.platform.startswith('win'):
8383
INSTALL_REQUIRES.append('pyreadline')
84+
else:
85+
INSTALL_REQUIRES.append('wcwidth')
8486
if sys.version_info < (3, 5):
8587
INSTALL_REQUIRES.append('contextlib2')
8688
if sys.version_info < (3, 4):

tox.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ deps =
2222
pytest-xdist
2323
six
2424
subprocess32
25+
wcwidth
2526
commands =
2627
py.test {posargs: -n 2} --cov=cmd2 --cov-report=term-missing --forked
2728
codecov
@@ -52,6 +53,7 @@ deps =
5253
pytest-forked
5354
pytest-xdist
5455
six
56+
wcwidth
5557
commands = py.test -v -n2 --forked
5658

5759
[testenv:py35]
@@ -63,6 +65,7 @@ deps =
6365
pytest-forked
6466
pytest-xdist
6567
six
68+
wcwidth
6669
commands = py.test -v -n2 --forked
6770

6871
[testenv:py35-win]
@@ -87,6 +90,7 @@ deps =
8790
pytest-forked
8891
pytest-xdist
8992
six
93+
wcwidth
9094
commands =
9195
py.test {posargs: -n 2} --cov=cmd2 --cov-report=term-missing --forked
9296
codecov
@@ -111,5 +115,6 @@ deps =
111115
pytest-forked
112116
pytest-xdist
113117
six
118+
wcwidth
114119
commands = py.test -v -n2 --forked
115120

0 commit comments

Comments
 (0)