diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 92d9b1e..e31c3f9 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -268,6 +268,11 @@ class ConsoleWidget(QtGui.QWidget): A boolean indicating whether the source was executed. """ if not hidden: + # Do everything here inside an edit block so continuation prompts + # are removed seamlessly via undo/redo. + cursor = self._control.textCursor() + cursor.beginEditBlock() + if source is not None: self.input_buffer = source @@ -284,12 +289,20 @@ class ConsoleWidget(QtGui.QWidget): # This ensures that _prompt_pos does not become invalid due to # text truncation. self._control.document().setMaximumBlockCount(self.buffer_size) + + # Setting a positive maximum block count will automatically + # disable the undo/redo history, but just to be safe: + self._control.setUndoRedoEnabled(False) + self._execute(real_source, hidden) elif hidden: raise RuntimeError('Incomplete noninteractive input: "%s"' % source) else: self._show_continuation_prompt() + if not hidden: + cursor.endEditBlock() + return complete def _get_input_buffer(self): @@ -546,6 +559,7 @@ class ConsoleWidget(QtGui.QWidget): control.redoAvailable.connect(self.redo_available) control.undoAvailable.connect(self.undo_available) control.setReadOnly(True) + control.setUndoRedoEnabled(False) control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) return control @@ -555,6 +569,7 @@ class ConsoleWidget(QtGui.QWidget): control = QtGui.QPlainTextEdit() control.installEventFilter(self) control.setReadOnly(True) + control.setUndoRedoEnabled(False) control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) return control @@ -664,10 +679,12 @@ class ConsoleWidget(QtGui.QWidget): if not self._reading and \ cursor.columnNumber() == len_prompt and \ position != self._prompt_pos: + cursor.beginEditBlock() cursor.movePosition(QtGui.QTextCursor.StartOfBlock, QtGui.QTextCursor.KeepAnchor) cursor.removeSelectedText() cursor.deletePreviousChar() + cursor.endEditBlock() intercepted = True # Regular backwards deletion @@ -679,8 +696,23 @@ class ConsoleWidget(QtGui.QWidget): intercepted = not self._in_buffer(min(anchor, position)) elif key == QtCore.Qt.Key_Delete: - anchor = cursor.anchor() - intercepted = not self._in_buffer(min(anchor, position)) + + # Line deletion (remove continuation prompt) + if not self._reading and cursor.atBlockEnd() and not \ + cursor.hasSelection(): + cursor.movePosition(QtGui.QTextCursor.NextBlock, + QtGui.QTextCursor.KeepAnchor) + cursor.movePosition(QtGui.QTextCursor.Right, + QtGui.QTextCursor.KeepAnchor, + len(self._continuation_prompt)) + cursor.removeSelectedText() + intercepted = True + + # Regular forwards deletion: + else: + anchor = cursor.anchor() + intercepted = (not self._in_buffer(anchor) or + not self._in_buffer(position)) # Don't move the cursor if control is down to allow copy-paste using # the keyboard in any part of the buffer. @@ -1026,7 +1058,10 @@ class ConsoleWidget(QtGui.QWidget): """ # Temporarily disable the maximum block count to permit undo/redo and # to ensure that the prompt position does not change due to truncation. - self._control.document().setMaximumBlockCount(0) + # Because setting this property clears the undo/redo history, we only + # set it if we have to. + if self._control.document().maximumBlockCount() > 0: + self._control.document().setMaximumBlockCount(0) self._control.setUndoRedoEnabled(True) self._control.setReadOnly(False) @@ -1039,7 +1074,6 @@ class ConsoleWidget(QtGui.QWidget): """ Called immediately after a prompt is finished, i.e. when some input will be processed and a new prompt displayed. """ - self._control.setUndoRedoEnabled(False) self._control.setReadOnly(True) self._prompt_finished_hook() diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 593683c..ae1c4b4 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -296,8 +296,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): # Decide if it makes sense to show a call tip cursor = self._get_cursor() cursor.movePosition(QtGui.QTextCursor.Left) - document = self._control.document() - if document.characterAt(cursor.position()).toAscii() != '(': + if cursor.document().characterAt(cursor.position()).toAscii() != '(': return False context = self._get_context(cursor) if not context: @@ -312,14 +311,6 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): def _complete(self): """ Performs completion at the current cursor location. """ - # Decide if it makes sense to do completion - - # We should return only if the line is empty. Otherwise, let the - # kernel split the line up. - line = self._get_input_buffer_cursor_line() - if not line: - return False - # We let the kernel split the input line, so we *always* send an empty # text field. Readline-based frontends do get a real text field which # they can use. @@ -327,12 +318,11 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): # Send the completion request to the kernel self._complete_id = self.kernel_manager.xreq_channel.complete( - text, # text - line, # line + text, # text + self._get_input_buffer_cursor_line(), # line self._get_input_buffer_cursor_column(), # cursor_pos self.input_buffer) # block self._complete_pos = self._get_cursor().position() - return True def _get_banner(self): """ Gets a banner to display at the beginning of a session. @@ -342,7 +332,8 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): return banner % (sys.version, sys.platform) def _get_context(self, cursor=None): - """ Gets the context at the current cursor location. + """ Gets the context for the specified cursor (or the current cursor + if none is specified). """ if cursor is None: cursor = self._get_cursor()