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
|
``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
|
||||||
|
|
|
@ -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
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 — 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/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> »
|
<a href="../index.html">3.11.0a0 Documentation</a> »
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</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">
|
<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 Python’s Integrated Development and Learning Environment.</p>
|
<p>IDLE is Python’s 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> »
|
<a href="../index.html">3.11.0a0 Documentation</a> »
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</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">
|
<div class="footer">
|
||||||
© <a href="../copyright.html">Copyright</a> 2001-2021, Python Software Foundation.
|
© <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 />
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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