Skip to content

Commit 5d5d5bf

Browse files
committed
A bit of refactoring to tab completion. Shortcuts not beginning with symbols now tab complete.
1 parent 5400403 commit 5d5d5bf

1 file changed

Lines changed: 65 additions & 37 deletions

File tree

cmd2.py

100755100644
Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,11 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict):
182182
"""
183183
completions = []
184184

185-
# Get all tokens prior to the text being completed
185+
# Get all tokens prior to text being completed
186186
try:
187187
tokens = shlex.split(line[:begidx], posix=POSIX_SHLEX)
188188
except ValueError:
189-
# Invalid syntax for shlex (Probably due to missing closing quotes)
189+
# Invalid syntax for shlex (Probably due to missing closing quote)
190190
return completions
191191

192192
# Must have at least the command and one argument
@@ -197,7 +197,7 @@ def flag_based_complete(text, line, begidx, endidx, flag_dict):
197197

198198
# Check if this flag does completions using an Iterable
199199
if isinstance(flag_dict[flag], collections.Iterable):
200-
strs_to_match = (flag_dict[flag])
200+
strs_to_match = flag_dict[flag]
201201
completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
202202

203203
# If there is only 1 match and it's at the end of the line, then add a space
@@ -232,11 +232,11 @@ def index_based_complete(text, line, begidx, endidx, index_dict):
232232
"""
233233
completions = []
234234

235-
# Get all tokens prior to the text being completed
235+
# Get all tokens prior to text being completed
236236
try:
237237
tokens = shlex.split(line[:begidx], posix=POSIX_SHLEX)
238238
except ValueError:
239-
# Invalid syntax for shlex (Probably due to missing closing quotes)
239+
# Invalid syntax for shlex (Probably due to missing closing quote)
240240
return completions
241241

242242
# Must have at least the command
@@ -247,7 +247,7 @@ def index_based_complete(text, line, begidx, endidx, index_dict):
247247

248248
# Check if this index does completions using an Iterable
249249
if isinstance(index_dict[index], collections.Iterable):
250-
strs_to_match = (index_dict[index])
250+
strs_to_match = index_dict[index]
251251
completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
252252

253253
# If there is only 1 match and it's at the end of the line, then add a space
@@ -1259,25 +1259,23 @@ def complete(self, text, state):
12591259
# If begidx is greater than 0, then the cursor is past the command
12601260
if begidx > 0:
12611261

1262-
# Expand command shortcuts
1263-
for (shortcut, expansion) in self.shortcuts:
1264-
if line.startswith(shortcut):
1265-
# If the next character after the shortcut isn't a space, then insert one
1266-
shortcut_len = len(shortcut)
1267-
if len(line) == shortcut_len or line[shortcut_len] != ' ':
1268-
expansion += ' '
1262+
# Parse the command line
1263+
command, args, expanded_line = self.parseline(line)
12691264

1270-
# Expand the shortcut
1271-
line = line.replace(shortcut, expansion, 1)
1265+
# We overwrote line with a properly formatted but fully stripped version
1266+
# Restore the end spaces from the original since line is only supposed to be
1267+
# lstripped when passed to completer functions according to Python docs
1268+
rstripped_len = len(origline) - len(origline.rstrip())
1269+
expanded_line += ' ' * rstripped_len
12721270

1273-
# Calculate difference in length to adjust indexes
1274-
diff = len(expansion) - len(shortcut)
1275-
begidx += diff
1276-
endidx += diff
1277-
break
1271+
# Fix the index values if expanded_line has a different size than line
1272+
if len(expanded_line) != len(line):
1273+
diff = len(expanded_line) - len(line)
1274+
begidx += diff
1275+
endidx += diff
12781276

1279-
# Parse the command line
1280-
command, args, foo = self.parseline(line)
1277+
# Overwrite line to pass into completers
1278+
line = expanded_line
12811279

12821280
if command == '':
12831281
compfunc = self.completedefault
@@ -1303,10 +1301,34 @@ def complete(self, text, state):
13031301
compfunc = getattr(self, 'complete_' + command)
13041302
except AttributeError:
13051303
compfunc = self.completedefault
1304+
1305+
# Call the completer function
1306+
self.completion_matches = compfunc(text, line, begidx, endidx)
1307+
13061308
else:
1307-
compfunc = self.completenames
1309+
# Complete the command against command names and shortcuts. By design, shortcuts that start with
1310+
# symbols not in self.identchars won't be tab completed since they are handled in the above if
1311+
# statement. This includes shortcuts like: ?, !, @, @@
1312+
strs_to_match = []
1313+
1314+
# If a command has been started, then match against shortcuts. This keeps shortcuts out of the
1315+
# full list of commands that show up when tab completion is done on an empty line.
1316+
if len(line) > 0:
1317+
for (shortcut, expansion) in self.shortcuts:
1318+
strs_to_match.append(shortcut)
1319+
1320+
# Get command names
1321+
do_text = 'do_' + text
1322+
strs_to_match.extend([cur_name[3:] for cur_name in self.get_names() if cur_name.startswith(do_text)])
1323+
1324+
# Perform matching
1325+
completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
13081326

1309-
self.completion_matches = compfunc(text, line, begidx, endidx)
1327+
# If there is only 1 match and it's at the end of the line, then add a space
1328+
if len(completions) == 1 and endidx == len(line):
1329+
completions[0] += ' '
1330+
1331+
self.completion_matches = completions
13101332

13111333
try:
13121334
return self.completion_matches[state]
@@ -1399,13 +1421,25 @@ def parseline(self, line):
13991421
# Expand command shortcuts to the full command name
14001422
for (shortcut, expansion) in self.shortcuts:
14011423
if line.startswith(shortcut):
1402-
line = line.replace(shortcut, expansion + ' ', 1)
1424+
# If the next character after the shortcut isn't a space, then insert one
1425+
shortcut_len = len(shortcut)
1426+
if len(line) == shortcut_len or line[shortcut_len] != ' ':
1427+
expansion += ' '
1428+
1429+
# Expand the shortcut
1430+
line = line.replace(shortcut, expansion, 1)
14031431
break
14041432

14051433
i, n = 0, len(line)
14061434
while i < n and line[i] in self.identchars:
14071435
i += 1
14081436
command, arg = line[:i], line[i:].strip()
1437+
1438+
# Make sure there is a space between the command and args
1439+
# This can occur when a character not in self.identchars bumps against the command (ex: help@)
1440+
if len(command) > 0 and len(arg) > 0 and line[len(command)] != ' ':
1441+
line = line.replace(command, command + ' ', 1)
1442+
14091443
return command, arg, line
14101444

14111445
def onecmd_plus_hooks(self, line):
@@ -2157,10 +2191,8 @@ def do_pyscript(self, arglist):
21572191
# Restore command line arguments to original state
21582192
sys.argv = orig_args
21592193

2160-
@staticmethod
2161-
def complete_pyscript(text, line, begidx, endidx):
2162-
"""Readline tab-completion method for completing pyscript command."""
2163-
return path_complete(text, line, begidx, endidx)
2194+
# Enable tab-completion for pyscript command
2195+
complete_pyscript = functools.partial(path_complete)
21642196

21652197
# Only include the do_ipy() method if IPython is available on the system
21662198
if ipython_available:
@@ -2301,10 +2333,8 @@ def do_edit(self, arglist):
23012333
else:
23022334
os.system('"{}"'.format(self.editor))
23032335

2304-
@staticmethod
2305-
def complete_edit(text, line, begidx, endidx):
2306-
"""Readline tab-completion method for completing edit command."""
2307-
return path_complete(text, line, begidx, endidx)
2336+
# Enable tab-completion for edit command
2337+
complete_edit = functools.partial(path_complete)
23082338

23092339
@property
23102340
def _current_script_dir(self):
@@ -2392,10 +2422,8 @@ def do_load(self, arglist):
23922422

23932423
self._script_dir.append(os.path.dirname(expanded_path))
23942424

2395-
@staticmethod
2396-
def complete_load(text, line, begidx, endidx):
2397-
"""Readline tab-completion method for completing load command."""
2398-
return path_complete(text, line, begidx, endidx)
2425+
# Enable tab-completion for load command
2426+
complete_load = functools.partial(path_complete)
23992427

24002428
@staticmethod
24012429
def is_text_file(file_path):

0 commit comments

Comments
 (0)