Skip to content
Closed
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
61 changes: 32 additions & 29 deletions Lib/idlelib/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,6 @@ def __init__(self, flist=None, filename=None, key=None, root=None):
idleConf.blink_off_time = self.text['insertofftime']
self.update_cursor_blink()

# When searching backwards for a reliable place to begin parsing,
# first start num_context_lines[0] lines back, then
# num_context_lines[1] lines back if that didn't work, and so on.
# The last value should be huge (larger than the # of lines in a
# conceivable file).
# Making the initial values larger slows things down more often.
self.num_context_lines = 50, 500, 5000000
self.per = per = self.Percolator(text)
self.undo = undo = self.UndoDelegator()
per.insertfilter(undo)
Expand Down Expand Up @@ -1396,28 +1389,7 @@ def newline_and_indent_event(self, event):

# Adjust indentation for continuations and block open/close.
# First need to find the last statement.
lno = index2line(text.index('insert'))
y = pyparse.Parser(self.indentwidth, self.tabwidth)
if not self.prompt_last_line:
for context in self.num_context_lines:
startat = max(lno - context, 1)
startatindex = repr(startat) + ".0"
rawtext = text.get(startatindex, "insert")
y.set_code(rawtext)
bod = y.find_good_parse_start(
self._build_char_in_string_func(startatindex))
if bod is not None or startat == 1:
break
y.set_lo(bod or 0)
else:
r = text.tag_prevrange("console", "insert")
if r:
startatindex = r[1]
else:
startatindex = "1.0"
rawtext = text.get(startatindex, "insert")
y.set_code(rawtext)
y.set_lo(0)
y = self.parser()

c = y.get_continuation_type()
if c != pyparse.C_NONE:
Expand Down Expand Up @@ -1474,6 +1446,37 @@ def inner(offset, _startindex=startindex,
return _icis(_startindex + "+%dc" % offset)
return inner

def parser(self, index="insert", lineend=""):
"Create a PyParse instance and find the last statement."
text = self.text
p = pyparse.Parser(self.indentwidth, self.tabwidth)
lineno = self.getlineno(index)
stopatindex = index if not lineend else f"{lineno}.end"

if self.prompt_last_line:
r = text.tag_prevrange("console", index)
startatindex = r[1] if r else "1.0"
Copy link
Member

Choose a reason for hiding this comment

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

'1.0', copied from the old code, seems wrong, as it is the Shell top, before the sign-in message, let alone the first prompt. If one hits Enter at the prompt, I would think that the code should be set to '\n'.

I need to add some prints to be sure of what happens in Shell and editor with various inputs.

Copy link
Contributor Author

@csabella csabella Feb 18, 2020

Choose a reason for hiding this comment

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

In the shell, it looks like it will always find the last console tag, which is marked from the beginning to the end of the prompt. For example, in a new shell window, typing >>> def hello( without pressing Enter shows r to be (3.0, 3.4).

I wasn't able to think of a scenario where the prompt would be missing in a real shell, so I'm not sure if the default of 1.0 would ever be used there. However, adding a print(r) and running the testsuite shows that the console tag isn't set for most tests, so it falls back to the default.

Copy link
Member

Choose a reason for hiding this comment

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

Then the shell tests are unrealistic. Of course, with the prompt moved to a sidebar, we should be looking, I believe, for a mark.

Copy link
Contributor

@taleinat taleinat Oct 4, 2020

Choose a reason for hiding this comment

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

Of course, with the prompt moved to a sidebar, we should be looking, I believe, for a mark.

With the shell moved to a sidebar (PR GH-15474), there's still a "console" tag on the newline (\n) before the beginning of the current input block, making code such as this continue to "just work".

Copy link
Contributor

@taleinat taleinat Oct 4, 2020

Choose a reason for hiding this comment

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

While we're refactoring this, I think it's a great opportunity to remove the shell-specific part of this code from the EditorWindow class and into PyShell.

p.set_code(text.get(startatindex, stopatindex) + lineend)
return p

# When searching backwards for a reliable place to begin parsing,
# first start num_context_lines[0] lines back, then
# num_context_lines[1] lines back if that didn't work, and so on.
# The last value should be huge (larger than the # of lines in a
# conceivable file).
# Making the initial values larger slows things down more often.
num_context_lines = 50, 500, 5000000
for context in num_context_lines:
startat = max(lineno - context, 1)
startatindex = f"{startat}.0"
p.set_code(text.get(startatindex, stopatindex) + lineend)
bod = p.find_good_parse_start(
self._build_char_in_string_func(startatindex))
if bod is not None or startat == 1:
break
p.set_lo(bod or 0)
return p

# XXX this isn't bound to anything -- see tabwidth comments
## def change_tabwidth_event(self, event):
## new = self._asktabwidth()
Expand Down
40 changes: 6 additions & 34 deletions Lib/idlelib/hyperparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,46 +29,18 @@ def __init__(self, editwin, index):
self.editwin = editwin
self.text = text = editwin.text

parser = pyparse.Parser(editwin.indentwidth, editwin.tabwidth)

def index2line(index):
return int(float(index))
lno = index2line(text.index(index))

if not editwin.prompt_last_line:
for context in editwin.num_context_lines:
startat = max(lno - context, 1)
startatindex = repr(startat) + ".0"
stopatindex = "%d.end" % lno
# We add the newline because PyParse requires a newline
# at end. We add a space so that index won't be at end
# of line, so that its status will be the same as the
# char before it, if should.
parser.set_code(text.get(startatindex, stopatindex)+' \n')
bod = parser.find_good_parse_start(
editwin._build_char_in_string_func(startatindex))
if bod is not None or startat == 1:
break
parser.set_lo(bod or 0)
else:
r = text.tag_prevrange("console", index)
if r:
startatindex = r[1]
else:
startatindex = "1.0"
stopatindex = "%d.end" % lno
# We add the newline because PyParse requires it. We add a
# space so that index won't be at end of line, so that its
# status will be the same as the char before it, if should.
parser.set_code(text.get(startatindex, stopatindex)+' \n')
parser.set_lo(0)
# We add the newline because PyParse requires a newline
# at end. We add a space so that index won't be at end
# of line, so that its status will be the same as the
# char before it.
parser = editwin.parser(index=index, lineend=" \n")

# We want what the parser has, minus the last newline and space.
self.rawtext = parser.code[:-2]
# Parser.code apparently preserves the statement we are in, so
# that stopatindex can be used to synchronize the string with
# the text box indices.
self.stopatindex = stopatindex
self.stopatindex = f"{editwin.getlineno(index)}.end"
self.bracketing = parser.get_last_stmt_bracketing()
# find which pairs of bracketing are openers. These always
# correspond to a character of rawtext.
Expand Down
4 changes: 4 additions & 0 deletions Lib/idlelib/idle_test/test_autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import idlelib.autocomplete as ac
import idlelib.autocomplete_w as acw
from idlelib.editor import EditorWindow
from idlelib.idle_test.mock_idle import Func
from idlelib.idle_test.mock_tk import Event

Expand All @@ -21,6 +22,9 @@ def __init__(self, root, text):
self.tabwidth = 8
self.prompt_last_line = '>>>' # Currently not used by autocomplete.

getlineno = EditorWindow.getlineno
parser = EditorWindow.parser


class AutoCompleteTest(unittest.TestCase):

Expand Down
75 changes: 72 additions & 3 deletions Lib/idlelib/idle_test/test_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,17 @@ def tearDownClass(cls):
cls.root.destroy()
del cls.root

def test_indent_and_newline_event(self):
@classmethod
def tearDown(self):
self.window.text.delete("1.0", "end-1c")

def test_indent_and_newline_event_editor(self):
eq = self.assertEqual
w = self.window
text = w.text
get = text.get
nl = w.newline_and_indent_event
w.prompt_last_line = ''

TestInfo = namedtuple('Tests', ['label', 'text', 'expected', 'mark'])

Expand Down Expand Up @@ -167,7 +172,6 @@ def test_indent_and_newline_event(self):
'2.end'),
)

w.prompt_last_line = ''
for test in tests:
with self.subTest(label=test.label):
insert(text, test.text)
Expand All @@ -182,13 +186,78 @@ def test_indent_and_newline_event(self):
# 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.
def test_indent_and_newline_event_shell(self):
eq = self.assertEqual
w = self.window
text = w.text
get = text.get
nl = w.newline_and_indent_event
w.prompt_last_line = '>>> '
insert(text, '>>> \t\ta =')
text.mark_set('insert', '1.5')
text.tag_add("console", "1.0", "1.4")
nl(None)
eq(get('1.0', 'end'), '>>> \na =\n')

def test_parser_editor(self):
eq = self.assertEqual
w = self.window
text = w.text
p = w.parser
w.prompt_last_line = ''

eq(p().code, "")

# Two lines with block opener.
insert(text, " def f1(self, a, b):\n return a + b\n")
eq(p().code, " return a + b\n")

# No newline in code.
insert(text, " def f1(")
eq(p(lineend=" \n").code, " def f1( \n")

# Two lines without block opener.
code = ("a = 1\n"
"b = 2\n")
insert(text, code)
eq(p().code, code)

# No newline in code.
insert(text, " def f1(")
eq(p(lineend=" \n").code, " def f1( \n")

def test_parser_shell(self):
eq = self.assertEqual
w = self.window
text = w.text
p = w.parser
w.prompt_last_line = '>>> '

# One line with block opener.
code = (">>> def f1(self, a, b):\n")
insert(text, code)
text.tag_add("console", "1.0", "1.4")
# The prompt is removed with the parser.
eq(p().code, code[4:])

# Two lines with block opener parses everything but prompt.
code = (">>> def f1(self, a, b):\n return a + b\n")
insert(text, code)
text.tag_add("console", "1.0", "1.4")
eq(p().code, code[4:])

# Two lines without block opener.
code = (">>> a = 1\n"
">>> b = 2\n")
insert(text, code)
text.tag_add("console", "2.0", "2.4")
eq(p().code, "b = 2\n")

# No newline in code.
insert(text, ">>> def f1(")
text.tag_add("console", "1.0", "1.4")
eq(p(lineend=" \n").code, " def f1( \n")


class RMenuTest(unittest.TestCase):

Expand Down
3 changes: 2 additions & 1 deletion Lib/idlelib/idle_test/test_hyperparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ def __init__(self, text):
self.indentwidth = 8
self.tabwidth = 8
self.prompt_last_line = '>>>'
self.num_context_lines = 50, 500, 1000

_build_char_in_string_func = EditorWindow._build_char_in_string_func
is_char_in_string = EditorWindow.is_char_in_string
getlineno = EditorWindow.getlineno
parser = EditorWindow.parser


class HyperParserTest(unittest.TestCase):
Expand Down
4 changes: 3 additions & 1 deletion Lib/idlelib/idle_test/test_parenmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import unittest
from unittest.mock import Mock
from tkinter import Tk, Text
from idlelib.editor import EditorWindow


class DummyEditwin:
Expand All @@ -18,7 +19,8 @@ def __init__(self, text):
self.indentwidth = 8
self.tabwidth = 8
self.prompt_last_line = '>>>' # Currently not used by parenmatch.

getlineno = EditorWindow.getlineno
parser = EditorWindow.parser

class ParenMatchTest(unittest.TestCase):

Expand Down