mirror of https://github.com/python/cpython
bpo-44010: IDLE: colorize pattern-matching soft keywords (GH-25851)
This commit is contained in:
parent
d798acc873
commit
60d343a816
|
@ -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
|
||||
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
|
||||
visible. To change the color scheme, use the Configure IDLE dialog
|
||||
Highlighting tab. The marking of debugger breakpoint lines in the editor and
|
||||
|
|
|
@ -1030,6 +1030,12 @@ Terry Jan Reedy in :issue:`37892`.)
|
|||
We expect to backport these shell changes to a future 3.9 maintenance
|
||||
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
|
||||
------------------
|
||||
|
||||
|
|
|
@ -16,6 +16,32 @@ def any(name, alternates):
|
|||
|
||||
def make_pat():
|
||||
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)
|
||||
if not name.startswith('_') and
|
||||
name not in keyword.kwlist]
|
||||
|
@ -27,12 +53,29 @@ def make_pat():
|
|||
sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
|
||||
dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
|
||||
string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
|
||||
return (kw + "|" + builtin + "|" + comment + "|" + string +
|
||||
"|" + any("SYNC", [r"\n"]))
|
||||
prog = re.compile("|".join([
|
||||
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)
|
||||
idprog = re.compile(r"\s+(\w+)", re.S)
|
||||
prog = make_pat()
|
||||
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):
|
||||
|
@ -231,14 +274,10 @@ class ColorDelegator(Delegator):
|
|||
def recolorize_main(self):
|
||||
"Evaluate text and apply colorizing tags."
|
||||
next = "1.0"
|
||||
while True:
|
||||
item = self.tag_nextrange("TODO", next)
|
||||
if not item:
|
||||
break
|
||||
head, tail = item
|
||||
self.tag_remove("SYNC", head, tail)
|
||||
item = self.tag_prevrange("SYNC", head)
|
||||
head = item[1] if item else "1.0"
|
||||
while todo_tag_range := self.tag_nextrange("TODO", next):
|
||||
self.tag_remove("SYNC", todo_tag_range[0], todo_tag_range[1])
|
||||
sync_tag_range = self.tag_prevrange("SYNC", todo_tag_range[0])
|
||||
head = sync_tag_range[1] if sync_tag_range else "1.0"
|
||||
|
||||
chars = ""
|
||||
next = head
|
||||
|
@ -256,23 +295,8 @@ class ColorDelegator(Delegator):
|
|||
return
|
||||
for tag in self.tagdefs:
|
||||
self.tag_remove(tag, mark, next)
|
||||
chars = chars + line
|
||||
m = self.prog.search(chars)
|
||||
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())
|
||||
chars += line
|
||||
self._add_tags_in_section(chars, head)
|
||||
if "SYNC" in self.tag_names(next + "-1c"):
|
||||
head = next
|
||||
chars = ""
|
||||
|
@ -291,6 +315,40 @@ class ColorDelegator(Delegator):
|
|||
if DEBUG: print("colorizing stopped")
|
||||
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):
|
||||
"Remove all colorizing tags."
|
||||
for tag in self.tagdefs:
|
||||
|
@ -299,27 +357,14 @@ class ColorDelegator(Delegator):
|
|||
|
||||
def _color_delegator(parent): # htest #
|
||||
from tkinter import Toplevel, Text
|
||||
from idlelib.idle_test.test_colorizer import source
|
||||
from idlelib.percolator import Percolator
|
||||
|
||||
top = Toplevel(parent)
|
||||
top.title("Test ColorDelegator")
|
||||
x, y = map(int, parent.geometry().split('+')[1:])
|
||||
top.geometry("700x250+%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"
|
||||
)
|
||||
top.geometry("700x550+%d+%d" % (x + 20, y + 175))
|
||||
|
||||
text = Text(top, background="white")
|
||||
text.pack(expand=1, fill="both")
|
||||
text.insert("insert", source)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>IDLE — Python 3.10.0a6 documentation</title>
|
||||
<title>IDLE — Python 3.11.0a0 documentation</title>
|
||||
<link rel="stylesheet" href="../_static/pydoctheme.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
|||
<script src="../_static/sidebar.js"></script>
|
||||
|
||||
<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"/>
|
||||
<link rel="author" title="About these documents" href="../about.html" />
|
||||
<link rel="index" title="Index" href="../genindex.html" />
|
||||
|
@ -71,7 +71,7 @@
|
|||
|
||||
|
||||
<li id="cpython-language-and-version">
|
||||
<a href="../index.html">3.10.0a6 Documentation</a> »
|
||||
<a href="../index.html">3.11.0a0 Documentation</a> »
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> »</li>
|
||||
|
@ -102,7 +102,7 @@
|
|||
|
||||
<div class="section" id="idle">
|
||||
<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" />
|
||||
<p>IDLE is Python’s Integrated Development and Learning Environment.</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
|
||||
<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>
|
||||
<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
|
||||
visible. To change the color scheme, use the Configure IDLE dialog
|
||||
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.
|
||||
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,
|
||||
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
|
||||
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>
|
||||
|
@ -939,7 +944,7 @@ also used for testing.</p>
|
|||
<ul class="this-page-menu">
|
||||
<li><a href="../bugs.html">Report a Bug</a></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
|
||||
</a>
|
||||
</li>
|
||||
|
@ -971,7 +976,7 @@ also used for testing.</p>
|
|||
|
||||
|
||||
<li id="cpython-language-and-version">
|
||||
<a href="../index.html">3.10.0a6 Documentation</a> »
|
||||
<a href="../index.html">3.11.0a0 Documentation</a> »
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> »</li>
|
||||
|
@ -997,13 +1002,19 @@ also used for testing.</p>
|
|||
<div class="footer">
|
||||
© <a href="../copyright.html">Copyright</a> 2001-2021, Python Software Foundation.
|
||||
<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.
|
||||
<a href="https://www.python.org/psf/donations/">Please donate.</a>
|
||||
<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>?
|
||||
<br />
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
"Test colorizer, coverage 93%."
|
||||
|
||||
"Test colorizer, coverage 99%."
|
||||
from idlelib import colorizer
|
||||
from test.support import requires
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from .tkinter_testing_utils import run_in_tk_mainloop
|
||||
|
||||
from functools import partial
|
||||
import textwrap
|
||||
from tkinter import Tk, Text
|
||||
from idlelib import config
|
||||
from idlelib.percolator import Percolator
|
||||
|
@ -19,15 +20,38 @@ testcfg = {
|
|||
'extensions': config.IdleUserConfParser(''),
|
||||
}
|
||||
|
||||
source = (
|
||||
"if True: int ('1') # keyword, builtin, string, comment\n"
|
||||
"elif False: print(0) # 'string' in comment\n"
|
||||
"else: float(None) # if in comment\n"
|
||||
"if iF + If + IF: 'keyword matching must respect case'\n"
|
||||
"if'': x or'' # valid string-keyword no-space combinations\n"
|
||||
"async def f(): await g()\n"
|
||||
"'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
|
||||
)
|
||||
source = textwrap.dedent("""\
|
||||
if True: int ('1') # keyword, builtin, string, comment
|
||||
elif False: print(0) # 'string' in comment
|
||||
else: float(None) # if in comment
|
||||
if iF + If + IF: 'keyword matching must respect case'
|
||||
if'': x or'' # valid keyword-string no-space combinations
|
||||
async def f(): await g()
|
||||
# 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():
|
||||
|
@ -107,7 +131,7 @@ class ColorDelegatorInstantiationTest(unittest.TestCase):
|
|||
requires('gui')
|
||||
root = cls.root = Tk()
|
||||
root.withdraw()
|
||||
text = cls.text = Text(root)
|
||||
cls.text = Text(root)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
|
@ -152,7 +176,7 @@ class ColorDelegatorTest(unittest.TestCase):
|
|||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.percolator.redir.close()
|
||||
cls.percolator.close()
|
||||
del cls.percolator, cls.text
|
||||
cls.root.update_idletasks()
|
||||
cls.root.destroy()
|
||||
|
@ -364,8 +388,21 @@ class ColorDelegatorTest(unittest.TestCase):
|
|||
('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()),
|
||||
('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)),
|
||||
('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()),
|
||||
('7.0', ('STRING',)), ('7.4', ()), ('7.5', ('STRING',)),
|
||||
('7.12', ()), ('7.14', ('STRING',)),
|
||||
('8.0', ('STRING',)), ('8.4', ()), ('8.5', ('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.
|
||||
('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('SYNC', '2.0'), ('2.43', '3.0'))
|
||||
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', '7.3'), ('7.5', '7.12'))
|
||||
eq(text.tag_nextrange('STRING', '7.12'), ('7.14', '7.17'))
|
||||
eq(text.tag_nextrange('STRING', '7.17'), ('7.19', '7.26'))
|
||||
eq(text.tag_nextrange('SYNC', '7.0'), ('7.26', '9.0'))
|
||||
eq(text.tag_nextrange('STRING', '8.0'), ('8.0', '8.3'))
|
||||
eq(text.tag_nextrange('STRING', '8.3'), ('8.5', '8.12'))
|
||||
eq(text.tag_nextrange('STRING', '8.12'), ('8.14', '8.17'))
|
||||
eq(text.tag_nextrange('STRING', '8.17'), ('8.19', '8.26'))
|
||||
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, 'notify_range')
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue