Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-44010: IDLE: colorize pattern-matching soft keywords #25851

Merged
merged 31 commits into from
May 19, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7f193fa
modernize ColorDelegator.recolorize_main() code
taleinat May 3, 2021
1b4f728
add htest source example for pattern-matching
taleinat May 3, 2021
f977c50
initial implementation of pattern-matching soft-keyword colorization
taleinat May 3, 2021
df1c1a5
additional test cases for pattern-matching soft keyword colorization
taleinat May 3, 2021
816ea8d
remove dead code comment
taleinat May 3, 2021
146bf70
also ignore match/case immediately followed by )]}
taleinat May 3, 2021
ff463a6
simplify regexps using re.MULTILINE + more test cases
taleinat May 3, 2021
9758870
refactor and mark all lone underscores in case patterns as keywords
taleinat May 6, 2021
4adc318
fix comments in htest code sample
taleinat May 6, 2021
1d9ce7f
handle case guard and capture patterns
taleinat May 6, 2021
21c2f79
use single example source in tests
taleinat May 6, 2021
288fea3
fix highlighting in case guard and capture patterns
taleinat May 6, 2021
2988693
add a NEWS entry
taleinat May 9, 2021
c6015dc
more tests for function defs
taleinat May 9, 2021
68d41fe
improved doc-strings and indentation as per code review
taleinat May 9, 2021
12b7dd5
add test with long multi-line string at beginning of text
taleinat May 9, 2021
4bdbe2a
remove unused import
taleinat May 9, 2021
d550c0f
add more reference links in NEWS entry
taleinat May 9, 2021
ee98cf3
simplify handling of case softkw, and bring back specific handling of…
taleinat May 10, 2021
9a34c3b
add test simulating typing and deleting
taleinat May 10, 2021
25dfd1a
fix highlighting of underscore in case, and its tests
taleinat May 10, 2021
1c87c8a
avoid highlighting match and case in more scenarios (+ tests)
taleinat May 10, 2021
78980da
add info in idle help about soft keyword highlighting
taleinat May 10, 2021
5748063
clean up _assert_highlighting and add a doc-string
taleinat May 10, 2021
6c0f5c2
refactor mocking of notify_range() with _assert_highlighting
taleinat May 10, 2021
4b2b8d7
refactor another test to use _assert_highlighting()
taleinat May 10, 2021
99a67f6
update coverage percentage at head of test file
taleinat May 11, 2021
fae9905
Merge remote-tracking branch 'upstream/main' into idle-colorize-soft-…
taleinat May 11, 2021
b991d56
add a What's New entry
taleinat May 11, 2021
8b4f201
improve wording in NEWS, What's New and docs, and update help.html
taleinat May 11, 2021
ae3e7e1
remove dead code, recently added but no longer used
taleinat May 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 77 additions & 42 deletions Lib/idlelib/colorizer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import builtins
import keyword
import re
import textwrap
import time

from idlelib.config import idleConf
Expand All @@ -16,6 +17,20 @@ 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<PM_MATCH>match)\b" +
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved
r"(?!([ \t]|\\\n)*[=,)\]}])" # not followed by any of =,)]}
)
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved
match_case_default = (
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<PM_DEFAULT_CASE>case)[ \t]+(?P<PM_DEFAULT_UNDERSCORE>_)[ \t]*:"
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved
)
match_case_nondefault = (
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<PM_NONDEFAULT_CASE>case)\b" +
r"(?!([ \t]|\\\n)*[=,)\]}])" # not followed by any of =,)]}
)
builtinlist = [str(name) for name in dir(builtins)
if not name.startswith('_') and
name not in keyword.kwlist]
Expand All @@ -27,12 +42,20 @@ 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"]))
return "|".join([
builtin, comment, string, any("SYNC", [r"\n"]),
kw, match_softkw, match_case_default, match_case_nondefault,
])


prog = re.compile(make_pat(), re.S)
idprog = re.compile(r"\s+(\w+)", re.S)
prog = re.compile(make_pat(), re.DOTALL | re.MULTILINE)
idprog = re.compile(r"\s+(\w+)")
prog_group_name_to_tag = {
"PM_MATCH": "KEYWORD",
"PM_DEFAULT_CASE": "KEYWORD",
"PM_DEFAULT_UNDERSCORE": "KEYWORD",
"PM_NONDEFAULT_CASE": "KEYWORD",
}


def color_config(text):
Expand Down Expand Up @@ -231,14 +254,10 @@ def recolorize(self):
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
Expand All @@ -257,22 +276,21 @@ def recolorize_main(self):
for tag in self.tagdefs:
self.tag_remove(tag, mark, next)
chars = chars + line
m = self.prog.search(chars)
while m:
for m in self.prog.finditer(chars):
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 not value:
continue
a, b = m.span(key)
tag = prog_group_name_to_tag.get(key, key)
self.tag_add(tag,
head + "+%dc" % a,
head + "+%dc" % b)
if value in ("def", "class"):
if m1 := self.idprog.match(chars, b):
a, b = m1.span(1)
self.tag_add("DEFINITION",
head + "+%dc" % a,
head + "+%dc" % b)
if "SYNC" in self.tag_names(next + "-1c"):
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved
head = next
chars = ""
Expand Down Expand Up @@ -304,22 +322,39 @@ def _color_delegator(parent): # htest #
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))
source = textwrap.dedent("""\
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved
if True: int ('1') # keyword, builtin, string, comment
elif False: print(0)
else: float(None)
if iF + If + IF: 'keyword matching must respect case'
if'': x or'' # valid keyword-string no-space combinations
async def f(): await g()
# All valid prefixes for unicode and byte strings should be colored.
'x', '''x''', "x", \"""x\"""
'abc\\
def'
'''abc\\
def'''
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved
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):
print(f"X={x}")
case _:
raise ValueError("Not a point")
# The following statement should all be in the default color for code.
match = (
case,
_,
)
'''
case _:'''
"match x:"
""")
text = Text(top, background="white")
text.pack(expand=1, fill="both")
text.insert("insert", source)
Expand Down
43 changes: 33 additions & 10 deletions Lib/idlelib/idle_test/test_colorizer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"Test colorizer, coverage 93%."
Copy link
Member

@terryjreedy terryjreedy May 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now 99%. Try to fix on one of followups. I am not going to rerun tests for this.


from idlelib import colorizer
from test.support import requires
import unittest
from unittest import mock

from functools import partial
import textwrap
from tkinter import Tk, Text
from idlelib import config
from idlelib.percolator import Percolator
Expand All @@ -19,15 +19,32 @@
'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 string-keyword no-space combinations
async def f(): await g()
'x', '''x''', "x", \"""x\"""
'abc\\
def'
'''abc\\
def'''
match point:
case (x, 0):
print(f"X={x}")
case _:
raise ValueError("Not a point")
# The following statement should all be in the default color for code.
match = (
case,
_,
)
'''
case _:'''
"match x:"
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved
""")


def setUpModule():
Expand Down Expand Up @@ -366,6 +383,11 @@ def test_recolorize_main(self, mock_notify):
('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()),
('7.0', ('STRING',)), ('7.4', ()), ('7.5', ('STRING',)),
('7.12', ()), ('7.14', ('STRING',)),
('12.0', ('KEYWORD',)),
('13.4', ('KEYWORD',)),
('15.4', ('KEYWORD',)), ('15.9', ('KEYWORD',)),
('18.0', ()), ('19.4', ()), ('20.4', ()),
('23.0', ('STRING',)), ('24.1', ('STRING',)),
# SYNC at the end of every line.
('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)),
)
Expand Down Expand Up @@ -395,7 +417,8 @@ def test_recolorize_main(self, mock_notify):
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('SYNC', '7.0'), ('7.26', '8.0'))
eq(text.tag_nextrange('SYNC', '24.0'), ('24.10', '26.0'))

@mock.patch.object(colorizer.ColorDelegator, 'recolorize')
@mock.patch.object(colorizer.ColorDelegator, 'notify_range')
Expand Down