-
-
Notifications
You must be signed in to change notification settings - Fork 30.8k
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
base: main
Are you sure you want to change the base?
Changes from 74 commits
5c57faf
d49170f
8c3a966
757bb38
d7e136c
b178af5
2adebb9
86c3d4c
1c246fc
0418ad0
097a0d8
4232e77
a7b29f5
e30b7b4
252672b
999b573
1a74591
3501c45
232f711
f095c53
a661bfa
5c90a8f
5a6a007
709b434
b9b1f9a
8b76ccb
b986c12
f86acaa
5805462
165b548
aee5d02
cb161bd
dc09a2b
100463e
3fbaa90
60ff82f
e3f6f16
a253f0c
fe8eb7a
caa7774
c29184e
c62f726
d0e009f
9e889c2
6afdea3
7de27db
a2e76a7
2265b53
03eed24
207b0ca
00acd22
2d2bfbd
fe76cd3
d2966e0
6845bb0
b7afebb
b1a4101
7a53366
d3b6848
eb58d39
119bbfc
814efbe
7f596b9
26323e2
7862d8a
2f19ca8
6d2fa26
15f8d82
d141abc
caa9240
21f4482
25ba0f8
a4cd0e3
cffa9d6
9aaa395
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
""" | ||
Parens and Ticks Closing Extension | ||
|
||
When you hit left paren or tick, | ||
automatically creates the closing paren or tick. | ||
|
||
Author: Charles M. Wohlganger | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 13-Aug-2017 by Charles Wohlganger | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove. Already obsolete. The git log has revision history. |
||
|
||
Add to the end of config-extensions.def : | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delete. |
||
paren_close = True | ||
tick_close = True | ||
skip_closures = True | ||
mutual_delete = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think defaults should be 'off', as in no change from current behavior. I am not sure what latter 2 mean. See below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing the specification of the new feature by adding action on deletion should have been discussed on the issue. (This applies generally to changing specs.) Notepad++ does do anything special on deletion. Binding the deletion keys here prevents binding them elsewhere, and I have an idea of where it might be needed, as opposed to optional. I also think sometimes delete more and sometimes not is confusing. The deletion part of 2f19ca8 and anything later should be reverted unless and until there is a clear agreement on adding this. |
||
[ParenClose_cfgBindings] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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-quotedbl> <Key-quoteright> | ||
p-close = <Key-parenright> <Key-bracketright> <Key-braceright> | ||
terryjreedy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
back = <Key-BackSpace> | ||
delete = <Key-Delete> | ||
""" | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
If the cursor is between two closures and mutual_delete or mutual_backspace | ||
is True, when the respective command is given, both closures are deleted. | ||
''' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really understand these. |
||
|
||
closers = {'(': ')', '[': ']', '{': '}', "'": "'", '"': '"'} | ||
|
||
def __init__(self, editwin=None): | ||
# Setting default to none makes testing easier. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I an not sure that this will work for a non-extension feature. |
||
if editwin: | ||
self.text = editwin.text | ||
else: | ||
self.text = None | ||
self.paren_close = idleConf.GetOption( | ||
'extensions', 'ParenClose', 'paren_close', | ||
type='bool', default=True) | ||
self.tick_close = idleConf.GetOption( | ||
'extensions', 'ParenClose', 'tick_close', | ||
type='bool', default=True) | ||
self.skip_closures = idleConf.GetOption( | ||
'extensions', 'ParenClose', 'skip_closures', | ||
type='bool', default=True) | ||
self.mutual_delete = idleConf.GetOption( | ||
'extensions', 'ParenClose', 'mutual_delete', | ||
type='bool', default=True) | ||
self.mutual_backspace = idleConf.GetOption( | ||
'extensions', 'ParenClose', 'mutual_backspace', | ||
type='bool', default=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Getting the options should be done in a |
||
|
||
def delcheck(self, pos): | ||
symbol1 = self.text.get(pos + '-1c', pos) | ||
symbol2 = self.text.get(pos, pos + '+1c') | ||
return (symbol1 in self.closers | ||
and self.closers[symbol1] == symbol2) | ||
|
||
def back_event(self, event): | ||
if self.mutual_backspace: | ||
pos = self.text.index('insert') | ||
if self.delcheck(pos): | ||
self.text.delete(pos, pos + '+1c') | ||
|
||
def delete_event(self, event): | ||
if self.mutual_delete: | ||
pos = self.text.index('insert') | ||
if self.delcheck(pos): | ||
self.text.delete(pos + '-1c', pos) | ||
|
||
def p_open_event(self, event): | ||
if self.paren_close: | ||
pos = self.text.index('insert') | ||
self.text.insert(pos, self.closers[event.char]) | ||
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') | ||
return True | ||
return False | ||
|
||
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 three 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) | ||
elif not self.p_close_event(event): | ||
self.p_open_event(event) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have not yet looked at the instance method code. |
||
|
||
|
||
if __name__ == '__main__': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,3 +63,17 @@ z-text= Z | |
z-in= <Control-Shift-KeyRelease-Insert> | ||
[ZzDummy_bindings] | ||
z-out= <Control-Shift-KeyRelease-Delete> | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
mutual_delete = True | ||
mutual_backspace = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the configuration options should be discussed more on the issue tracker. |
||
[ParenClose_bindings] | ||
p-open = <Key-parenleft> <Key-bracketleft> <Key-braceleft> | ||
t-open = <Key-quotedbl> <Key-quoteright> | ||
p-close = <Key-parenright> <Key-bracketright> <Key-braceright> | ||
back = <Key-BackSpace> | ||
delete = <Key-Delete> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, non-configurable bindings are not needed here. On the other hand, the bindings will have to be added elsewhere. Currently, these are done in EditorWindow.init. In the future, I want to think about and maybe try putting feature bindings in the feature class. Also, I believe they could be class rather than instance bindings, as they are the same for all instances. EDIT: Key bindings could, maybe should, be done in a loop iterating through (key, event_handler) pairs, and in a separate method. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -439,21 +439,21 @@ def test_get_extensions(self): | |
''') | ||
eq = self.assertEqual | ||
iGE = idleConf.GetExtensions | ||
eq(iGE(shell_only=True), []) | ||
eq(iGE(), ['ZzDummy']) | ||
eq(iGE(editor_only=True), ['ZzDummy']) | ||
eq(iGE(active_only=False), ['ZzDummy', 'DISABLE']) | ||
eq(iGE(active_only=False, editor_only=True), ['ZzDummy', 'DISABLE']) | ||
eq(iGE(shell_only=True), ['ParenClose']) | ||
eq(iGE(), ['ZzDummy', 'ParenClose']) | ||
eq(iGE(editor_only=True), ['ZzDummy', 'ParenClose']) | ||
eq(iGE(active_only=False), ['ZzDummy', 'ParenClose', 'DISABLE']) | ||
eq(iGE(active_only=False, editor_only=True), ['ZzDummy', 'ParenClose', 'DISABLE']) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that all these changes to test_config only apply to extensions, and are therefore not needed. |
||
userextn.remove_section('ZzDummy') | ||
userextn.remove_section('DISABLE') | ||
|
||
|
||
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): | ||
userextn.read_string(''' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
'''Test idlelib.parenclose. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
# Can't use mock text; it doesn't handle positioning correctly. | ||
from tkinter import Text | ||
from test.support import requires | ||
requires('gui') | ||
|
||
class MockEvent(object): | ||
def __init__(self, char): | ||
self.char = char | ||
|
||
|
||
class ParenCloseTest(unittest.TestCase): | ||
def test_parenClose(self): | ||
p = ParenClose() | ||
p.text = Text() | ||
p_open = p.p_open_event | ||
p_close = p.p_close_event | ||
t_open = p.t_open_event | ||
back = p.back_event | ||
delete = p.delete_event | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not like testing all 5 methods in one method. On the other hand, I don't like repeating code, and using subtests allows multiple failures in one method. A helper function might allow separate test functions without duplication, but this is a lower priority than getting good coverage to start with. |
||
for paren_close_set, tick_close_set, skip_closures_set, mutual_b_set, \ | ||
mutual_d_set, insert, func, pos, char, result, posend in [ | ||
(1, 1, 1, 1, 1, 'def abuse', p_open, '1.9', '(', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
'def abuse()', '1.10'), | ||
(1, 1, 1, 1, 1, 'spam = ', p_open, '1.7', '[', | ||
'spam = []', '1.8'), | ||
(1, 1, 1, 1, 1, 'eggs = ', p_open, '1.7', '{', | ||
'eggs = {}', '1.8'), | ||
(1, 1, 1, 1, 1, 'def abuse2()', p_close, '1.11', ')', | ||
'def abuse2()', '1.12'), | ||
(1, 1, 1, 1, 1, 'spam2 = []', p_close, '1.9', ']', | ||
'spam2 = []', '1.10'), | ||
(1, 1, 1, 1, 1, 'eggs2 = {}', p_close, '1.9', '}', | ||
'eggs2 = {}', '1.10'), | ||
(1, 1, 1, 1, 1, "color = ", t_open, '1.8', "'", | ||
"color = ''", '1.9'), | ||
(1, 1, 1, 1, 1, "velocity = ", t_open, '1.11', '"', | ||
'velocity = ""', '1.12'), | ||
(1, 1, 1, 1, 1, "more_spam = ''", t_open, '1.14', "'", | ||
"more_spam = ''''''", '1.15'), | ||
(1, 1, 1, 1, 1, 'more_eggs = ""', t_open, '1.14', '"', | ||
'more_eggs = """"""', '1.15'), | ||
(1, 1, 1, 1, 1, "more_spam2 = ''''''", t_open, '1.16', "'", | ||
"more_spam2 = ''''''", '1.17'), | ||
(1, 1, 1, 1, 1, 'more_eggs2 = """"""', t_open, '1.16', '"', | ||
'more_eggs2 = """"""', '1.17'), | ||
(0, 1, 1, 1, 1, 'no_spam = ', p_open, '1.10', '(', | ||
'no_spam = (', '1.11'), | ||
(1, 0, 1, 1, 1, 'no_velocity = ', t_open, '1.14', "'", | ||
"no_velocity = '", '1.15'), | ||
(1, 0, 1, 1, 1, 'no_more_eggs = ""', t_open, '1.17', '"', | ||
'no_more_eggs = """', '1.18'), | ||
(1, 1, 0, 1, 1, 'tinny = []', p_close, '1.9', ']', | ||
'tinny = []]', '1.10'), | ||
(1, 1, 0, 1, 1, 'gone = ""', t_open, '1.8', '"', | ||
'gone = """"', '1.9'), | ||
(0, 0, 0, 0, 1, "honk = []", back, '1.8', '', | ||
"honk = []", '1.8'), | ||
(0, 0, 0, 1, 0, "henk = ()", delete, '1.8', '', | ||
"henk = ()", '1.8'), | ||
(0, 0, 0, 1, 1, 'nurb = ""', back, '1.8', '', | ||
'nurb = "', '1.8'), | ||
(0, 0, 0, 1, 1, 'chiz = []', delete, '1.8', '', | ||
'chiz = ]', '1.7'), | ||
(0, 0, 0, 1, 1, 'pink = {}', back, '1.8', '', | ||
'pink = {', '1.8') | ||
]: | ||
# The actual delete and backspace events do not occur in | ||
# this test. We are checking to make sure everything is | ||
# correct before they would delete or backspace. | ||
|
||
# Reset text and self. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
p.paren_close = paren_close_set | ||
p.tick_close = tick_close_set | ||
p.skip_closures = skip_closures_set | ||
p.mutual_delete = mutual_d_set | ||
p.mutual_backspace = mutual_b_set | ||
p.text.delete('1.0', 'end') | ||
# Write text, move to current position, write character. | ||
p.text.insert('1.0', insert) | ||
p.text.mark_set('insert', pos) | ||
event = MockEvent(char) | ||
func(event) | ||
pos = p.text.index('insert') | ||
p.text.insert(pos, char) | ||
# Checking position and text at the same time | ||
# makes it easier to spot where errors occur. | ||
pos = p.text.index('insert') | ||
actual = p.text.get('1.0', 'end') | ||
self.assertTupleEqual((actual, pos), (result+'\n', posend)) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main(verbosity=2) |
There was a problem hiding this comment.
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.