bpo-44010: IDLE: colorize pattern-matching soft keywords (GH-25851)

This commit is contained in:
Tal Einat 2021-05-19 12:18:10 +03:00 committed by GitHub
parent d798acc873
commit 60d343a816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 345 additions and 73 deletions

View File

@ -613,6 +613,12 @@ keywords, builtin class and function names, names following ``class`` and
``def``, strings, and comments. For any text window, these are the cursor (when ``def``, strings, and comments. For any text window, these are the cursor (when
present), found text (when possible), and selected text. present), found text (when possible), and selected text.
IDLE also highlights the :ref:`soft keywords <soft-keywords>` :keyword:`match`,
:keyword:`case <match>`, and :keyword:`_ <wildcard-patterns>` in
pattern-matching statements. However, this highlighting is not perfect and
will be incorrect in some rare cases, including some ``_``-s in ``case``
patterns.
Text coloring is done in the background, so uncolorized text is occasionally Text coloring is done in the background, so uncolorized text is occasionally
visible. To change the color scheme, use the Configure IDLE dialog visible. To change the color scheme, use the Configure IDLE dialog
Highlighting tab. The marking of debugger breakpoint lines in the editor and Highlighting tab. The marking of debugger breakpoint lines in the editor and

View File

@ -1030,6 +1030,12 @@ Terry Jan Reedy in :issue:`37892`.)
We expect to backport these shell changes to a future 3.9 maintenance We expect to backport these shell changes to a future 3.9 maintenance
release. release.
Highlight the new :ref:`soft keywords <soft-keywords>` :keyword:`match`,
:keyword:`case <match>`, and :keyword:`_ <wildcard-patterns>` in
pattern-matching statements. However, this highlighting is not perfect
and will be incorrect in some rare cases, including some ``_``-s in
``case`` patterns. (Contributed by Tal Einat in bpo-44010.)
importlib.metadata importlib.metadata
------------------ ------------------

View File

@ -16,6 +16,32 @@ def any(name, alternates):
def make_pat(): def make_pat():
kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b" kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b"
match_softkw = (
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<MATCH_SOFTKW>match)\b" +
r"(?![ \t]*(?:" + "|".join([ # not followed by ...
r"[:,;=^&|@~)\]}]", # a character which means it can't be a
# pattern-matching statement
r"\b(?:" + r"|".join(keyword.kwlist) + r")\b", # a keyword
]) +
r"))"
)
case_default = (
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<CASE_SOFTKW>case)" +
r"[ \t]+(?P<CASE_DEFAULT_UNDERSCORE>_\b)"
)
case_softkw_and_pattern = (
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<CASE_SOFTKW2>case)\b" +
r"(?![ \t]*(?:" + "|".join([ # not followed by ...
r"_\b", # a lone underscore
r"[:,;=^&|@~)\]}]", # a character which means it can't be a
# pattern-matching case
r"\b(?:" + r"|".join(keyword.kwlist) + r")\b", # a keyword
]) +
r"))"
)
builtinlist = [str(name) for name in dir(builtins) builtinlist = [str(name) for name in dir(builtins)
if not name.startswith('_') and if not name.startswith('_') and
name not in keyword.kwlist] name not in keyword.kwlist]
@ -27,12 +53,29 @@ def make_pat():
sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?' dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
string = any("STRING", [sq3string, dq3string, sqstring, dqstring]) string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
return (kw + "|" + builtin + "|" + comment + "|" + string + prog = re.compile("|".join([
"|" + any("SYNC", [r"\n"])) builtin, comment, string, kw,
match_softkw, case_default,
case_softkw_and_pattern,
any("SYNC", [r"\n"]),
]),
re.DOTALL | re.MULTILINE)
return prog
prog = re.compile(make_pat(), re.S) prog = make_pat()
idprog = re.compile(r"\s+(\w+)", re.S) idprog = re.compile(r"\s+(\w+)")
prog_group_name_to_tag = {
"MATCH_SOFTKW": "KEYWORD",
"CASE_SOFTKW": "KEYWORD",
"CASE_DEFAULT_UNDERSCORE": "KEYWORD",
"CASE_SOFTKW2": "KEYWORD",
}
def matched_named_groups(re_match):
"Get only the non-empty named groups from an re.Match object."
return ((k, v) for (k, v) in re_match.groupdict().items() if v)
def color_config(text): def color_config(text):
@ -231,14 +274,10 @@ class ColorDelegator(Delegator):
def recolorize_main(self): def recolorize_main(self):
"Evaluate text and apply colorizing tags." "Evaluate text and apply colorizing tags."
next = "1.0" next = "1.0"
while True: while todo_tag_range := self.tag_nextrange("TODO", next):
item = self.tag_nextrange("TODO", next) self.tag_remove("SYNC", todo_tag_range[0], todo_tag_range[1])
if not item: sync_tag_range = self.tag_prevrange("SYNC", todo_tag_range[0])
break head = sync_tag_range[1] if sync_tag_range else "1.0"
head, tail = item
self.tag_remove("SYNC", head, tail)
item = self.tag_prevrange("SYNC", head)
head = item[1] if item else "1.0"
chars = "" chars = ""
next = head next = head
@ -256,23 +295,8 @@ class ColorDelegator(Delegator):
return return
for tag in self.tagdefs: for tag in self.tagdefs:
self.tag_remove(tag, mark, next) self.tag_remove(tag, mark, next)
chars = chars + line chars += line
m = self.prog.search(chars) self._add_tags_in_section(chars, head)
while m:
for key, value in m.groupdict().items():
if value:
a, b = m.span(key)
self.tag_add(key,
head + "+%dc" % a,
head + "+%dc" % b)
if value in ("def", "class"):
m1 = self.idprog.match(chars, b)
if m1:
a, b = m1.span(1)
self.tag_add("DEFINITION",
head + "+%dc" % a,
head + "+%dc" % b)
m = self.prog.search(chars, m.end())
if "SYNC" in self.tag_names(next + "-1c"): if "SYNC" in self.tag_names(next + "-1c"):
head = next head = next
chars = "" chars = ""
@ -291,6 +315,40 @@ class ColorDelegator(Delegator):
if DEBUG: print("colorizing stopped") if DEBUG: print("colorizing stopped")
return return
def _add_tag(self, start, end, head, matched_group_name):
"""Add a tag to a given range in the text widget.
This is a utility function, receiving the range as `start` and
`end` positions, each of which is a number of characters
relative to the given `head` index in the text widget.
The tag to add is determined by `matched_group_name`, which is
the name of a regular expression "named group" as matched by
by the relevant highlighting regexps.
"""
tag = prog_group_name_to_tag.get(matched_group_name,
matched_group_name)
self.tag_add(tag,
f"{head}+{start:d}c",
f"{head}+{end:d}c")
def _add_tags_in_section(self, chars, head):
"""Parse and add highlighting tags to a given part of the text.
`chars` is a string with the text to parse and to which
highlighting is to be applied.
`head` is the index in the text widget where the text is found.
"""
for m in self.prog.finditer(chars):
for name, matched_text in matched_named_groups(m):
a, b = m.span(name)
self._add_tag(a, b, head, name)
if matched_text in ("def", "class"):
if m1 := self.idprog.match(chars, b):
a, b = m1.span(1)
self._add_tag(a, b, head, "DEFINITION")
def removecolors(self): def removecolors(self):
"Remove all colorizing tags." "Remove all colorizing tags."
for tag in self.tagdefs: for tag in self.tagdefs:
@ -299,27 +357,14 @@ class ColorDelegator(Delegator):
def _color_delegator(parent): # htest # def _color_delegator(parent): # htest #
from tkinter import Toplevel, Text from tkinter import Toplevel, Text
from idlelib.idle_test.test_colorizer import source
from idlelib.percolator import Percolator from idlelib.percolator import Percolator
top = Toplevel(parent) top = Toplevel(parent)
top.title("Test ColorDelegator") top.title("Test ColorDelegator")
x, y = map(int, parent.geometry().split('+')[1:]) x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("700x250+%d+%d" % (x + 20, y + 175)) top.geometry("700x550+%d+%d" % (x + 20, y + 175))
source = (
"if True: int ('1') # keyword, builtin, string, comment\n"
"elif False: print(0)\n"
"else: float(None)\n"
"if iF + If + IF: 'keyword matching must respect case'\n"
"if'': x or'' # valid keyword-string no-space combinations\n"
"async def f(): await g()\n"
"# All valid prefixes for unicode and byte strings should be colored.\n"
"'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
"r'x', u'x', R'x', U'x', f'x', F'x'\n"
"fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'\n"
"b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x', rB'x',Rb'x',RB'x'\n"
"# Invalid combinations of legal characters should be half colored.\n"
"ur'x', ru'x', uf'x', fu'x', UR'x', ufr'x', rfu'x', xf'x', fx'x'\n"
)
text = Text(top, background="white") text = Text(top, background="white")
text.pack(expand=1, fill="both") text.pack(expand=1, fill="both")
text.insert("insert", source) text.insert("insert", source)

View File

@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IDLE &#8212; Python 3.10.0a6 documentation</title> <title>IDLE &#8212; Python 3.11.0a0 documentation</title>
<link rel="stylesheet" href="../_static/pydoctheme.css" type="text/css" /> <link rel="stylesheet" href="../_static/pydoctheme.css" type="text/css" />
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
@ -18,7 +18,7 @@
<script src="../_static/sidebar.js"></script> <script src="../_static/sidebar.js"></script>
<link rel="search" type="application/opensearchdescription+xml" <link rel="search" type="application/opensearchdescription+xml"
title="Search within Python 3.10.0a6 documentation" title="Search within Python 3.11.0a0 documentation"
href="../_static/opensearch.xml"/> href="../_static/opensearch.xml"/>
<link rel="author" title="About these documents" href="../about.html" /> <link rel="author" title="About these documents" href="../about.html" />
<link rel="index" title="Index" href="../genindex.html" /> <link rel="index" title="Index" href="../genindex.html" />
@ -71,7 +71,7 @@
<li id="cpython-language-and-version"> <li id="cpython-language-and-version">
<a href="../index.html">3.10.0a6 Documentation</a> &#187; <a href="../index.html">3.11.0a0 Documentation</a> &#187;
</li> </li>
<li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> &#187;</li> <li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> &#187;</li>
@ -102,7 +102,7 @@
<div class="section" id="idle"> <div class="section" id="idle">
<span id="id1"></span><h1>IDLE<a class="headerlink" href="#idle" title="Permalink to this headline"></a></h1> <span id="id1"></span><h1>IDLE<a class="headerlink" href="#idle" title="Permalink to this headline"></a></h1>
<p><strong>Source code:</strong> <a class="reference external" href="https://github.com/python/cpython/tree/master/Lib/idlelib/">Lib/idlelib/</a></p> <p><strong>Source code:</strong> <a class="reference external" href="https://github.com/python/cpython/tree/main/Lib/idlelib/">Lib/idlelib/</a></p>
<hr class="docutils" id="index-0" /> <hr class="docutils" id="index-0" />
<p>IDLE is Pythons Integrated Development and Learning Environment.</p> <p>IDLE is Pythons Integrated Development and Learning Environment.</p>
<p>IDLE has the following features:</p> <p>IDLE has the following features:</p>
@ -581,6 +581,11 @@ user error. For Python code, at the shell prompt or in an editor, these are
keywords, builtin class and function names, names following <code class="docutils literal notranslate"><span class="pre">class</span></code> and keywords, builtin class and function names, names following <code class="docutils literal notranslate"><span class="pre">class</span></code> and
<code class="docutils literal notranslate"><span class="pre">def</span></code>, strings, and comments. For any text window, these are the cursor (when <code class="docutils literal notranslate"><span class="pre">def</span></code>, strings, and comments. For any text window, these are the cursor (when
present), found text (when possible), and selected text.</p> present), found text (when possible), and selected text.</p>
<p>IDLE also highlights the <a class="reference internal" href="../reference/lexical_analysis.html#soft-keywords"><span class="std std-ref">soft keywords</span></a> <a class="reference internal" href="../reference/compound_stmts.html#match"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">match</span></code></a>,
<a class="reference internal" href="../reference/compound_stmts.html#match"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">case</span></code></a>, and <a class="reference internal" href="../reference/compound_stmts.html#wildcard-patterns"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">_</span></code></a> in
pattern-matching statements. However, this highlighting is not perfect and
will be incorrect in some rare cases, including some <code class="docutils literal notranslate"><span class="pre">_</span></code>-s in <code class="docutils literal notranslate"><span class="pre">case</span></code>
patterns.</p>
<p>Text coloring is done in the background, so uncolorized text is occasionally <p>Text coloring is done in the background, so uncolorized text is occasionally
visible. To change the color scheme, use the Configure IDLE dialog visible. To change the color scheme, use the Configure IDLE dialog
Highlighting tab. The marking of debugger breakpoint lines in the editor and Highlighting tab. The marking of debugger breakpoint lines in the editor and
@ -685,7 +690,7 @@ intended to be the same as executing the same code by the default method,
directly with Python in a text-mode system console or terminal window. directly with Python in a text-mode system console or terminal window.
However, the different interface and operation occasionally affect However, the different interface and operation occasionally affect
visible results. For instance, <code class="docutils literal notranslate"><span class="pre">sys.modules</span></code> starts with more entries, visible results. For instance, <code class="docutils literal notranslate"><span class="pre">sys.modules</span></code> starts with more entries,
and <code class="docutils literal notranslate"><span class="pre">threading.activeCount()</span></code> returns 2 instead of 1.</p> and <code class="docutils literal notranslate"><span class="pre">threading.active_count()</span></code> returns 2 instead of 1.</p>
<p>By default, IDLE runs user code in a separate OS process rather than in <p>By default, IDLE runs user code in a separate OS process rather than in
the user interface process that runs the shell and editor. In the execution the user interface process that runs the shell and editor. In the execution
process, it replaces <code class="docutils literal notranslate"><span class="pre">sys.stdin</span></code>, <code class="docutils literal notranslate"><span class="pre">sys.stdout</span></code>, and <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code> process, it replaces <code class="docutils literal notranslate"><span class="pre">sys.stdin</span></code>, <code class="docutils literal notranslate"><span class="pre">sys.stdout</span></code>, and <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code>
@ -939,7 +944,7 @@ also used for testing.</p>
<ul class="this-page-menu"> <ul class="this-page-menu">
<li><a href="../bugs.html">Report a Bug</a></li> <li><a href="../bugs.html">Report a Bug</a></li>
<li> <li>
<a href="https://github.com/python/cpython/blob/master/Doc/library/idle.rst" <a href="https://github.com/python/cpython/blob/main/Doc/library/idle.rst"
rel="nofollow">Show Source rel="nofollow">Show Source
</a> </a>
</li> </li>
@ -971,7 +976,7 @@ also used for testing.</p>
<li id="cpython-language-and-version"> <li id="cpython-language-and-version">
<a href="../index.html">3.10.0a6 Documentation</a> &#187; <a href="../index.html">3.11.0a0 Documentation</a> &#187;
</li> </li>
<li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> &#187;</li> <li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> &#187;</li>
@ -997,13 +1002,19 @@ also used for testing.</p>
<div class="footer"> <div class="footer">
&copy; <a href="../copyright.html">Copyright</a> 2001-2021, Python Software Foundation. &copy; <a href="../copyright.html">Copyright</a> 2001-2021, Python Software Foundation.
<br /> <br />
This page is licensed under the Python Software Foundation License Version 2.
<br />
Examples, recipes, and other code in the documentation are additionally licensed under the Zero Clause BSD License.
<br />
See <a href="">History and License</a> for more information.
<br /><br />
The Python Software Foundation is a non-profit corporation. The Python Software Foundation is a non-profit corporation.
<a href="https://www.python.org/psf/donations/">Please donate.</a> <a href="https://www.python.org/psf/donations/">Please donate.</a>
<br /> <br />
<br /> <br />
Last updated on Mar 29, 2021. Last updated on May 11, 2021.
<a href="https://docs.python.org/3/bugs.html">Found a bug</a>? <a href="https://docs.python.org/3/bugs.html">Found a bug</a>?
<br /> <br />

View File

@ -1,11 +1,12 @@
"Test colorizer, coverage 93%." "Test colorizer, coverage 99%."
from idlelib import colorizer from idlelib import colorizer
from test.support import requires from test.support import requires
import unittest import unittest
from unittest import mock from unittest import mock
from .tkinter_testing_utils import run_in_tk_mainloop
from functools import partial from functools import partial
import textwrap
from tkinter import Tk, Text from tkinter import Tk, Text
from idlelib import config from idlelib import config
from idlelib.percolator import Percolator from idlelib.percolator import Percolator
@ -19,15 +20,38 @@ testcfg = {
'extensions': config.IdleUserConfParser(''), 'extensions': config.IdleUserConfParser(''),
} }
source = ( source = textwrap.dedent("""\
"if True: int ('1') # keyword, builtin, string, comment\n" if True: int ('1') # keyword, builtin, string, comment
"elif False: print(0) # 'string' in comment\n" elif False: print(0) # 'string' in comment
"else: float(None) # if in comment\n" else: float(None) # if in comment
"if iF + If + IF: 'keyword matching must respect case'\n" if iF + If + IF: 'keyword matching must respect case'
"if'': x or'' # valid string-keyword no-space combinations\n" if'': x or'' # valid keyword-string no-space combinations
"async def f(): await g()\n" async def f(): await g()
"'x', '''x''', \"x\", \"\"\"x\"\"\"\n" # Strings should be entirely colored, including quotes.
) 'x', '''x''', "x", \"""x\"""
'abc\\
def'
'''abc\\
def'''
# All valid prefixes for unicode and byte strings should be colored.
r'x', u'x', R'x', U'x', f'x', F'x'
fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'
b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x', rB'x',Rb'x',RB'x'
# Invalid combinations of legal characters should be half colored.
ur'x', ru'x', uf'x', fu'x', UR'x', ufr'x', rfu'x', xf'x', fx'x'
match point:
case (x, 0) as _:
print(f"X={x}")
case [_, [_], "_",
_]:
pass
case _ if ("a" if _ else set()): pass
case _:
raise ValueError("Not a point _")
'''
case _:'''
"match x:"
""")
def setUpModule(): def setUpModule():
@ -107,7 +131,7 @@ class ColorDelegatorInstantiationTest(unittest.TestCase):
requires('gui') requires('gui')
root = cls.root = Tk() root = cls.root = Tk()
root.withdraw() root.withdraw()
text = cls.text = Text(root) cls.text = Text(root)
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
@ -152,7 +176,7 @@ class ColorDelegatorTest(unittest.TestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
cls.percolator.redir.close() cls.percolator.close()
del cls.percolator, cls.text del cls.percolator, cls.text
cls.root.update_idletasks() cls.root.update_idletasks()
cls.root.destroy() cls.root.destroy()
@ -364,8 +388,21 @@ class ColorDelegatorTest(unittest.TestCase):
('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()), ('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()),
('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)), ('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)),
('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()), ('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()),
('7.0', ('STRING',)), ('7.4', ()), ('7.5', ('STRING',)), ('8.0', ('STRING',)), ('8.4', ()), ('8.5', ('STRING',)),
('7.12', ()), ('7.14', ('STRING',)), ('8.12', ()), ('8.14', ('STRING',)),
('19.0', ('KEYWORD',)),
('20.4', ('KEYWORD',)), ('20.16', ('KEYWORD',)),# ('20.19', ('KEYWORD',)),
#('22.4', ('KEYWORD',)), ('22.10', ('KEYWORD',)), ('22.14', ('KEYWORD',)), ('22.19', ('STRING',)),
#('23.12', ('KEYWORD',)),
('24.8', ('KEYWORD',)),
('25.4', ('KEYWORD',)), ('25.9', ('KEYWORD',)),
('25.11', ('KEYWORD',)), ('25.15', ('STRING',)),
('25.19', ('KEYWORD',)), ('25.22', ()),
('25.24', ('KEYWORD',)), ('25.29', ('BUILTIN',)), ('25.37', ('KEYWORD',)),
('26.4', ('KEYWORD',)), ('26.9', ('KEYWORD',)),# ('26.11', ('KEYWORD',)), ('26.14', (),),
('27.25', ('STRING',)), ('27.38', ('STRING',)),
('29.0', ('STRING',)),
('30.1', ('STRING',)),
# SYNC at the end of every line. # SYNC at the end of every line.
('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)), ('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)),
) )
@ -391,11 +428,173 @@ class ColorDelegatorTest(unittest.TestCase):
eq(text.tag_nextrange('COMMENT', '2.0'), ('2.22', '2.43')) eq(text.tag_nextrange('COMMENT', '2.0'), ('2.22', '2.43'))
eq(text.tag_nextrange('SYNC', '2.0'), ('2.43', '3.0')) eq(text.tag_nextrange('SYNC', '2.0'), ('2.43', '3.0'))
eq(text.tag_nextrange('STRING', '2.0'), ('4.17', '4.53')) eq(text.tag_nextrange('STRING', '2.0'), ('4.17', '4.53'))
eq(text.tag_nextrange('STRING', '7.0'), ('7.0', '7.3')) eq(text.tag_nextrange('STRING', '8.0'), ('8.0', '8.3'))
eq(text.tag_nextrange('STRING', '7.3'), ('7.5', '7.12')) eq(text.tag_nextrange('STRING', '8.3'), ('8.5', '8.12'))
eq(text.tag_nextrange('STRING', '7.12'), ('7.14', '7.17')) eq(text.tag_nextrange('STRING', '8.12'), ('8.14', '8.17'))
eq(text.tag_nextrange('STRING', '7.17'), ('7.19', '7.26')) eq(text.tag_nextrange('STRING', '8.17'), ('8.19', '8.26'))
eq(text.tag_nextrange('SYNC', '7.0'), ('7.26', '9.0')) eq(text.tag_nextrange('SYNC', '8.0'), ('8.26', '9.0'))
eq(text.tag_nextrange('SYNC', '30.0'), ('30.10', '32.0'))
def _assert_highlighting(self, source, tag_ranges):
"""Check highlighting of a given piece of code.
This inserts just this code into the Text widget. It will then
check that the resulting highlighting tag ranges exactly match
those described in the given `tag_ranges` dict.
Note that the irrelevant tags 'sel', 'TODO' and 'SYNC' are
ignored.
"""
text = self.text
with mock.patch.object(colorizer.ColorDelegator, 'notify_range'):
text.delete('1.0', 'end-1c')
text.insert('insert', source)
text.tag_add('TODO', '1.0', 'end-1c')
self.color.recolorize_main()
# Make a dict with highlighting tag ranges in the Text widget.
text_tag_ranges = {}
for tag in set(text.tag_names()) - {'sel', 'TODO', 'SYNC'}:
indexes = [rng.string for rng in text.tag_ranges(tag)]
for index_pair in zip(indexes[::2], indexes[1::2]):
text_tag_ranges.setdefault(tag, []).append(index_pair)
self.assertEqual(text_tag_ranges, tag_ranges)
with mock.patch.object(colorizer.ColorDelegator, 'notify_range'):
text.delete('1.0', 'end-1c')
def test_def_statement(self):
# empty def
self._assert_highlighting('def', {'KEYWORD': [('1.0', '1.3')]})
# def followed by identifier
self._assert_highlighting('def foo:', {'KEYWORD': [('1.0', '1.3')],
'DEFINITION': [('1.4', '1.7')]})
# def followed by partial identifier
self._assert_highlighting('def fo', {'KEYWORD': [('1.0', '1.3')],
'DEFINITION': [('1.4', '1.6')]})
# def followed by non-keyword
self._assert_highlighting('def ++', {'KEYWORD': [('1.0', '1.3')]})
def test_match_soft_keyword(self):
# empty match
self._assert_highlighting('match', {'KEYWORD': [('1.0', '1.5')]})
# match followed by partial identifier
self._assert_highlighting('match fo', {'KEYWORD': [('1.0', '1.5')]})
# match followed by identifier and colon
self._assert_highlighting('match foo:', {'KEYWORD': [('1.0', '1.5')]})
# match followed by keyword
self._assert_highlighting('match and', {'KEYWORD': [('1.6', '1.9')]})
# match followed by builtin with keyword prefix
self._assert_highlighting('match int:', {'KEYWORD': [('1.0', '1.5')],
'BUILTIN': [('1.6', '1.9')]})
# match followed by non-text operator
self._assert_highlighting('match^', {})
self._assert_highlighting('match @', {})
# match followed by colon
self._assert_highlighting('match :', {})
# match followed by comma
self._assert_highlighting('match\t,', {})
# match followed by a lone underscore
self._assert_highlighting('match _:', {'KEYWORD': [('1.0', '1.5')]})
def test_case_soft_keyword(self):
# empty case
self._assert_highlighting('case', {'KEYWORD': [('1.0', '1.4')]})
# case followed by partial identifier
self._assert_highlighting('case fo', {'KEYWORD': [('1.0', '1.4')]})
# case followed by identifier and colon
self._assert_highlighting('case foo:', {'KEYWORD': [('1.0', '1.4')]})
# case followed by keyword
self._assert_highlighting('case and', {'KEYWORD': [('1.5', '1.8')]})
# case followed by builtin with keyword prefix
self._assert_highlighting('case int:', {'KEYWORD': [('1.0', '1.4')],
'BUILTIN': [('1.5', '1.8')]})
# case followed by non-text operator
self._assert_highlighting('case^', {})
self._assert_highlighting('case @', {})
# case followed by colon
self._assert_highlighting('case :', {})
# case followed by comma
self._assert_highlighting('case\t,', {})
# case followed by a lone underscore
self._assert_highlighting('case _:', {'KEYWORD': [('1.0', '1.4'),
('1.5', '1.6')]})
def test_long_multiline_string(self):
source = textwrap.dedent('''\
"""a
b
c
d
e"""
''')
self._assert_highlighting(source, {'STRING': [('1.0', '5.4')]})
@run_in_tk_mainloop
def test_incremental_editing(self):
text = self.text
eq = self.assertEqual
# Simulate typing 'inte'. During this, the highlighting should
# change from normal to keyword to builtin to normal.
text.insert('insert', 'i')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
text.insert('insert', 'n')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2'))
text.insert('insert', 't')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ('1.0', '1.3'))
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
text.insert('insert', 'e')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
# Simulate deleting three characters from the end of 'inte'.
# During this, the highlighting should change from normal to
# builtin to keyword to normal.
text.delete('insert-1c', 'insert')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ('1.0', '1.3'))
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
text.delete('insert-1c', 'insert')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2'))
text.delete('insert-1c', 'insert')
yield
eq(text.tag_nextrange('BUILTIN', '1.0'), ())
eq(text.tag_nextrange('KEYWORD', '1.0'), ())
@mock.patch.object(colorizer.ColorDelegator, 'recolorize') @mock.patch.object(colorizer.ColorDelegator, 'recolorize')
@mock.patch.object(colorizer.ColorDelegator, 'notify_range') @mock.patch.object(colorizer.ColorDelegator, 'notify_range')

View File

@ -0,0 +1,5 @@
Highlight the new :ref:`match <match>` statement's
:ref:`soft keywords <soft-keywords>`: :keyword:`match`,
:keyword:`case <match>`, and :keyword:`_ <wildcard-patterns>`.
However, this highlighting is not perfect and will be incorrect in some
rare cases, including some ``_``-s in ``case`` patterns.