Skip to content

Commit c773a79

Browse files
committed
Fixed multiline command highlighting in Cmd2Lexer
1 parent df99fc8 commit c773a79

File tree

2 files changed

+90
-66
lines changed

2 files changed

+90
-66
lines changed

cmd2/pt_utils.py

Lines changed: 72 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -219,77 +219,84 @@ def __init__(
219219

220220
def lex_document(self, document: Document) -> Callable[[int], Any]:
221221
"""Lex the document."""
222+
# Get redirection tokens and terminators to avoid highlighting them as values
223+
exclude_tokens = set(constants.REDIRECTION_TOKENS)
224+
exclude_tokens.update(self.cmd_app.statement_parser.terminators)
225+
arg_pattern = re.compile(r'(\s+)|(--?[^\s\'"]+)|("[^"]*"?|\'[^\']*\'?)|([^\s\'"]+)')
226+
227+
def highlight_args(text: str, tokens: list[tuple[str, str]]) -> None:
228+
"""Highlight arguments in a string."""
229+
for m in arg_pattern.finditer(text):
230+
space, flag, quoted, word = m.groups()
231+
match_text = m.group(0)
232+
233+
if space:
234+
tokens.append(('', match_text))
235+
elif flag:
236+
tokens.append((self.flag_color, match_text))
237+
elif (quoted or word) and match_text not in exclude_tokens:
238+
tokens.append((self.argument_color, match_text))
239+
else:
240+
tokens.append(('', match_text))
222241

223242
def get_line(lineno: int) -> list[tuple[str, str]]:
224243
"""Return the tokens for the given line number."""
225244
line = document.lines[lineno]
226245
tokens: list[tuple[str, str]] = []
227246

228-
# Use cmd2's command pattern to find the first word (the command)
229-
if ru.ALLOW_STYLE != ru.AllowStyle.NEVER and (
230-
match := self.cmd_app.statement_parser._command_pattern.search(line)
231-
):
232-
# Group 1 is the command, Group 2 is the character(s) that terminated the command match
233-
command = match.group(1)
234-
cmd_start = match.start(1)
235-
cmd_end = match.end(1)
236-
237-
# Add any leading whitespace
238-
if cmd_start > 0:
239-
tokens.append(('', line[:cmd_start]))
240-
241-
if command:
242-
# Determine the style for the command
243-
shortcut_found = False
244-
for shortcut, _ in self.cmd_app.statement_parser.shortcuts:
245-
if command.startswith(shortcut):
246-
# Add the shortcut with the command style
247-
tokens.append((self.command_color, shortcut))
248-
249-
# If there's more in the command word, it's an argument
250-
if len(command) > len(shortcut):
251-
tokens.append((self.argument_color, command[len(shortcut) :]))
252-
253-
shortcut_found = True
254-
break
255-
256-
if not shortcut_found:
257-
style = ''
258-
if command in self.cmd_app.get_all_commands():
259-
style = self.command_color
260-
elif command in self.cmd_app.aliases:
261-
style = self.alias_color
262-
elif command in self.cmd_app.macros:
263-
style = self.macro_color
264-
265-
# Add the command with the determined style
266-
tokens.append((style, command))
267-
268-
# Add the rest of the line
269-
if cmd_end < len(line):
270-
rest = line[cmd_end:]
271-
# Regex to match whitespace, flags, quoted strings, or other words
272-
arg_pattern = re.compile(r'(\s+)|(--?[^\s\'"]+)|("[^"]*"?|\'[^\']*\'?)|([^\s\'"]+)')
273-
274-
# Get redirection tokens and terminators to avoid highlighting them as values
275-
exclude_tokens = set(constants.REDIRECTION_TOKENS)
276-
exclude_tokens.update(self.cmd_app.statement_parser.terminators)
277-
278-
for m in arg_pattern.finditer(rest):
279-
space, flag, quoted, word = m.groups()
280-
text = m.group(0)
281-
282-
if space:
283-
tokens.append(('', text))
284-
elif flag:
285-
tokens.append((self.flag_color, text))
286-
elif (quoted or word) and text not in exclude_tokens:
287-
tokens.append((self.argument_color, text))
288-
else:
289-
tokens.append(('', text))
290-
elif line:
291-
# No command match found or colors aren't allowed, add the entire line unstyled
292-
tokens.append(('', line))
247+
# Only attempt to match a command on the first line
248+
if lineno == 0 and ru.ALLOW_STYLE != ru.AllowStyle.NEVER:
249+
# Use cmd2's command pattern to find the first word (the command)
250+
match = self.cmd_app.statement_parser._command_pattern.search(line)
251+
if match:
252+
# Group 1 is the command, Group 2 is the character(s) that terminated the command match
253+
command = match.group(1)
254+
cmd_start = match.start(1)
255+
cmd_end = match.end(1)
256+
257+
# Add any leading whitespace
258+
if cmd_start > 0:
259+
tokens.append(('', line[:cmd_start]))
260+
261+
if command:
262+
# Determine the style for the command
263+
shortcut_found = False
264+
for shortcut, _ in self.cmd_app.statement_parser.shortcuts:
265+
if command.startswith(shortcut):
266+
# Add the shortcut with the command style
267+
tokens.append((self.command_color, shortcut))
268+
269+
# If there's more in the command word, it's an argument
270+
if len(command) > len(shortcut):
271+
tokens.append((self.argument_color, command[len(shortcut) :]))
272+
273+
shortcut_found = True
274+
break
275+
276+
if not shortcut_found:
277+
style = ''
278+
if command in self.cmd_app.get_all_commands():
279+
style = self.command_color
280+
elif command in self.cmd_app.aliases:
281+
style = self.alias_color
282+
elif command in self.cmd_app.macros:
283+
style = self.macro_color
284+
285+
# Add the command with the determined style
286+
tokens.append((style, command))
287+
288+
# Add the rest of the line as arguments
289+
if cmd_end < len(line):
290+
highlight_args(line[cmd_end:], tokens)
291+
else:
292+
# No command match found on the first line
293+
tokens.append(('', line))
294+
else:
295+
# All other lines are unstyled or treated as arguments
296+
if ru.ALLOW_STYLE != ru.AllowStyle.NEVER:
297+
highlight_args(line, tokens)
298+
else:
299+
tokens.append(('', line))
293300

294301
return tokens
295302

tests/test_pt_utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,30 @@ def test_lex_document_shortcut(self, mock_cmd_app):
210210
tokens = get_line(0)
211211
assert tokens == [('ansigreen', '!'), ('ansiyellow', 'ls')]
212212

213-
# Case 2: Shortcut with space
214213
line = "! ls"
215214
document = Document(line)
216215
get_line = lexer.lex_document(document)
217216
tokens = get_line(0)
218217
assert tokens == [('ansigreen', '!'), ('', ' '), ('ansiyellow', 'ls')]
219218

219+
def test_lex_document_multiline(self, mock_cmd_app):
220+
"""Test lexing a multiline command."""
221+
mock_cmd_app.all_commands = ["orate"]
222+
lexer = pt_utils.Cmd2Lexer(cast(Any, mock_cmd_app))
223+
224+
# Command on first line, argument on second line that looks like a command
225+
line = "orate\nhelp"
226+
document = Document(line)
227+
get_line = lexer.lex_document(document)
228+
229+
# First line should have command
230+
tokens0 = get_line(0)
231+
assert tokens0 == [('ansigreen', 'orate')]
232+
233+
# Second line should have argument (not command)
234+
tokens1 = get_line(1)
235+
assert tokens1 == [('ansiyellow', 'help')]
236+
220237

221238
class TestCmd2Completer:
222239
def test_get_completions(self, mock_cmd_app: MockCmd, monkeypatch) -> None:

0 commit comments

Comments
 (0)