From 331ecc9823b5ca98f6f0b07db986aba4062e6680 2010-08-13 18:51:14
From: epatters <epatters@enthought.com>
Date: 2010-08-13 18:51:14
Subject: [PATCH] Numerous fixes to ConsoleWidget editing region maintenence code. It now impossible (or at least very difficult :) to break the input region.

---

diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py
index cefd80e..cd2e4f4 100644
--- a/IPython/frontend/qt/console/console_widget.py
+++ b/IPython/frontend/qt/console/console_widget.py
@@ -221,8 +221,13 @@ class ConsoleWidget(QtGui.QWidget):
     def _set_input_buffer(self, string):
         """ Replaces the text in the input buffer with 'string'.
         """
+        # For now, it is an error to modify the input buffer during execution.
+        if self._executing:
+            raise RuntimeError("Cannot change input buffer during execution.")
+
         # Remove old text.
         cursor = self._get_end_cursor()
+        cursor.beginEditBlock()
         cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
         cursor.removeSelectedText()
 
@@ -236,6 +241,7 @@ class ConsoleWidget(QtGui.QWidget):
                 else:
                     self._append_html(self._continuation_prompt_html)
                 self._append_plain_text(lines[i])
+        cursor.endEditBlock()
         self._control.moveCursor(QtGui.QTextCursor.End)
 
     input_buffer = property(_get_input_buffer, _set_input_buffer)
@@ -392,12 +398,14 @@ class ConsoleWidget(QtGui.QWidget):
             ANSI codes if enabled.
         """
         cursor = self._get_end_cursor()
+        cursor.beginEditBlock()
         if self.ansi_codes:
             for substring in self._ansi_processor.split_string(text):
                 format = self._ansi_processor.get_format()
                 cursor.insertText(substring, format)
         else:
             cursor.insertText(text)
+        cursor.endEditBlock()
 
     def _append_plain_text_keeping_prompt(self, text):
         """ Writes 'text' after the current prompt, then restores the old prompt
@@ -490,8 +498,8 @@ class ConsoleWidget(QtGui.QWidget):
             intercepted = True
 
         elif ctrl_down:
-            if key == QtCore.Qt.Key_C and self._executing:
-                intercepted = self._execute_interrupt()
+            if key == QtCore.Qt.Key_C:
+                intercepted = self._executing and self._execute_interrupt()
 
             elif key == QtCore.Qt.Key_K:
                 if self._in_buffer(position):
@@ -563,7 +571,7 @@ class ConsoleWidget(QtGui.QWidget):
                 intercepted = not self._in_buffer(position - 1)
 
             elif key == QtCore.Qt.Key_Home:
-                cursor.movePosition(QtGui.QTextCursor.StartOfLine)
+                cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
                 start_line = cursor.blockNumber()
                 if start_line == self._get_prompt_cursor().blockNumber():
                     start_pos = self._prompt_pos
@@ -576,15 +584,15 @@ class ConsoleWidget(QtGui.QWidget):
                     self._set_position(start_pos)
                 intercepted = True
 
-            elif key == QtCore.Qt.Key_Backspace and not alt_down:
+            elif key == QtCore.Qt.Key_Backspace:
 
                 # Line deletion (remove continuation prompt)
                 len_prompt = len(self._continuation_prompt)
                 if not self._reading and \
                         cursor.columnNumber() == len_prompt and \
                         position != self._prompt_pos:
-                    cursor.setPosition(position - len_prompt, 
-                                       QtGui.QTextCursor.KeepAnchor)
+                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
+                                        QtGui.QTextCursor.KeepAnchor)
                     cursor.removeSelectedText()
 
                 # Regular backwards deletion
@@ -733,10 +741,10 @@ class ConsoleWidget(QtGui.QWidget):
         """
         document = self._control.document()
         position -= 1
-        while self._in_buffer(position) and \
+        while position >= self._prompt_pos and \
                 not document.characterAt(position).isLetterOrNumber():
             position -= 1
-        while self._in_buffer(position) and \
+        while position >= self._prompt_pos and \
                 document.characterAt(position).isLetterOrNumber():
             position -= 1
         cursor = self._control.textCursor()
@@ -764,6 +772,7 @@ class ConsoleWidget(QtGui.QWidget):
         """ Insert HTML using the specified cursor in such a way that future
             formatting is unaffected.
         """
+        cursor.beginEditBlock()
         cursor.insertHtml(html)
 
         # After inserting HTML, the text document "remembers" it's in "html
@@ -776,6 +785,7 @@ class ConsoleWidget(QtGui.QWidget):
             cursor.removeSelectedText()
         cursor.movePosition(QtGui.QTextCursor.Right)
         cursor.insertText(' ', QtGui.QTextCharFormat())
+        cursor.endEditBlock()
 
     def _insert_into_buffer(self, text):
         """ Inserts text into the input buffer at the current cursor position,
@@ -799,19 +809,29 @@ class ConsoleWidget(QtGui.QWidget):
     def _in_buffer(self, position):
         """ Returns whether the given position is inside the editing region.
         """
-        return position >= self._prompt_pos
+        cursor = self._control.textCursor()
+        cursor.setPosition(position)
+        line = cursor.blockNumber()
+        prompt_line = self._get_prompt_cursor().blockNumber()
+        if line == prompt_line:
+            return position >= self._prompt_pos
+        elif line > prompt_line:
+            cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
+            prompt_pos = cursor.position() + len(self._continuation_prompt)
+            return position >= prompt_pos
+        return False
 
     def _keep_cursor_in_buffer(self):
         """ Ensures that the cursor is inside the editing region. Returns
             whether the cursor was moved.
         """
         cursor = self._control.textCursor()
-        if cursor.position() < self._prompt_pos:
+        if self._in_buffer(cursor.position()):
+            return False
+        else:
             cursor.movePosition(QtGui.QTextCursor.End)
             self._control.setTextCursor(cursor)
             return True
-        else:
-            return False
 
     def _prompt_started(self):
         """ Called immediately after a new prompt is displayed.
diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py
index 00d39fa..e16eb0b 100644
--- a/IPython/frontend/qt/console/frontend_widget.py
+++ b/IPython/frontend/qt/console/frontend_widget.py
@@ -261,7 +261,7 @@ class FrontendWidget(HistoryConsoleWidget):
         """
         if cursor is None:
             cursor = self._get_cursor()
-        cursor.movePosition(QtGui.QTextCursor.StartOfLine, 
+        cursor.movePosition(QtGui.QTextCursor.StartOfBlock, 
                             QtGui.QTextCursor.KeepAnchor)
         text = str(cursor.selection().toPlainText())
         return self._completion_lexer.get_context(text)