mirror of
https://github.com/python/cpython.git
synced 2025-08-22 17:55:18 +00:00
bpo-30723: IDLE -- Enhance parenmatch; add style, flash, and help (#2306)
* 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.
This commit is contained in:
parent
84d9d14a1f
commit
fae2c3538e
5 changed files with 94 additions and 69 deletions
|
@ -1407,6 +1407,21 @@ The IDLE Modern Unix key set is new in June 2016. It can only
|
||||||
be used with older IDLE releases if it is saved as a custom
|
be used with older IDLE releases if it is saved as a custom
|
||||||
key set, with a different name.
|
key set, with a different name.
|
||||||
''',
|
''',
|
||||||
|
'Extensions': '''
|
||||||
|
Extensions:
|
||||||
|
|
||||||
|
Autocomplete: Popupwait is milleseconds to wait after key char, without
|
||||||
|
cursor movement, before popping up completion box. Key char is '.' after
|
||||||
|
identifier or a '/' (or '\\' on Windows) within a string.
|
||||||
|
|
||||||
|
FormatParagraph: Max-width is max chars in lines after re-formatting.
|
||||||
|
Use with paragraphs in both strings and comment blocks.
|
||||||
|
|
||||||
|
ParenMatch: Style indicates what is highlighted when closer is entered:
|
||||||
|
'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
|
||||||
|
'expression' (default) - also everything in between. Flash-delay is how
|
||||||
|
long to highlight if cursor is not moved (0 means forever).
|
||||||
|
'''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
This must currently be a gui test because ParenMatch methods use
|
This must currently be a gui test because ParenMatch methods use
|
||||||
several text methods not defined on idlelib.idle_test.mock_tk.Text.
|
several text methods not defined on idlelib.idle_test.mock_tk.Text.
|
||||||
'''
|
'''
|
||||||
|
from idlelib.parenmatch import ParenMatch
|
||||||
from test.support import requires
|
from test.support import requires
|
||||||
requires('gui')
|
requires('gui')
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
from tkinter import Tk, Text
|
from tkinter import Tk, Text
|
||||||
from idlelib.parenmatch import ParenMatch
|
|
||||||
|
|
||||||
class DummyEditwin:
|
class DummyEditwin:
|
||||||
def __init__(self, text):
|
def __init__(self, text):
|
||||||
|
@ -44,46 +45,39 @@ class ParenMatchTest(unittest.TestCase):
|
||||||
pm.bell = lambda: None
|
pm.bell = lambda: None
|
||||||
return pm
|
return pm
|
||||||
|
|
||||||
def test_paren_expression(self):
|
def test_paren_styles(self):
|
||||||
"""
|
"""
|
||||||
Test ParenMatch with 'expression' style.
|
Test ParenMatch with each style.
|
||||||
"""
|
"""
|
||||||
text = self.text
|
text = self.text
|
||||||
pm = self.get_parenmatch()
|
pm = self.get_parenmatch()
|
||||||
pm.set_style('expression')
|
for style, range1, range2 in (
|
||||||
|
('opener', ('1.10', '1.11'), ('1.10', '1.11')),
|
||||||
|
('default',('1.10', '1.11'),('1.10', '1.11')),
|
||||||
|
('parens', ('1.14', '1.15'), ('1.15', '1.16')),
|
||||||
|
('expression', ('1.10', '1.15'), ('1.10', '1.16'))):
|
||||||
|
with self.subTest(style=style):
|
||||||
|
text.delete('1.0', 'end')
|
||||||
|
pm.set_style(style)
|
||||||
|
text.insert('insert', 'def foobar(a, b')
|
||||||
|
|
||||||
text.insert('insert', 'def foobar(a, b')
|
pm.flash_paren_event('event')
|
||||||
pm.flash_paren_event('event')
|
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
|
||||||
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
|
if style == 'parens':
|
||||||
self.assertTupleEqual(text.tag_prevrange('paren', 'end'),
|
self.assertTupleEqual(text.tag_nextrange('paren', '1.0'),
|
||||||
('1.10', '1.15'))
|
('1.10', '1.11'))
|
||||||
text.insert('insert', ')')
|
self.assertTupleEqual(
|
||||||
pm.restore_event()
|
text.tag_prevrange('paren', 'end'), range1)
|
||||||
self.assertNotIn('<<parenmatch-check-restore>>', text.event_info())
|
|
||||||
self.assertEqual(text.tag_prevrange('paren', 'end'), ())
|
|
||||||
|
|
||||||
# paren_closed_event can only be tested as below
|
text.insert('insert', ')')
|
||||||
pm.paren_closed_event('event')
|
pm.restore_event()
|
||||||
self.assertTupleEqual(text.tag_prevrange('paren', 'end'),
|
self.assertNotIn('<<parenmatch-check-restore>>',
|
||||||
('1.10', '1.16'))
|
text.event_info())
|
||||||
|
self.assertEqual(text.tag_prevrange('paren', 'end'), ())
|
||||||
|
|
||||||
def test_paren_default(self):
|
pm.paren_closed_event('event')
|
||||||
"""
|
self.assertTupleEqual(
|
||||||
Test ParenMatch with 'default' style.
|
text.tag_prevrange('paren', 'end'), range2)
|
||||||
"""
|
|
||||||
text = self.text
|
|
||||||
pm = self.get_parenmatch()
|
|
||||||
pm.set_style('default')
|
|
||||||
|
|
||||||
text.insert('insert', 'def foobar(a, b')
|
|
||||||
pm.flash_paren_event('event')
|
|
||||||
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
|
|
||||||
self.assertTupleEqual(text.tag_prevrange('paren', 'end'),
|
|
||||||
('1.10', '1.11'))
|
|
||||||
text.insert('insert', ')')
|
|
||||||
pm.restore_event()
|
|
||||||
self.assertNotIn('<<parenmatch-check-restore>>', text.event_info())
|
|
||||||
self.assertEqual(text.tag_prevrange('paren', 'end'), ())
|
|
||||||
|
|
||||||
def test_paren_corner(self):
|
def test_paren_corner(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -11,43 +11,37 @@ _openers = {')':'(',']':'[','}':'{'}
|
||||||
CHECK_DELAY = 100 # milliseconds
|
CHECK_DELAY = 100 # milliseconds
|
||||||
|
|
||||||
class ParenMatch:
|
class ParenMatch:
|
||||||
"""Highlight matching parentheses
|
"""Highlight matching openers and closers, (), [], and {}.
|
||||||
|
|
||||||
There are three supported style of paren matching, based loosely
|
There are three supported styles of paren matching. When a right
|
||||||
on the Emacs options. The style is select based on the
|
paren (opener) is typed:
|
||||||
HILITE_STYLE attribute; it can be changed used the set_style
|
|
||||||
method.
|
|
||||||
|
|
||||||
The supported styles are:
|
opener -- highlight the matching left paren (closer);
|
||||||
|
parens -- highlight the left and right parens (opener and closer);
|
||||||
|
expression -- highlight the entire expression from opener to closer.
|
||||||
|
(For back compatibility, 'default' is a synonym for 'opener').
|
||||||
|
|
||||||
default -- When a right paren is typed, highlight the matching
|
Flash-delay is the maximum milliseconds the highlighting remains.
|
||||||
left paren for 1/2 sec.
|
Any cursor movement (key press or click) before that removes the
|
||||||
|
highlight. If flash-delay is 0, there is no maximum.
|
||||||
expression -- When a right paren is typed, highlight the entire
|
|
||||||
expression from the left paren to the right paren.
|
|
||||||
|
|
||||||
TODO:
|
TODO:
|
||||||
- extend IDLE with configuration dialog to change options
|
- Augment bell() with mismatch warning in status window.
|
||||||
- implement rest of Emacs highlight styles (see below)
|
- Highlight when cursor is moved to the right of a closer.
|
||||||
- print mismatch warning in IDLE status window
|
This might be too expensive to check.
|
||||||
|
|
||||||
Note: In Emacs, there are several styles of highlight where the
|
|
||||||
matching paren is highlighted whenever the cursor is immediately
|
|
||||||
to the right of a right paren. I don't know how to do that in Tk,
|
|
||||||
so I haven't bothered.
|
|
||||||
"""
|
"""
|
||||||
menudefs = [
|
menudefs = [
|
||||||
('edit', [
|
('edit', [
|
||||||
("Show surrounding parens", "<<flash-paren>>"),
|
("Show surrounding parens", "<<flash-paren>>"),
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
STYLE = idleConf.GetOption('extensions','ParenMatch','style',
|
STYLE = idleConf.GetOption(
|
||||||
default='expression')
|
'extensions','ParenMatch','style', default='expression')
|
||||||
FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
|
FLASH_DELAY = idleConf.GetOption(
|
||||||
type='int',default=500)
|
'extensions','ParenMatch','flash-delay', type='int',default=500)
|
||||||
|
BELL = idleConf.GetOption(
|
||||||
|
'extensions','ParenMatch','bell', type='bool',default=1)
|
||||||
HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite')
|
HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite')
|
||||||
BELL = idleConf.GetOption('extensions','ParenMatch','bell',
|
|
||||||
type='bool',default=1)
|
|
||||||
|
|
||||||
RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>"
|
RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>"
|
||||||
# We want the restore event be called before the usual return and
|
# We want the restore event be called before the usual return and
|
||||||
|
@ -69,27 +63,32 @@ class ParenMatch:
|
||||||
self.set_style(self.STYLE)
|
self.set_style(self.STYLE)
|
||||||
|
|
||||||
def activate_restore(self):
|
def activate_restore(self):
|
||||||
|
"Activate mechanism to restore text from highlighting."
|
||||||
if not self.is_restore_active:
|
if not self.is_restore_active:
|
||||||
for seq in self.RESTORE_SEQUENCES:
|
for seq in self.RESTORE_SEQUENCES:
|
||||||
self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
|
self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
|
||||||
self.is_restore_active = True
|
self.is_restore_active = True
|
||||||
|
|
||||||
def deactivate_restore(self):
|
def deactivate_restore(self):
|
||||||
|
"Remove restore event bindings."
|
||||||
if self.is_restore_active:
|
if self.is_restore_active:
|
||||||
for seq in self.RESTORE_SEQUENCES:
|
for seq in self.RESTORE_SEQUENCES:
|
||||||
self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
|
self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
|
||||||
self.is_restore_active = False
|
self.is_restore_active = False
|
||||||
|
|
||||||
def set_style(self, style):
|
def set_style(self, style):
|
||||||
|
"Set tag and timeout functions."
|
||||||
self.STYLE = style
|
self.STYLE = style
|
||||||
if style == "default":
|
self.create_tag = (
|
||||||
self.create_tag = self.create_tag_default
|
self.create_tag_opener if style in {"opener", "default"} else
|
||||||
self.set_timeout = self.set_timeout_last
|
self.create_tag_parens if style == "parens" else
|
||||||
elif style == "expression":
|
self.create_tag_expression) # "expression" or unknown
|
||||||
self.create_tag = self.create_tag_expression
|
|
||||||
self.set_timeout = self.set_timeout_none
|
self.set_timeout = (self.set_timeout_last if self.FLASH_DELAY else
|
||||||
|
self.set_timeout_none)
|
||||||
|
|
||||||
def flash_paren_event(self, event):
|
def flash_paren_event(self, event):
|
||||||
|
"Handle editor 'show surrounding parens' event (menu or shortcut)."
|
||||||
indices = (HyperParser(self.editwin, "insert")
|
indices = (HyperParser(self.editwin, "insert")
|
||||||
.get_surrounding_brackets())
|
.get_surrounding_brackets())
|
||||||
if indices is None:
|
if indices is None:
|
||||||
|
@ -97,11 +96,12 @@ class ParenMatch:
|
||||||
return "break"
|
return "break"
|
||||||
self.activate_restore()
|
self.activate_restore()
|
||||||
self.create_tag(indices)
|
self.create_tag(indices)
|
||||||
self.set_timeout_last()
|
self.set_timeout()
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
def paren_closed_event(self, event):
|
def paren_closed_event(self, event):
|
||||||
# If it was a shortcut and not really a closing paren, quit.
|
"Handle user input of closer."
|
||||||
|
# If user bound non-closer to <<paren-closed>>, quit.
|
||||||
closer = self.text.get("insert-1c")
|
closer = self.text.get("insert-1c")
|
||||||
if closer not in _openers:
|
if closer not in _openers:
|
||||||
return "break"
|
return "break"
|
||||||
|
@ -118,6 +118,7 @@ class ParenMatch:
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
def restore_event(self, event=None):
|
def restore_event(self, event=None):
|
||||||
|
"Remove effect of doing match."
|
||||||
self.text.tag_delete("paren")
|
self.text.tag_delete("paren")
|
||||||
self.deactivate_restore()
|
self.deactivate_restore()
|
||||||
self.counter += 1 # disable the last timer, if there is one.
|
self.counter += 1 # disable the last timer, if there is one.
|
||||||
|
@ -129,11 +130,20 @@ class ParenMatch:
|
||||||
# any one of the create_tag_XXX methods can be used depending on
|
# any one of the create_tag_XXX methods can be used depending on
|
||||||
# the style
|
# the style
|
||||||
|
|
||||||
def create_tag_default(self, indices):
|
def create_tag_opener(self, indices):
|
||||||
"""Highlight the single paren that matches"""
|
"""Highlight the single paren that matches"""
|
||||||
self.text.tag_add("paren", indices[0])
|
self.text.tag_add("paren", indices[0])
|
||||||
self.text.tag_config("paren", self.HILITE_CONFIG)
|
self.text.tag_config("paren", self.HILITE_CONFIG)
|
||||||
|
|
||||||
|
def create_tag_parens(self, indices):
|
||||||
|
"""Highlight the left and right parens"""
|
||||||
|
if self.text.get(indices[1]) in (')', ']', '}'):
|
||||||
|
rightindex = indices[1]+"+1c"
|
||||||
|
else:
|
||||||
|
rightindex = indices[1]
|
||||||
|
self.text.tag_add("paren", indices[0], indices[0]+"+1c", rightindex+"-1c", rightindex)
|
||||||
|
self.text.tag_config("paren", self.HILITE_CONFIG)
|
||||||
|
|
||||||
def create_tag_expression(self, indices):
|
def create_tag_expression(self, indices):
|
||||||
"""Highlight the entire expression"""
|
"""Highlight the entire expression"""
|
||||||
if self.text.get(indices[1]) in (')', ']', '}'):
|
if self.text.get(indices[1]) in (')', ']', '}'):
|
||||||
|
@ -162,7 +172,7 @@ class ParenMatch:
|
||||||
self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
|
self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
|
||||||
|
|
||||||
def set_timeout_last(self):
|
def set_timeout_last(self):
|
||||||
"""The last highlight created will be removed after .5 sec"""
|
"""The last highlight created will be removed after FLASH_DELAY millisecs"""
|
||||||
# associate a counter with an event; only disable the "paren"
|
# associate a counter with an event; only disable the "paren"
|
||||||
# tag if the event is for the most recent timer.
|
# tag if the event is for the most recent timer.
|
||||||
self.counter += 1
|
self.counter += 1
|
||||||
|
|
|
@ -1713,6 +1713,7 @@ John Wiseman
|
||||||
Chris Withers
|
Chris Withers
|
||||||
Stefan Witzel
|
Stefan Witzel
|
||||||
Irek Wlizlo
|
Irek Wlizlo
|
||||||
|
Charles Wohlganger
|
||||||
David Wolever
|
David Wolever
|
||||||
Klaus-Juergen Wolf
|
Klaus-Juergen Wolf
|
||||||
Dan Wolfe
|
Dan Wolfe
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
IDLE: Make several improvements to parenmatch. 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 help for
|
||||||
|
parenmatch. Add new tests. Original patch by Charles Wohlganger.
|
Loading…
Add table
Add a link
Reference in a new issue