diff -r 07cdce316b1d Lib/idlelib/ClearWindow.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/ClearWindow.py Wed Feb 19 20:15:41 2014 +0200 @@ -0,0 +1,86 @@ +import sys +import traceback +from tkinter import INSERT, TclError + +from idlelib.UndoDelegator import DeleteCommand + + +class ClearWindow: + menudefs = [ + ('options', [ + ('Clear Shell Window', '<>'), + ]), + ] + + def __init__(self, editwin): + self.editwin = editwin + self.editwin.text.bind("<>", self.clear_window_event) + + def clear_window_event(self, event=None): + """event handler to be bound to user interactions""" + try: + self.clear_window() + except Exception: + # print a traceback but suppress the exception + traceback.print_exc(file=sys.stderr) + return "break" + + def clear_window(self): + """clear everything before the last prompt in the shell window""" + endpos = self._find_last_prompt_start() + + # to make this action undo-able, it is implemented as an + # UndoDelegator.Command sub-class and added to the EditorWindow's + # UndoDelegator instance + dc = ClearWindowDeleteCommand('1.0', endpos) + self.editwin.undo.addcmd(dc) + + def _find_last_prompt_start(self): + """helper function for finding the beginning of the last shell prompt""" + last_prompt_tag_range = self.editwin.text.tag_prevrange('prompt', 'end') + if last_prompt_tag_range: + return last_prompt_tag_range[0] + else: + try: + return self.editwin.text.index("iomark linestart") + except TclError as exc: + if exc.args[0].startswith('bad text index'): + # there is no 'iomark' tag in the Text widget + raise Exception( + 'ClearWindow could not find the last prompt!') + else: + raise + + +class ClearWindowDeleteCommand(DeleteCommand): + def __init__(self, index1, index2): + DeleteCommand.__init__(self, index1, index2) + + self.dump = None + self.expandingbuttons_dump = None + + def do(self, text): + # dump and remove all of the text + self.dump = text.dump(self.index1, self.index2, all=True) + text.delete(self.index1, self.index2) + text.see(INSERT) + + def redo(self, text): + self.do(text) + + def undo(self, text): + # inspired by "Serializing a text widget" at http://wiki.tcl.tk/9167 + tag_starts = {} + for (key, value, index) in self.dump: + if key == 'text': + text.insert(index, value, '') + elif key == 'tagon': + tag_starts[value] = index + elif key == 'tagoff': + text.tag_add(value, tag_starts.pop(value), index) + + # extend existing tags to the end position + for tag_name in tag_starts: + text.tag_add(tag_name, tag_starts[tag_name], self.index2) + + text.see(INSERT) diff -r 07cdce316b1d Lib/idlelib/PyShell.py --- a/Lib/idlelib/PyShell.py Wed Feb 19 18:32:03 2014 +0100 +++ b/Lib/idlelib/PyShell.py Wed Feb 19 20:15:41 2014 +0200 @@ -1252,12 +1252,14 @@ def showprompt(self): self.resetoutput() + prompt_start = self.text.index("insert") try: s = str(sys.ps1) except: s = "" self.console.write(s) self.text.mark_set("insert", "end-1c") + self.text.tag_add('prompt', prompt_start, "insert") self.set_line_and_column() self.io.reset_undo() diff -r 07cdce316b1d Lib/idlelib/config-extensions.def --- a/Lib/idlelib/config-extensions.def Wed Feb 19 18:32:03 2014 +0100 +++ b/Lib/idlelib/config-extensions.def Wed Feb 19 20:15:41 2014 +0200 @@ -94,3 +94,8 @@ enable_shell=0 enable_editor=1 +[ClearWindow] +enable=1 +enable_editor=0 +[ClearWindow_cfgBindings] +clear-window= diff -r 07cdce316b1d Lib/idlelib/idle_test/test_clearwindow.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/idle_test/test_clearwindow.py Wed Feb 19 20:15:41 2014 +0200 @@ -0,0 +1,222 @@ +from io import StringIO +from tkinter import Text, INSERT, TclError + +import unittest +from unittest.mock import Mock, NonCallableMagicMock, patch, sentinel +from test.support import requires + +from idlelib.ClearWindow import ClearWindow, ClearWindowDeleteCommand + + +class TestClearWindw(unittest.TestCase): + """tests for the ClearWindow class""" + def make_mock_editor_window(self): + """create a mock EditorWindow instance""" + editwin = NonCallableMagicMock() + editwin.extensions = {} + return editwin + + def make_text_widget_with_shell_stuff(self): + requires('gui') + + text_widget = Text() + text_widget.mark_set(INSERT, '1.0') + text_widget.insert(INSERT, '>>> ', 'prompt') + text_widget.insert(INSERT, 'print("testing 123")\n') + text_widget.insert(INSERT, 'testing 123\n', 'stdout') + text_widget.insert(INSERT, '>>> ', 'prompt') + text_widget.mark_set("iomark", INSERT) + text_widget.insert(INSERT, 'print("asdf') + text_widget.mark_set(INSERT, text_widget.index(INSERT) + '-2c') + return text_widget + + def test_init(self): + """test the creation of ClearWindow instances""" + editwin = self.make_mock_editor_window() + clearwindow = ClearWindow(editwin) + self.assertIs(clearwindow.editwin, editwin) + self.assertEquals(editwin.text.bind.call_count, 1) + editwin.text.bind.assert_called_with( + "<>", clearwindow.clear_window_event) + + def test_clear_window_event(self): + """test ClearWindow.clear_window_event()""" + editwin = self.make_mock_editor_window() + clearwindow = ClearWindow(editwin) + clearwindow.clear_window = Mock() + + retval = clearwindow.clear_window_event(sentinel.MOCK_EVENT) + self.assertEquals(retval, "break") + self.assertEquals(clearwindow.clear_window.call_count, 1) + + def test_clear_window_event_catches_exceptions(self): + """test clear_window_event() when clear_window raises an exception""" + editwin = self.make_mock_editor_window() + clearwindow = ClearWindow(editwin) + clearwindow.clear_window = Mock(side_effect=Exception()) + + with patch('sys.stderr', new=StringIO()) as mock_stderr: + retval = clearwindow.clear_window_event(sentinel.MOCK_EVENT) + # clear_window_event should print the exception traceback to stderr + self.assertIn('traceback', mock_stderr.getvalue().lower()) + self.assertEquals(retval, "break") + + def test_clear_window_method_basic(self): + """test a basic invocation of ClearWindow.clear_window()""" + editwin = self.make_mock_editor_window() + clearwindow = ClearWindow(editwin) + clearwindow._find_last_prompt_start = Mock(return_value=sentinel.IDX) + + clearwindow.clear_window() + self.assertEquals(editwin.undo.addcmd.call_count, 1) + cmd = editwin.undo.addcmd.call_args[0][0] + self.assertIsInstance(cmd, ClearWindowDeleteCommand) + self.assertEquals(cmd.index1, '1.0') + self.assertEquals(cmd.index2, sentinel.IDX) + + def test_find_last_prompt_start_no_iomark(self): + """test _find_last_prompt_start() with no 'iomark' mark in the text""" + # this should work wihtout a 'iomark' tag, since it is only used as a + # fall-back when a 'prompt' tag is not found + editwin = self.make_mock_editor_window() + editwin.text = self.make_text_widget_with_shell_stuff() + editwin.text.mark_unset('iomark') + clearwindow = ClearWindow(editwin) + + clearwindow.clear_window() + self.assertEquals(editwin.undo.addcmd.call_count, 1) + cmd = editwin.undo.addcmd.call_args[0][0] + self.assertIsInstance(cmd, ClearWindowDeleteCommand) + self.assertEquals(cmd.index1, '1.0') + self.assertEquals(cmd.index2, '3.0') + + def test_find_last_prompt_start_no_prompt(self): + """test _find_last_prompt_start() with no 'prompt' tag in the text""" + # this should work wihtout a 'prompt' tag, since ClearWindow falls back + # to using the 'iomark' mark instead + editwin = self.make_mock_editor_window() + editwin.text = self.make_text_widget_with_shell_stuff() + editwin.text.tag_remove('prompt', '1.0', 'end') + clearwindow = ClearWindow(editwin) + + clearwindow.clear_window() + self.assertEquals(editwin.undo.addcmd.call_count, 1) + cmd = editwin.undo.addcmd.call_args[0][0] + self.assertIsInstance(cmd, ClearWindowDeleteCommand) + self.assertEquals(cmd.index1, '1.0') + self.assertEquals(cmd.index2, '3.0') + + def test_find_last_prompt_start_iomark_and_prompt(self): + """test _find_last_prompt_start() with both 'prompt' and 'iomark'""" + editwin = self.make_mock_editor_window() + editwin.text = self.make_text_widget_with_shell_stuff() + editwin.text.mark_set('iomark', '2.0') + clearwindow = ClearWindow(editwin) + + # this should use the index of the beginning of the last 'prompt' tag + clearwindow.clear_window() + self.assertEquals(editwin.undo.addcmd.call_count, 1) + cmd = editwin.undo.addcmd.call_args[0][0] + self.assertIsInstance(cmd, ClearWindowDeleteCommand) + self.assertEquals(cmd.index1, '1.0') + self.assertEquals(cmd.index2, '3.0') + + def test_find_last_prompt_start_no_iomark_or_prompt(self): + """test _find_last_prompt_start() with no 'prompt' or 'iomark'""" + editwin = self.make_mock_editor_window() + editwin.text = self.make_text_widget_with_shell_stuff() + editwin.text.tag_remove('prompt', '1.0', 'end') + editwin.text.mark_unset('iomark') + clearwindow = ClearWindow(editwin) + + # this should fail and raise an exception + with self.assertRaises(Exception) as cm: + clearwindow.clear_window() + # the exception raised should not be the original TclError + self.assertNotIsInstance(cm.exception, TclError) + # check the the exception message includes relevant text + self.assertIn('ClearWindow', cm.exception.args[0]) + self.assertIn('prompt', cm.exception.args[0].lower()) + + +class TestClearWindowDeleteCommand(unittest.TestCase): + """tests for the ClearWindowDeleteCommand class""" + def test_init(self): + cmd = ClearWindowDeleteCommand(sentinel.index1, sentinel.index2) + self.assertEquals(cmd.index1, sentinel.index1) + self.assertEquals(cmd.index2, sentinel.index2) + + def test_do(self): + requires('gui') + + text_widget = Text() + text_widget.insert(INSERT, 'text\n') + cmd = ClearWindowDeleteCommand('1.0', 'end-1c') + cmd.do(text_widget) + self.assertEquals(text_widget.get('1.0', 'end-1c'), '') + + def test_undo(self): + requires('gui') + + text_widget = Text() + text_widget.insert(INSERT, 'text\n') + cmd = ClearWindowDeleteCommand('1.0', 'end-1c') + cmd.do(text_widget) + cmd.undo(text_widget) + self.assertEquals(text_widget.get('1.0', 'end-1c'), 'text\n') + + def test_redo(self): + requires('gui') + + text_widget = Text() + text_widget.insert(INSERT, 'text\n') + cmd = ClearWindowDeleteCommand('1.0', 'end-1c') + cmd.do(text_widget) + cmd.undo(text_widget) + cmd.redo(text_widget) + self.assertEquals(text_widget.get('1.0', 'end-1c'), '') + + def test_tags(self): + """an extensive scenario testing the support for tags in the text""" + requires('gui') + + text_widget = Text() + text_widget.insert(INSERT, 'text\n', 'start_tag') + text_widget.insert(INSERT, 'text\n') + text_widget.insert(INSERT, 'some ', 'mid_tag1') + text_widget.insert(INSERT, 'more ', 'mid_tag1', 'mid_tag2') + text_widget.insert(INSERT, 'text\n', 'mid_tag2') + text_widget.insert(INSERT, 'text\n') + text_widget.insert(INSERT, 'text\n', 'mid_tag1') + text_widget.insert(INSERT, 'text\n') + text_widget.insert(INSERT, 'text\n', 'end_tag') + + def get_tag_ranges(): + return dict([ + (tagname, text_widget.tag_ranges(tagname)) + for tagname in ['start_tag', 'mid_tag1', 'mid_tag2', 'end_tag'] + ]) + + orig_text = text_widget.get('1.0', 'end-1c') + orig_tag_ranges = get_tag_ranges() + + cmd = ClearWindowDeleteCommand('1.0', 'end-1c') + cmd.do(text_widget) + + self.assertEquals(text_widget.get('1.0', 'end-1c'), '') + self.assertEquals(list(get_tag_ranges().values()), [(), (), (), ()]) + + cmd.undo(text_widget) + + self.assertEquals(text_widget.get('1.0', 'end-1c'), orig_text) + self.assertEquals(get_tag_ranges(), orig_tag_ranges) + + cmd.redo(text_widget) + + self.assertEquals(text_widget.get('1.0', 'end-1c'), '') + self.assertEquals(list(get_tag_ranges().values()), [(), (), (), ()]) + + cmd.undo(text_widget) + + self.assertEquals(text_widget.get('1.0', 'end-1c'), orig_text) + self.assertEquals(get_tag_ranges(), orig_tag_ranges)