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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy