From fb7e2092e343b3587715d23ea03a78caf437c046 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 20 Oct 2026 16:22:14 -0700 Subject: [PATCH 1/2] Highlight soft keywords after `token.NL` --- Lib/_pyrepl/utils.py | 12 ++--- Lib/test/test_pyrepl/test_utils.py | 46 +++++++++++++++++++ ...-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst | 2 + 3 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index e77001f6a1d10b..3a369b4dde6b7f 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -266,14 +266,14 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: trace("is_soft_keyword_used{t}", t=tokens) match tokens: case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT) | TI(string=":"), TI(string="match"), TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START) | TI(T.OP, string="(" | "*" | "-" | "+" | "[" | "{" | "~" | "...") ): return True case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT) | TI(string=":"), TI(string="match"), TI(T.NAME, string=s) ): @@ -281,14 +281,14 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: return s in keyword_first_sets_match return True case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="case"), TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START) | TI(T.OP, string="(" | "*" | "-" | "+" | "[" | "{") ): return True case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="case"), TI(T.NAME, string=s) ): @@ -298,13 +298,13 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: case (TI(string="case"), TI(string="_"), TI(string=":")): return True case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="type"), TI(T.NAME, string=s) ): return not keyword.iskeyword(s) case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":" | ";"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":" | ";"), TI(string="lazy"), TI(string="import") | TI(string="from") ): diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index e87d14304c8225..be6aa76e47a4ba 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -160,6 +160,52 @@ def test_gen_colors_keyword_highlighting(self): actual_highlights.append((span_text, color.tag)) self.assertEqual(actual_highlights, expected_highlights) + def test_gen_colors_soft_keywords_after_blank_lines(self): + cases = [ + ( + "def f(): pass\n", + "match -1:\n case _: pass", + ["match", "case", "_"], + ), + ( + "def f(): pass\n", + "match value:\n case _: pass", + ["match", "case", "_"], + ), + ( + "match value:\n case 0: pass\n", + " case -1: pass", + ["match", "case", "case"], + ), + ( + "match value:\n case 0: pass\n", + " case other: pass", + ["match", "case", "case"], + ), + ("def f(): pass\n", "type Alias = int", ["type"]), + ("def f(): pass\n", "lazy import mod", ["lazy"]), + ("def f(): pass\n", "lazy from mod import name", ["lazy"]), + ] + for prefix, statement, expected in cases: + for blank_line in ("\n", " \n", "# comment\n"): + code = prefix + blank_line + statement + with self.subTest(code=code): + actual = [ + code[color.span.start:color.span.end + 1] + for color in gen_colors(code) + if color.tag == "soft_keyword" + ] + self.assertEqual(actual, expected) + + def test_gen_colors_soft_keyword_names_in_brackets(self): + for opening, closing in ("()", "[]", "{}"): + for name in ("match", "case"): + code = f"{opening}\n{name} - 1\n{closing}" + with self.subTest(code=code): + self.assertNotIn( + "soft_keyword", [color.tag for color in gen_colors(code)] + ) + def test_gen_colors_command_highlighting(self): cases = [ # highlights bare command names (after stripping whitespaces) diff --git a/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst b/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst new file mode 100644 index 00000000000000..bf78e8c740d91d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst @@ -0,0 +1,2 @@ +Fix soft keyword highlighting after blank and comment-only lines in the +:ref:`interactive shell `. Patch by Bartosz Sławecki. From b701ca16c86f16e5276fc91f41957e8a59ef42ea Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 20 Oct 2026 17:26:08 -0700 Subject: [PATCH 2/2] Highlight match statements after dedents --- Lib/_pyrepl/utils.py | 4 ++-- Lib/test/test_pyrepl/test_utils.py | 15 +++++++++++++++ ...2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst | 5 +++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index 3a369b4dde6b7f..77b1fd1eed81ab 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -266,14 +266,14 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: trace("is_soft_keyword_used{t}", t=tokens) match tokens: case ( - None | TI(T.NEWLINE | T.NL | T.INDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="match"), TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START) | TI(T.OP, string="(" | "*" | "-" | "+" | "[" | "{" | "~" | "...") ): return True case ( - None | TI(T.NEWLINE | T.NL | T.INDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="match"), TI(T.NAME, string=s) ): diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index be6aa76e47a4ba..1cb9961e0082ed 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -197,6 +197,21 @@ def test_gen_colors_soft_keywords_after_blank_lines(self): ] self.assertEqual(actual, expected) + def test_gen_colors_match_after_dedent(self): + for subject in ("-1", "value"): + for separator in ("", "\n", "# comment\n"): + code = ( + "if x:\n pass\n" + f"{separator}match {subject}:\n case _: pass" + ) + with self.subTest(code=code): + actual = [ + code[color.span.start:color.span.end + 1] + for color in gen_colors(code) + if color.tag == "soft_keyword" + ] + self.assertEqual(actual, ["match", "case", "_"]) + def test_gen_colors_soft_keyword_names_in_brackets(self): for opening, closing in ("()", "[]", "{}"): for name in ("match", "case"): diff --git a/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst b/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst index bf78e8c740d91d..506c71ca82bdba 100644 --- a/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst +++ b/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst @@ -1,2 +1,3 @@ -Fix soft keyword highlighting after blank and comment-only lines in the -:ref:`interactive shell `. Patch by Bartosz Sławecki. +Fix soft keyword highlighting after blank lines, comment-only lines, and +dedents in the :ref:`interactive shell `. Patch by Bartosz +Sławecki.