bpo-32989: IDLE - fix bad editor call of pyparse method (GH-5968)

Fix comments and add tests for editor newline_and_indent_event method.
Remove unused None default for function parameter of pyparse find_good_parse_start method
and code triggered by that default.

Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
This commit is contained in:
Cheryl Sabella 2020-01-21 05:11:26 -05:00 committed by Terry Jan Reedy
parent 8698b34b68
commit ec64640a2c
6 changed files with 154 additions and 39 deletions

View File

@ -3,6 +3,9 @@ Released on 2020-10-05?
====================================== ======================================
bpo-32989: Add tests for editor newline_and_indent_event method.
Remove dead code from pyparse find_good_parse_start method.
bpo-38943: Fix autocomplete windows not always appearing on some bpo-38943: Fix autocomplete windows not always appearing on some
systems. Patch by Johnny Najera. systems. Patch by Johnny Najera.

View File

@ -1342,38 +1342,51 @@ class EditorWindow(object):
text.undo_block_stop() text.undo_block_stop()
def newline_and_indent_event(self, event): def newline_and_indent_event(self, event):
"""Insert a newline and indentation after Enter keypress event.
Properly position the cursor on the new line based on information
from the current line. This takes into account if the current line
is a shell prompt, is empty, has selected text, contains a block
opener, contains a block closer, is a continuation line, or
is inside a string.
"""
text = self.text text = self.text
first, last = self.get_selection_indices() first, last = self.get_selection_indices()
text.undo_block_start() text.undo_block_start()
try: try: # Close undo block and expose new line in finally clause.
if first and last: if first and last:
text.delete(first, last) text.delete(first, last)
text.mark_set("insert", first) text.mark_set("insert", first)
line = text.get("insert linestart", "insert") line = text.get("insert linestart", "insert")
# Count leading whitespace for indent size.
i, n = 0, len(line) i, n = 0, len(line)
while i < n and line[i] in " \t": while i < n and line[i] in " \t":
i = i+1 i += 1
if i == n: if i == n:
# the cursor is in or at leading indentation in a continuation # The cursor is in or at leading indentation in a continuation
# line; just inject an empty line at the start # line; just inject an empty line at the start.
text.insert("insert linestart", '\n') text.insert("insert linestart", '\n')
return "break" return "break"
indent = line[:i] indent = line[:i]
# strip whitespace before insert point unless it's in the prompt
# Strip whitespace before insert point unless it's in the prompt.
i = 0 i = 0
while line and line[-1] in " \t" and line != self.prompt_last_line: while line and line[-1] in " \t" and line != self.prompt_last_line:
line = line[:-1] line = line[:-1]
i = i+1 i += 1
if i: if i:
text.delete("insert - %d chars" % i, "insert") text.delete("insert - %d chars" % i, "insert")
# strip whitespace after insert point
# Strip whitespace after insert point.
while text.get("insert") in " \t": while text.get("insert") in " \t":
text.delete("insert") text.delete("insert")
# start new line
# Insert new line.
text.insert("insert", '\n') text.insert("insert", '\n')
# adjust indentation for continuations and block # Adjust indentation for continuations and block open/close.
# open/close first need to find the last stmt # First need to find the last statement.
lno = index2line(text.index('insert')) lno = index2line(text.index('insert'))
y = pyparse.Parser(self.indentwidth, self.tabwidth) y = pyparse.Parser(self.indentwidth, self.tabwidth)
if not self.prompt_last_line: if not self.prompt_last_line:
@ -1383,7 +1396,7 @@ class EditorWindow(object):
rawtext = text.get(startatindex, "insert") rawtext = text.get(startatindex, "insert")
y.set_code(rawtext) y.set_code(rawtext)
bod = y.find_good_parse_start( bod = y.find_good_parse_start(
self._build_char_in_string_func(startatindex)) self._build_char_in_string_func(startatindex))
if bod is not None or startat == 1: if bod is not None or startat == 1:
break break
y.set_lo(bod or 0) y.set_lo(bod or 0)
@ -1399,26 +1412,26 @@ class EditorWindow(object):
c = y.get_continuation_type() c = y.get_continuation_type()
if c != pyparse.C_NONE: if c != pyparse.C_NONE:
# The current stmt hasn't ended yet. # The current statement hasn't ended yet.
if c == pyparse.C_STRING_FIRST_LINE: if c == pyparse.C_STRING_FIRST_LINE:
# after the first line of a string; do not indent at all # After the first line of a string do not indent at all.
pass pass
elif c == pyparse.C_STRING_NEXT_LINES: elif c == pyparse.C_STRING_NEXT_LINES:
# inside a string which started before this line; # Inside a string which started before this line;
# just mimic the current indent # just mimic the current indent.
text.insert("insert", indent) text.insert("insert", indent)
elif c == pyparse.C_BRACKET: elif c == pyparse.C_BRACKET:
# line up with the first (if any) element of the # Line up with the first (if any) element of the
# last open bracket structure; else indent one # last open bracket structure; else indent one
# level beyond the indent of the line with the # level beyond the indent of the line with the
# last open bracket # last open bracket.
self.reindent_to(y.compute_bracket_indent()) self.reindent_to(y.compute_bracket_indent())
elif c == pyparse.C_BACKSLASH: elif c == pyparse.C_BACKSLASH:
# if more than one line in this stmt already, just # If more than one line in this statement already, just
# mimic the current indent; else if initial line # mimic the current indent; else if initial line
# has a start on an assignment stmt, indent to # has a start on an assignment stmt, indent to
# beyond leftmost =; else to beyond first chunk of # beyond leftmost =; else to beyond first chunk of
# non-whitespace on initial line # non-whitespace on initial line.
if y.get_num_lines_in_stmt() > 1: if y.get_num_lines_in_stmt() > 1:
text.insert("insert", indent) text.insert("insert", indent)
else: else:
@ -1427,9 +1440,9 @@ class EditorWindow(object):
assert 0, "bogus continuation type %r" % (c,) assert 0, "bogus continuation type %r" % (c,)
return "break" return "break"
# This line starts a brand new stmt; indent relative to # This line starts a brand new statement; indent relative to
# indentation of initial line of closest preceding # indentation of initial line of closest preceding
# interesting stmt. # interesting statement.
indent = y.get_base_indent_string() indent = y.get_base_indent_string()
text.insert("insert", indent) text.insert("insert", indent)
if y.is_block_opener(): if y.is_block_opener():

View File

@ -2,6 +2,7 @@
from idlelib import editor from idlelib import editor
import unittest import unittest
from collections import namedtuple
from test.support import requires from test.support import requires
from tkinter import Tk from tkinter import Tk
@ -91,5 +92,103 @@ class TestGetLineIndent(unittest.TestCase):
) )
class IndentAndNewlineTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.window = Editor(root=cls.root)
cls.window.indentwidth = 2
cls.window.tabwidth = 2
@classmethod
def tearDownClass(cls):
cls.window._close()
del cls.window
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
def insert(self, text):
t = self.window.text
t.delete('1.0', 'end')
t.insert('end', text)
# Force update for colorizer to finish.
t.update()
def test_indent_and_newline_event(self):
eq = self.assertEqual
w = self.window
text = w.text
get = text.get
nl = w.newline_and_indent_event
TestInfo = namedtuple('Tests', ['label', 'text', 'expected', 'mark'])
tests = (TestInfo('Empty line inserts with no indent.',
' \n def __init__(self):',
'\n \n def __init__(self):\n',
'1.end'),
TestInfo('Inside bracket before space, deletes space.',
' def f1(self, a, b):',
' def f1(self,\n a, b):\n',
'1.14'),
TestInfo('Inside bracket after space, deletes space.',
' def f1(self, a, b):',
' def f1(self,\n a, b):\n',
'1.15'),
TestInfo('Inside string with one line - no indent.',
' """Docstring."""',
' """Docstring.\n"""\n',
'1.15'),
TestInfo('Inside string with more than one line.',
' """Docstring.\n Docstring Line 2"""',
' """Docstring.\n Docstring Line 2\n """\n',
'2.18'),
TestInfo('Backslash with one line.',
'a =\\',
'a =\\\n \n',
'1.end'),
TestInfo('Backslash with more than one line.',
'a =\\\n multiline\\',
'a =\\\n multiline\\\n \n',
'2.end'),
TestInfo('Block opener - indents +1 level.',
' def f1(self):\n pass',
' def f1(self):\n \n pass\n',
'1.end'),
TestInfo('Block closer - dedents -1 level.',
' def f1(self):\n pass',
' def f1(self):\n pass\n \n',
'2.end'),
)
w.prompt_last_line = ''
for test in tests:
with self.subTest(label=test.label):
self.insert(test.text)
text.mark_set('insert', test.mark)
nl(event=None)
eq(get('1.0', 'end'), test.expected)
# Selected text.
self.insert(' def f1(self, a, b):\n return a + b')
text.tag_add('sel', '1.17', '1.end')
nl(None)
# Deletes selected text before adding new line.
eq(get('1.0', 'end'), ' def f1(self, a,\n \n return a + b\n')
# Preserves the whitespace in shell prompt.
w.prompt_last_line = '>>> '
self.insert('>>> \t\ta =')
text.mark_set('insert', '1.5')
nl(None)
eq(get('1.0', 'end'), '>>> \na =\n')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main(verbosity=2) unittest.main(verbosity=2)

View File

@ -18,7 +18,7 @@ class ParseMapTest(unittest.TestCase):
# trans is the production instance of ParseMap, used in _study1 # trans is the production instance of ParseMap, used in _study1
parser = pyparse.Parser(4, 4) parser = pyparse.Parser(4, 4)
self.assertEqual('\t a([{b}])b"c\'d\n'.translate(pyparse.trans), self.assertEqual('\t a([{b}])b"c\'d\n'.translate(pyparse.trans),
'xxx(((x)))x"x\'x\n') 'xxx(((x)))x"x\'x\n')
class PyParseTest(unittest.TestCase): class PyParseTest(unittest.TestCase):
@ -61,14 +61,17 @@ class PyParseTest(unittest.TestCase):
# Split def across lines. # Split def across lines.
setcode('"""This is a module docstring"""\n' setcode('"""This is a module docstring"""\n'
'class C():\n' 'class C():\n'
' def __init__(self, a,\n' ' def __init__(self, a,\n'
' b=True):\n' ' b=True):\n'
' pass\n' ' pass\n'
) )
# No value sent for is_char_in_string(). # Passing no value or non-callable should fail (issue 32989).
self.assertIsNone(start()) with self.assertRaises(TypeError):
start()
with self.assertRaises(TypeError):
start(False)
# Make text look like a string. This returns pos as the start # Make text look like a string. This returns pos as the start
# position, but it's set to None. # position, but it's set to None.
@ -91,10 +94,10 @@ class PyParseTest(unittest.TestCase):
# Code without extra line break in def line - mostly returns the same # Code without extra line break in def line - mostly returns the same
# values. # values.
setcode('"""This is a module docstring"""\n' setcode('"""This is a module docstring"""\n'
'class C():\n' 'class C():\n'
' def __init__(self, a, b=True):\n' ' def __init__(self, a, b=True):\n'
' pass\n' ' pass\n'
) )
eq(start(is_char_in_string=lambda index: False), 44) eq(start(is_char_in_string=lambda index: False), 44)
eq(start(is_char_in_string=lambda index: index > 44), 44) eq(start(is_char_in_string=lambda index: index > 44), 44)
eq(start(is_char_in_string=lambda index: index >= 44), 33) eq(start(is_char_in_string=lambda index: index >= 44), 33)

View File

@ -133,8 +133,7 @@ class Parser:
self.code = s self.code = s
self.study_level = 0 self.study_level = 0
def find_good_parse_start(self, is_char_in_string=None, def find_good_parse_start(self, is_char_in_string, _synchre=_synchre):
_synchre=_synchre):
""" """
Return index of a good place to begin parsing, as close to the Return index of a good place to begin parsing, as close to the
end of the string as possible. This will be the start of some end of the string as possible. This will be the start of some
@ -149,10 +148,6 @@ class Parser:
""" """
code, pos = self.code, None code, pos = self.code, None
if not is_char_in_string:
# no clue -- make the caller pass everything
return None
# Peek back from the end for a good place to start, # Peek back from the end for a good place to start,
# but don't try too often; pos will be left None, or # but don't try too often; pos will be left None, or
# bumped to a legitimate synch point. # bumped to a legitimate synch point.

View File

@ -0,0 +1,2 @@
Add tests for editor newline_and_indent_event method.
Remove dead code from pyparse find_good_parse_start method.