8000 bpo-30723: Enhancement to IDLE parenmatch extension by wohlganger · Pull Request #2306 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

bpo-30723: Enhancement to IDLE parenmatch extension #2306

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

Merged
merged 8 commits into from
Jun 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions Lib/idlelib/configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,21 @@ def save_all_changed_extensions(self):
be used with older IDLE releases if it is saved as a custom
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).
'''
}


Expand Down
68 changes: 31 additions & 37 deletions Lib/idlelib/idle_test/test_parenmatch.py
< 8000 tr data-hunk="4a1775fa2b598aed67f2e9085f2916bd1b4db17e50766dbfbc427be72175a26a" class="show-top-border">
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
This must currently be a gui test because ParenMatch methods use
several text methods not defined on idlelib.idle_test.mock_tk.Text.
'''
from idlelib.parenmatch import ParenMatch
from test.support import requires
requires('gui')

import unittest
from unittest.mock import Mock
from tkinter import Tk, Text
from idlelib.parenmatch import ParenMatch


class DummyEditwin:
def __init__(self, text):
Expand Down Expand Up @@ -44,46 +45,39 @@ def get_parenmatch(self):
pm.bell = lambda: None
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
pm = self.get_parenmatch()
pm.set_style('expression')

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.15'))
text.insert('insert', ')')
pm.restore_event()
self.assertNotIn('<<parenmatch-check-restore>>', text.event_info())
self.assertEqual(text.tag_prevrange('paren', 'end'), ())

# paren_closed_event can only be tested as below
pm.paren_closed_event('event')
self.assertTupleEqual(text.tag_prevrange('paren', 'end'),
('1.10', '1.16'))

def test_paren_default(self):
"""
Test ParenMatch with 'default' style.
"""
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'), ())
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')

pm.flash_paren_event('event')
self.assertIn('<<parenmatch-check-restore>>', text.event_info())
if style == 'parens':
self.assertTupleEqual(text.tag_nextrange('paren', '1.0'),
('1.10', '1.11'))
self.assertTupleEqual(
text.tag_prevrange('paren', 'end'), range1)

text.insert('insert', ')')
pm.restore_event()
self.assertNotIn('<<parenmatch-check-restore>>',
text.event_info())
self.assertEqual(text.tag_prevrange('paren', 'end'), ())

pm.paren_closed_event('event')
self.assertTupleEqual(
text.tag_prevrange('paren', 'end'), range2)

def test_paren_corner(self):
"""
Expand Down
80 changes: 45 additions & 35 deletions Lib/idlelib/parenmatch.py
9E88
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,37 @@
CHECK_DELAY = 100 # milliseconds

class ParenMatch:
"""Highlight matching parentheses
"""Highlight matching openers and closers, (), [], and {}.

There are three supported style of paren matching, based loosely
on the Emacs options. The style is select based on the
HILITE_STYLE attribute; it can be changed used the set_style
method.
There are three supported styles of paren matching. When a right
paren (opener) is typed:

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
left paren for 1/2 sec.

expression -- When a right paren is typed, highlight the entire
expression from the left paren to the right paren.
Flash-delay is the maximum milliseconds the highlighting remains.
Any cursor movement (key press or click) before that removes the
highlight. If flash-delay is 0, there is no maximum.

TODO:
- extend IDLE with configuration dialog to change options
- implement rest of Emacs highlight styles (see below)
- print mismatch warning in IDLE status window

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.
- Augment bell() with mismatch warning in status window.
- Highlight when cursor is moved to the right of a closer.
This might be too expensive to check.
"""
menudefs = [
('edit', [
("Show surrounding parens", "<<flash-paren>>"),
])
]
STYLE = idleConf.GetOption('extensions','ParenMatch','style',
default='expression')
FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
type='int',default=500)
STYLE = idleConf.GetOption(
'extensions','ParenMatch','style', default='expression')
FLASH_DELAY = idleConf.GetOption(
'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')
BELL = idleConf.GetOption('extensions','ParenMatch','bell',
type='bool',default=1)

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

def activate_restore(self):
"Activate mechanism to restore text from highlighting."
if not self.is_restore_active:
for seq in self.RESTORE_SEQUENCES:
self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
self.is_restore_active = True

def deactivate_restore(self):
"Remove restore event bindings."
if self.is_restore_active:
for seq in self.RESTORE_SEQUENCES:
self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
self.is_restore_active = False

def set_style(self, style):
"Set tag and timeout functions."
self.STYLE = style
if style == "default":
self.create_tag = self.create_tag_default
self.set_timeout = self.set_timeout_last
elif style == "expression":
self.create_tag = self.create_tag_expression
self.set_timeout = self.set_timeout_none
self.create_tag = (
self.create_tag_opener if style in {"opener", "default"} else
self.create_tag_parens if style == "parens" else
self.create_tag_expression) # "expression" or unknown

self.set_timeout = (self.set_timeout_last if self.FLASH_DELAY else
self.set_timeout_none)

def flash_paren_event(self, event):
"Handle editor 'show surrounding parens' event (menu or shortcut)."
indices = (HyperParser(self.editwin, "insert")
.get_surrounding_brackets())
if indices is None:
self.bell()
return "break"
self.activate_restore()
self.create_tag(indices)
self.set_timeout_last()
self.set_timeout()
return "break"

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")
if closer not in _openers:
return "break"
Expand All @@ -118,6 +118,7 @@ def paren_closed_event(self, event):
return "break"

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

def create_tag_default(self, indices):
def create_tag_opener(self, indices):
"""Highlight the single paren that matches"""
self.text.tag_add("paren", indices[0])
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):
"""Highlight the entire expression"""
if self.text.get(indices[1]) in (')', ']', '}'):
Expand Down Expand Up @@ -162,7 +172,7 @@ def callme(callme, self=self, c=self.counter,
self.editwin.text_frame.after(CHECK_DELAY, callme, callme)

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"
# tag if the event is for the most recent timer.
self.counter += 1
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,7 @@ John Wiseman
Chris Withers
Stefan Witzel
Irek Wlizlo
Charles Wohlganger
David Wolever
Klaus-Juergen Wolf
Dan Wolfe
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
0