Skip to content

Commit af68382

Browse files
authored
[3.6] bpo-30723: IDLE -- Enhance parenmatch; add style, flash, and help (GH-2306) (#2460)
* Add 'parens' style to highlight both opener and closer. * Make 'default' style, which is not default, a synonym for 'opener'. * Make time-delay work the same with all styles. * Add help for config dialog extensions tab, including parenmatch. * Add new tests. Original patch by Charles Wohlganger. (cherry picked from commit fae2c35)
1 parent c4cc553 commit af68382

File tree

5 files changed

+97
-72
lines changed

5 files changed

+97
-72
lines changed

Lib/idlelib/configdialog.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,21 @@ def save_all_changed_extensions(self):
14071407
be used with older IDLE releases if it is saved as a custom
14081408
key set, with a different name.
14091409
''',
1410+
'Extensions': '''
1411+
Extensions:
1412+
1413+
Autocomplete: Popupwait is milleseconds to wait after key char, without
1414+
cursor movement, before popping up completion box. Key char is '.' after
1415+
identifier or a '/' (or '\\' on Windows) within a string.
1416+
1417+
FormatParagraph: Max-width is max chars in lines after re-formatting.
1418+
Use with paragraphs in both strings and comment blocks.
1419+
1420+
ParenMatch: Style indicates what is highlighted when closer is entered:
1421+
'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
1422+
'expression' (default) - also everything in between. Flash-delay is how
1423+
long to highlight if cursor is not moved (0 means forever).
1424+
'''
14101425
}
14111426

14121427

Lib/idlelib/idle_test/test_parenmatch.py

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
This must currently be a gui test because ParenMatch methods use
44
several text methods not defined on idlelib.idle_test.mock_tk.Text.
55
'''
6+
from idlelib.parenmatch import ParenMatch
67
from test.support import requires
78
requires('gui')
89

910
import unittest
1011
from unittest.mock import Mock
1112
from tkinter import Tk, Text
12-
from idlelib.parenmatch import ParenMatch
13+
1314

1415
class DummyEditwin:
1516
def __init__(self, text):
@@ -44,46 +45,39 @@ def get_parenmatch(self):
4445
pm.bell = lambda: None
4546
return pm
4647

47-
def test_paren_expression(self):
48+
def test_paren_styles(self):
4849
"""
49-
Test ParenMatch with 'expression' style.
50+
Test ParenMatch with each style.
5051
"""
5152
text = self.text
5253
pm = self.get_parenmatch()
53-
pm.set_style('expression')
54-
55-
text.insert('insert', 'def foobar(a, b')
56-
pm.flash_paren_event('event')
57-
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
58-
self.assertTupleEqual(text.tag_prevrange('paren', 'end'),
59-
('1.10', '1.15'))
60-
text.insert('insert', ')')
61-
pm.restore_event()
62-
self.assertNotIn('<<parenmatch-check-restore>>', text.event_info())
63-
self.assertEqual(text.tag_prevrange('paren', 'end'), ())
64-
65-
# paren_closed_event can only be tested as below
66-
pm.paren_closed_event('event')
67-
self.assertTupleEqual(text.tag_prevrange('paren', 'end'),
68-
('1.10', '1.16'))
69-
70-
def test_paren_default(self):
71-
"""
72-
Test ParenMatch with 'default' style.
73-
"""
74-
text = self.text
75-
pm = self.get_parenmatch()
76-
pm.set_style('default')
77-
78-
text.insert('insert', 'def foobar(a, b')
79-
pm.flash_paren_event('event')
80-
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
81-
self.assertTupleEqual(text.tag_prevrange('paren', 'end'),
82-
('1.10', '1.11'))
83-
text.insert('insert', ')')
84-
pm.restore_event()
85-
self.assertNotIn('<<parenmatch-check-restore>>', text.event_info())
86-
self.assertEqual(text.tag_prevrange('paren', 'end'), ())
54+
for style, range1, range2 in (
55+
('opener', ('1.10', '1.11'), ('1.10', '1.11')),
56+
('default',('1.10', '1.11'),('1.10', '1.11')),
57+
('parens', ('1.14', '1.15'), ('1.15', '1.16')),
58+
('expression', ('1.10', '1.15'), ('1.10', '1.16'))):
59+
with self.subTest(style=style):
60+
text.delete('1.0', 'end')
61+
pm.set_style(style)
62+
text.insert('insert', 'def foobar(a, b')
63+
64+
pm.flash_paren_event('event')
65+
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
66+
if style == 'parens':
67+
self.assertTupleEqual(text.tag_nextrange('paren', '1.0'),
68+
('1.10', '1.11'))
69+
self.assertTupleEqual(
70+
text.tag_prevrange('paren', 'end'), range1)
71+
72+
text.insert('insert', ')')
73+
pm.restore_event()
74+
self.assertNotIn('<<parenmatch-check-restore>>',
75+
text.event_info())
76+
self.assertEqual(text.tag_prevrange('paren', 'end'), ())
77+
78+
pm.paren_closed_event('event')
79+
self.assertTupleEqual(
80+
text.tag_prevrange('paren', 'end'), range2)
8781

8882
def test_paren_corner(self):
8983
"""

Lib/idlelib/parenmatch.py

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,37 @@
1111
CHECK_DELAY = 100 # milliseconds
1212

1313
class ParenMatch:
14-
"""Highlight matching parentheses
14+
"""Highlight matching openers and closers, (), [], and {}.
1515
16-
There are three supported style of paren matching, based loosely
17-
on the Emacs options. The style is select based on the
18-
HILITE_STYLE attribute; it can be changed used the set_style
19-
method.
16+
There are three supported styles of paren matching. When a right
17+
paren (opener) is typed:
2018
21-
The supported styles are:
19+
opener -- highlight the matching left paren (closer);
20+
parens -- highlight the left and right parens (opener and closer);
21+
expression -- highlight the entire expression from opener to closer.
22+
(For back compatibility, 'default' is a synonym for 'opener').
2223
23-
default -- When a right paren is typed, highlight the matching
24-
left paren for 1/2 sec.
25-
26-
expression -- When a right paren is typed, highlight the entire
27-
expression from the left paren to the right paren.
24+
Flash-delay is the maximum milliseconds the highlighting remains.
25+
Any cursor movement (key press or click) before that removes the
26+
highlight. If flash-delay is 0, there is no maximum.
2827
2928
TODO:
30-
- extend IDLE with configuration dialog to change options
31-
- implement rest of Emacs highlight styles (see below)
32-
- print mismatch warning in IDLE status window
33-
34-
Note: In Emacs, there are several styles of highlight where the
35-
matching paren is highlighted whenever the cursor is immediately
36-
to the right of a right paren. I don't know how to do that in Tk,
37-
so I haven't bothered.
29+
- Augment bell() with mismatch warning in status window.
30+
- Highlight when cursor is moved to the right of a closer.
31+
This might be too expensive to check.
3832
"""
3933
menudefs = [
4034
('edit', [
4135
("Show surrounding parens", "<<flash-paren>>"),
4236
])
4337
]
44-
STYLE = idleConf.GetOption('extensions','ParenMatch','style',
45-
default='expression')
46-
FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
47-
type='int',default=500)
38+
STYLE = idleConf.GetOption(
39+
'extensions','ParenMatch','style', default='expression')
40+
FLASH_DELAY = idleConf.GetOption(
41+
'extensions','ParenMatch','flash-delay', type='int',default=500)
42+
BELL = idleConf.GetOption(
43+
'extensions','ParenMatch','bell', type='bool',default=1)
4844
HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite')
49-
BELL = idleConf.GetOption('extensions','ParenMatch','bell',
50-
type='bool',default=1)
5145

5246
RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>"
5347
# We want the restore event be called before the usual return and
@@ -69,39 +63,45 @@ def __init__(self, editwin):
6963
self.set_style(self.STYLE)
7064

7165
def activate_restore(self):
66+
"Activate mechanism to restore text from highlighting."
7267
if not self.is_restore_active:
7368
for seq in self.RESTORE_SEQUENCES:
7469
self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
7570
self.is_restore_active = True
7671

7772
def deactivate_restore(self):
73+
"Remove restore event bindings."
7874
if self.is_restore_active:
7975
for seq in self.RESTORE_SEQUENCES:
8076
self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
8177
self.is_restore_active = False
8278

8379
def set_style(self, style):
80+
"Set tag and timeout functions."
8481
self.STYLE = style
85-
if style == "default":
86-
self.create_tag = self.create_tag_default
87-
self.set_timeout = self.set_timeout_last
88-
elif style == "expression":
89-
self.create_tag = self.create_tag_expression
90-
self.set_timeout = self.set_timeout_none
82+
self.create_tag = (
83+
self.create_tag_opener if style in {"opener", "default"} else
84+
self.create_tag_parens if style == "parens" else
85+
self.create_tag_expression) # "expression" or unknown
86+
87+
self.set_timeout = (self.set_timeout_last if self.FLASH_DELAY else
88+
self.set_timeout_none)
9189

9290
def flash_paren_event(self, event):
91+
"Handle editor 'show surrounding parens' event (menu or shortcut)."
9392
indices = (HyperParser(self.editwin, "insert")
9493
.get_surrounding_brackets())
9594
if indices is None:
9695
self.bell()
9796
return "break"
9897
self.activate_restore()
9998
self.create_tag(indices)
100-
self.set_timeout_last()
99+
self.set_timeout()
101100
return "break"
102101

103102
def paren_closed_event(self, event):
104-
# If it was a shortcut and not really a closing paren, quit.
103+
"Handle user input of closer."
104+
# If user bound non-closer to <<paren-closed>>, quit.
105105
closer = self.text.get("insert-1c")
106106
if closer not in _openers:
107107
return "break"
@@ -118,6 +118,7 @@ def paren_closed_event(self, event):
118118
return "break"
119119

120120
def restore_event(self, event=None):
121+
"Remove effect of doing match."
121122
self.text.tag_delete("paren")
122123
self.deactivate_restore()
123124
self.counter += 1 # disable the last timer, if there is one.
@@ -129,11 +130,20 @@ def handle_restore_timer(self, timer_count):
129130
# any one of the create_tag_XXX methods can be used depending on
130131
# the style
131132

132-
def create_tag_default(self, indices):
133+
def create_tag_opener(self, indices):
133134
"""Highlight the single paren that matches"""
134135
self.text.tag_add("paren", indices[0])
135136
self.text.tag_config("paren", self.HILITE_CONFIG)
136137

138+
def create_tag_parens(self, indices):
139+
"""Highlight the left and right parens"""
140+
if self.text.get(indices[1]) in (')', ']', '}'):
141+
rightindex = indices[1]+"+1c"
142+
else:
143+
rightindex = indices[1]
144+
self.text.tag_add("paren", indices[0], indices[0]+"+1c", rightindex+"-1c", rightindex)
145+
self.text.tag_config("paren", self.HILITE_CONFIG)
146+
137147
def create_tag_expression(self, indices):
138148
"""Highlight the entire expression"""
139149
if self.text.get(indices[1]) in (')', ']', '}'):
@@ -162,7 +172,7 @@ def callme(callme, self=self, c=self.counter,
162172
self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
163173

164174
def set_timeout_last(self):
165-
"""The last highlight created will be removed after .5 sec"""
175+
"""The last highlight created will be removed after FLASH_DELAY millisecs"""
166176
# associate a counter with an event; only disable the "paren"
167177
# tag if the event is for the most recent timer.
168178
self.counter += 1

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,6 +1690,7 @@ John Wiseman
16901690
Chris Withers
16911691
Stefan Witzel
16921692
Irek Wlizlo
1693+
Charles Wohlganger
16931694
David Wolever
16941695
Klaus-Juergen Wolf
16951696
Dan Wolfe
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
IDLE: Make several improvements to parenmatch. Add 'parens' style to
2+
highlight both opener and closer. Make 'default' style, which is not
3+
default, a synonym for 'opener'. Make time-delay work the same with all
4+
styles. Add help for config dialog extensions tab, including help for
5+
parenmatch. Add new tests. Original patch by Charles Wohlganger.

0 commit comments

Comments
 (0)