Skip to content
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

gh-63075: IDLE editor - Auto insertion of the closers #3520

Open
wants to merge 75 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
5c57faf
parenmatch highlighting options
wohlganger Jun 29, 2017
d49170f
fixed whitespace issue
wohlganger Jun 29, 2017
8c3a966
only select from highlighting options
wohlganger Jun 30, 2017
757bb38
fixed whitespace
wohlganger Jun 30, 2017
d7e136c
whitespace
wohlganger Jun 30, 2017
b178af5
Merge remote-tracking branch 'upstream/master'
wohlganger Jul 10, 2017
2adebb9
#27099 - turn builtin extensions to reg modules
wohlganger Jul 10, 2017
86c3d4c
whitespace + fix I thought I already committed.
wohlganger Jul 10, 2017
1c246fc
warning fixes, removed old config entries
wohlganger Jul 11, 2017
0418ad0
calltip append doc fix, default paren-fore fix, help update
wohlganger Jul 11, 2017
097a0d8
fix calltips
wohlganger Jul 12, 2017
4232e77
removed circular import
wohlganger Jul 12, 2017
a7b29f5
fix warnings due to missing default config options in idlelib.config-…
wohlganger Jul 12, 2017
e30b7b4
Merge branch 'master' into master
wohlganger Jul 17, 2017
252672b
added format_pmaxw to config-main
wohlganger Jul 19, 2017
999b573
Merge remote-tracking branch 'upstream/master'
wohlganger Jul 19, 2017
1a74591
replaced help.html edit with idle.rst edit
wohlganger Jul 20, 2017
3501c45
Merge branch 'master' into master
wohlganger Jul 24, 2017
232f711
Merge remote-tracking branch 'upstream/master'
wohlganger Aug 4, 2017
f095c53
added conflict resolved var descriptions to docstring in create_page_…
wohlganger Aug 4, 2017
a661bfa
strip trailing whitespace
wohlganger Aug 4, 2017
5c90a8f
Merge branch 'master' into master
wohlganger Aug 24, 2017
5a6a007
re-introduced highlight options.
wohlganger Aug 24, 2017
709b434
News blurb.
terryjreedy Aug 24, 2017
b9b1f9a
Reinstated zoomheight as an extension. Replaced autoexpand with zoomh…
wohlganger Aug 24, 2017
8b76ccb
fixed configdialog test (wrong cases), fixed whitespace
wohlganger Aug 24, 2017
b986c12
update idle.rst: zoomheight is an extension.
wohlganger Aug 24, 2017
f86acaa
more fixes for zoomheight - trying to minimize changes from master
wohlganger Aug 24, 2017
5805462
zoomheight config fix, zoomheight should be fully back to the way it …
wohlganger Aug 24, 2017
165b548
whitespace
wohlganger Aug 24, 2017
aee5d02
fixed force-open completions missing from config-keys and config. Fix…
wohlganger Aug 24, 2017
cb161bd
Changes made per terryjreedy request.
wohlganger Aug 25, 2017
dc09a2b
re-added autocomplete wait and format paragraph max width options to …
wohlganger Aug 25, 2017
100463e
Merge branch 'master' into master
wohlganger Aug 28, 2017
3fbaa90
Merge branch 'master' into master
wohlganger Aug 30, 2017
60ff82f
changes per terryjreedy
wohlganger Aug 30, 2017
e3f6f16
fix warnings, errors, bugs
wohlganger Aug 30, 2017
a253f0c
whitespace
wohlganger Aug 30, 2017
fe8eb7a
bugfix
wohlganger Aug 30, 2017
caa7774
Make changes need to get IDLE to run
terryjreedy Aug 31, 2017
c29184e
Edit extension/feature files, making existing tests pass.
terryjreedy Aug 31, 2017
c62f726
Redo feature bindings.
terryjreedy Aug 31, 2017
d0e009f
First draft of dummy extension.
terryjreedy Aug 31, 2017
9e889c2
Fix that eliminate startup error and all but 2 test_idle errors.
terryjreedy Aug 31, 2017
6afdea3
Move widgets, load widgets, add tests, make previous tests pass.
terryjreedy Sep 2, 2017
7de27db
Add format-paragraph to core keys.
terryjreedy Sep 5, 2017
a2e76a7
Merge remote-tracking branch 'origin/master' into pr_2494
terryjreedy Sep 5, 2017
2265b53
Fix autocomplete and long lines.
terryjreedy Sep 5, 2017
03eed24
Put fixed-key event-adds in EditorWindow.__init__ for now.
terryjreedy Sep 9, 2017
207b0ca
Don't warn if new core keys not in user key config.
terryjreedy Sep 9, 2017
00acd22
Make code context inoperative in outwin, shell.
terryjreedy Sep 9, 2017
2d2bfbd
Change new bindings for MacOsx.
terryjreedy Sep 9, 2017
fe76cd3
Disable test dependent on extension Alt keys.
terryjreedy Sep 9, 2017
d2966e0
Minor edits.
terryjreedy Sep 9, 2017
6845bb0
File out news blurb explaining effect for users.
terryjreedy Sep 10, 2017
b7afebb
Don't compute ('') during integer input in entry box.
terryjreedy Sep 10, 2017
b1a4101
Merge remote-tracking branch 'upstream/master'
wohlganger Sep 12, 2017
7a53366
fix conflict
wohlganger Sep 12, 2017
d3b6848
fix conflict
wohlganger Sep 12, 2017
eb58d39
fix conflict, match upstream
wohlganger Sep 12, 2017
119bbfc
match upstream
wohlganger Sep 12, 2017
814efbe
Auto-insert-parens extension
wohlganger Sep 12, 2017
7f596b9
removed unused old options
wohlganger Sep 12, 2017
26323e2
Changes made per csabella request.
wohlganger Sep 13, 2017
7862d8a
fix stupid mistake
wohlganger Sep 13, 2017
2f19ca8
added mutual delete / backspace. minor fixes
wohlganger Sep 13, 2017
6d2fa26
PEP8 fixes.
wohlganger Sep 13, 2017
15f8d82
replace type option of bool with 'bool'
wohlganger Sep 13, 2017
d141abc
Merge branch 'master' into 18875-Auto-insert-parens
wohlganger Sep 15, 2017
caa9240
whitespace
wohlganger Sep 15, 2017
21f4482
whitespace fixed with reindent.py
wohlganger Sep 15, 2017
25ba0f8
trying to fix Tk for travis testing parenclose
wohlganger Sep 15, 2017
a4cd0e3
Set parenclose test as gui required
wohlganger Sep 18, 2017
cffa9d6
Added option to distinguish between mutual delete and mutual backspace
wohlganger Sep 20, 2017
9aaa395
Update ParenClose.py
terryjreedy Apr 6, 2018
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
91 changes: 91 additions & 0 deletions Lib/idlelib/ParenClose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add ParenClose to parenmatch.py.

Parens and Ticks Closing Extension

When you hit left paren or tick,
automatically creates the closing paren or tick.

Author: Charles M. Wohlganger
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove. This already has input from 3 other people. I will list Charles as original author in the news entry.

charles.wohlganger@gmail.com

Last Updated: 12-Aug-2017 by Charles Wohlganger

Add to the end of config-extensions.def :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config options will be added to config-general.def. The general tab already needs to be split for another issue. This issue, and a complete patch, will depend on how we do that, and then how to lay out the option buttons.


[ParenClose]
enable = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete.

paren_close = True
tick_close = True
skip_closures = True
comment_space = True
[ParenClose_cfgBindings]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete section. Not needed for hard-coded features.

p-open = <Key-parenleft> <Key-bracketleft> <Key-braceleft>
t-open = <Key-'> <Key-">
p-close = <Key-parenright> <Key-bracketright> <Key-braceright>
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved

"""

from idlelib.config import idleConf


class ParenClose:
'''
When loaded as an extension to IDLE, and paren_close is True, the symbols
([{ will have their closures printed after them and the insertion cursor
moved between the two. The same is true for tick closures and the symbols
' and ". When \'\'\' or """ are typed and tick_close is True, it will also
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will consider rewording docstring, but this is secondary. Why are triples single quotes escaped?

produce the closing symbols. If skip_closures is True, then when a closure
symbol is typed and the same one is to the right of it, that symbols is
deleted before the new one is typed, effectively skipping over the closure.
'''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand these.

def __init__(self, editwin=None): #setting default to none makes testing easier
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline comments should be written like a sentence - begin with a capital letter and end with a period.

if editwin:
self.text = editwin.text
else:
self.text=None
self.paren_close = idleConf.GetOption(
'extensions', 'ParenClose', 'paren_close', default=True)
self.tick_close = idleConf.GetOption(
'extensions', 'ParenClose', 'tick_close', default=True)
self.skip_closures = idleConf.GetOption(
'extensions', 'ParenClose', 'skip_closures', default=True)
#sometimes idleConf loads boolean False as string "False"
if self.paren_close == 'False':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetOption has a type parameter. If you set it to bool, I think it should always return a boolean.

self.paren_close = False
if self.tick_close == 'False':
self.tick_close = False
if self.skip_closures == 'False':
self.skip_closures = False

def p_open_event(self, event):
if self.paren_close:
closer = {'(': ')', '[': ']', '{': '}'}[event.char]
pos = self.text.index('insert')
self.text.insert(pos, closer)
self.text.mark_set('insert', pos)

def p_close_event(self, event):
pos = self.text.index('insert')
if self.skip_closures \
and self.text.get(pos, pos + ' +1c') == event.char:
self.text.delete(pos, pos + '+1c')

def t_open_event(self, event):
if self.tick_close:
pos = self.text.index('insert')
if self.text.get(pos + ' -2c', pos) == event.char * 2 \
and self.text.get(pos, pos + ' +1c') != event.char:
# Instead of one tick, add two ticks if there are two;
# user wants to make docstring or multiline.
self.text.insert(pos, event.char * 3)
self.text.mark_set('insert', pos)
else:
if self.skip_closures \
and self.text.get(pos, pos + ' +1c') == event.char:
self.text.delete(pos, pos + ' +1c')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although there aren't a lot of lines here, this is duplicate code with p_close_event so I'm wondering if a helper function that both would call would be beneficial?

else:
self.text.insert(pos, event.char)
self.text.mark_set('insert', pos)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here with p_open_event. The dictionary could map ' to ' and " to " for it use closer instead of event.char. I know the triple quotes are still a special case.


if __name__ == '__main__':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already in parenmatch.

import unittest
unittest.main('idlelib.idle_test.test_parenclose', verbosity=2)
10 changes: 10 additions & 0 deletions Lib/idlelib/config-extensions.def
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,13 @@ z-text= Z
z-in= <Control-Shift-KeyRelease-Insert>
[ZzDummy_bindings]
z-out= <Control-Shift-KeyRelease-Delete>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said above, put in config-general.

[ParenClose]
enable = True
paren_close = True
tick_close = True
skip_closures = True
[ParenClose_cfgBindings]
p-open = <Key-parenleft> <Key-bracketleft> <Key-braceleft>
t-open = <Key-'> <Key-">
Copy link
Contributor

@csabella csabella Sep 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I downloaded the PR to take a look at it and couldn't open IDLE because of this error:

File "/home/cheryl/cpython/Lib/idlelib/editor.py", line 137, in __init__ self.apply_bindings()
File "/home/cheryl/cpython/Lib/idlelib/editor.py", line 1071, in apply_bindings text.event_add(event, *keylist)
File "/home/cheryl/cpython/Lib/idlelib/multicall.py", line 374, in event_add widget.event_add(self, virtual, seq)
File "/home/cheryl/cpython/Lib/tkinter/__init__.py", line 1653, in event_add self.tk.call(args)
_tkinter.TclError: bad event type or keysym "'"

I looked at this page for the keysyms:
https://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm

and it has quotedbl 34 0x0022 and quoteright 39 0x0027, which seems to fix the error.

Also, ' and " aren't available on the config keys screen, so they may need to be added.

p-close = <Key-parenright> <Key-bracketright> <Key-braceright>
19 changes: 10 additions & 9 deletions Lib/idlelib/idle_test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,30 +436,31 @@ def test_get_extensions(self):
conf.SetOption('extensions', 'DISABLE', 'enable', 'False')

eq = self.assertEqual
eq(conf.GetExtensions(),
['ZzDummy'])
eq((conf.GetExtensions()),
['ZzDummy', 'ParenClose'])
eq(conf.GetExtensions(active_only=False),
['ZzDummy', 'DISABLE'])
['ZzDummy', 'ParenClose', 'DISABLE'])
eq(conf.GetExtensions(editor_only=True),
['ZzDummy'])
['ZzDummy', 'ParenClose'])
eq(conf.GetExtensions(shell_only=True),
[])
['ParenClose'])
eq(conf.GetExtensions(active_only=False, editor_only=True),
['ZzDummy', 'DISABLE'])
['ZzDummy', 'ParenClose', 'DISABLE'])

# Add user extensions
conf.SetOption('extensions', 'Foobar', 'enable', 'True')
eq(conf.GetExtensions(),
['ZzDummy', 'Foobar']) # User extensions didn't sort
['ZzDummy', 'ParenClose', 'Foobar']) # User extensions didn't sort
eq(conf.GetExtensions(active_only=False),
['ZzDummy', 'DISABLE', 'Foobar'])
['ZzDummy', 'ParenClose', 'DISABLE', 'Foobar'])

def test_remove_key_bind_names(self):
conf = self.mock_config()

self.assertCountEqual(
conf.RemoveKeyBindNames(conf.GetSectionList('default', 'extensions')),
['AutoComplete', 'CodeContext', 'FormatParagraph', 'ParenMatch','ZzDummy'])
['AutoComplete', 'CodeContext', 'FormatParagraph', 'ParenClose',
'ParenMatch','ZzDummy'])

def test_get_extn_name_for_event(self):
conf = self.mock_config()
Expand Down
78 changes: 78 additions & 0 deletions Lib/idlelib/idle_test/test_parenclose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'''Test idlelib.parenclose.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add MockEvent and ParenCloseTest to test_parenmatch. I have not reviewed in detail, but have a couple of comments.

'''
from idlelib.ParenClose import ParenClose
import unittest
from unittest.mock import Mock
from tkinter import Text # mocktk doesn't do text position addition correctly


class MockEvent(object):
def __init__(self, char):
self.char = char


class ParenCloseTest(unittest.TestCase):
def test_parenClose(self):
self.p_open_event = ParenClose.p_open_event
self.p_close_event = ParenClose.p_close_event
self.t_open_event = ParenClose.t_open_event
self.text = Text()
p_open = self.p_open_event
p_close = self.p_close_event
t_open = self.t_open_event
for paren_close_set, tick_close_set, skip_closures_set, \
insert, func, pos, char, result, posend in [
(1, 1, 1, 'def abuse', p_open, '1.9', '(',
'def abuse()', '1.10'),
(1, 1, 1, 'spam = ', p_open, '1.7', '[',
'spam = []', '1.8'),
(1, 1, 1, 'eggs = ', p_open, '1.7', '{',
'eggs = {}', '1.8'),
(1, 1, 1, 'def abuse2()', p_close, '1.11', ')',
'def abuse2()', '1.12'),
(1, 1, 1, 'spam2 = []', p_close, '1.9', ']',
'spam2 = []', '1.10'),
(1, 1, 1, 'eggs2 = {}', p_close, '1.9', '}',
'eggs2 = {}', '1.10'),
(1, 1, 1, "color = ", t_open, '1.8', "'",
"color = ''", '1.9'),
(1, 1, 1, "velocity = ", t_open, '1.11', '"',
'velocity = ""', '1.12'),
(1, 1, 1, "more_spam = ''", t_open, '1.14', "'",
"more_spam = ''''''", '1.15'),
(1, 1, 1, 'more_eggs = ""', t_open, '1.14', '"',
'more_eggs = """"""', '1.15'),
(1, 1, 1, "more_spam2 = ''''''", t_open, '1.16', "'",
"more_spam2 = ''''''", '1.17'),
(1, 1, 1, 'more_eggs2 = """"""', t_open, '1.16', '"',
'more_eggs2 = """"""', '1.17'),
(0, 1, 1, 'no_spam = ', p_open, '1.10', '(',
'no_spam = (', '1.11'),
(1, 0, 1, 'no_velocity = ', t_open, '1.14', "'",
"no_velocity = '", '1.15'),
(1, 0, 1, 'no_more_eggs = ""', t_open, '1.17', '"',
'no_more_eggs = """', '1.18'),
(1, 1, 0, 'tinny = []', p_close, '1.9', ']',
'tinny = []]', '1.10'),
(1, 1, 0, 'gone = ""', t_open, '1.8', '"',
'gone = """"', '1.9')]:
# reset text and self
self.paren_close = paren_close_set
self.tick_close = tick_close_set
self.skip_closures = skip_closures_set
self.text.delete('1.0', 'end')
# write text, move to current position, write character
self.text.insert('1.0', insert)
self.text.mark_set('insert', pos)
event = MockEvent(char)
func(self, event)
p = self.text.index('insert')
self.text.insert(p, char)
# checking position and text at the same time
# makes it easier to spot where errors occur
p = self.text.index('insert')
actual = self.text.get('1.0', 'end')
self.assertTupleEqual((actual, p), (result+'\n', posend))

if __name__ == '__main__':
unittest.main(verbosity=2)
Empty file modified Tools/ssl/multissltests.py
100755 → 100644
Empty file.