Commit fae2c353 authored by wohlganger's avatar wohlganger Committed by terryjreedy

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.
parent 84d9d14a
...@@ -1407,6 +1407,21 @@ The IDLE Modern Unix key set is new in June 2016. It can only ...@@ -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 @@ ...@@ -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): ...@@ -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')),
text.insert('insert', 'def foobar(a, b') ('default',('1.10', '1.11'),('1.10', '1.11')),
pm.flash_paren_event('event') ('parens', ('1.14', '1.15'), ('1.15', '1.16')),
self.assertIn('<<parenmatch-check-restore>>', text.event_info()) ('expression', ('1.10', '1.15'), ('1.10', '1.16'))):
self.assertTupleEqual(text.tag_prevrange('paren', 'end'), with self.subTest(style=style):
('1.10', '1.15')) text.delete('1.0', 'end')
text.insert('insert', ')') pm.set_style(style)
pm.restore_event() text.insert('insert', 'def foobar(a, b')
self.assertNotIn('<<parenmatch-check-restore>>', text.event_info())
self.assertEqual(text.tag_prevrange('paren', 'end'), ()) pm.flash_paren_event('event')
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
# paren_closed_event can only be tested as below if style == 'parens':
pm.paren_closed_event('event') self.assertTupleEqual(text.tag_nextrange('paren', '1.0'),
self.assertTupleEqual(text.tag_prevrange('paren', 'end'), ('1.10', '1.11'))
('1.10', '1.16')) self.assertTupleEqual(
text.tag_prevrange('paren', 'end'), range1)
def test_paren_default(self):
""" text.insert('insert', ')')
Test ParenMatch with 'default' style. pm.restore_event()
""" self.assertNotIn('<<parenmatch-check-restore>>',
text = self.text text.event_info())
pm = self.get_parenmatch() self.assertEqual(text.tag_prevrange('paren', 'end'), ())
pm.set_style('default')
pm.paren_closed_event('event')
text.insert('insert', 'def foobar(a, b') self.assertTupleEqual(
pm.flash_paren_event('event') text.tag_prevrange('paren', 'end'), range2)
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 = {')':'(',']':'[','}':'{'} ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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 ...@@ -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
......
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.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment