diff --git a/qtconsole/console_widget.py b/qtconsole/console_widget.py index 62d20377..ebb8d23a 100644 --- a/qtconsole/console_widget.py +++ b/qtconsole/console_widget.py @@ -297,6 +297,10 @@ def __init__(self, parent=None, **kw): self._reading_callback = None self._tab_width = 4 + # Cursor position of where to insert text. + # Control characters allow this to move around on the current line. + self._insert_text_cursor = self._control.textCursor() + # List of strings pending to be appended as plain text in the widget. # The text is not immediately inserted when available to not # choke the Qt event loop with paint events for the widget in @@ -695,6 +699,9 @@ def do_execute(self, source, complete, indent): # effect when using a QTextEdit. I believe this is a Qt bug. self._control.moveCursor(QtGui.QTextCursor.End) + # Advance where text is inserted + self._insert_text_cursor.movePosition(QtGui.QTextCursor.End) + def export_html(self): """ Shows a dialog to export HTML/XML in various formats. """ @@ -712,6 +719,9 @@ def _finalize_input_request(self): self._append_before_prompt_cursor.setPosition( self._get_end_cursor().position()) + self._insert_text_cursor.setPosition( + self._get_end_cursor().position()) + # The maximum block count is only in effect during execution. # This ensures that _prompt_pos does not become invalid due to # text truncation. @@ -841,12 +851,12 @@ def paste(self, mode=QtGui.QClipboard.Clipboard): self._insert_plain_text_into_buffer(cursor, dedent(text)) - def print_(self, printer = None): + def print_(self, printer=None): """ Print the contents of the ConsoleWidget to the specified QPrinter. """ - if (not printer): + if not printer: printer = QtPrintSupport.QPrinter() - if(QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted): + if QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted: return self._control.print_(printer) @@ -998,18 +1008,40 @@ def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs): current prompt, if there is one. """ # Determine where to insert the content. - cursor = self._control.textCursor() + cursor = self._insert_text_cursor if before_prompt and (self._reading or not self._executing): self._flush_pending_stream() - cursor._insert_mode=True - cursor.setPosition(self._append_before_prompt_pos) + + # Jump to before prompt, if there is one + if cursor.position() >= self._append_before_prompt_pos \ + and self._append_before_prompt_pos != self._get_end_pos(): + cursor.setPosition(self._append_before_prompt_pos) + + # If we appending on the same line as the prompt, use insert mode + # If so, the character at self._append_before_prompt_pos will not be a newline + cursor.movePosition(QtGui.QTextCursor.Right, + QtGui.QTextCursor.KeepAnchor) + if cursor.selection().toPlainText() != '\n': + cursor._insert_mode = True + cursor.movePosition(QtGui.QTextCursor.Left) else: + # Insert at current printing point + # If cursor is before prompt jump to end, but only if there + # is a prompt (before_prompt_pos != end) + if cursor.position() <= self._append_before_prompt_pos \ + and self._append_before_prompt_pos != self._get_end_pos(): + cursor.movePosition(QtGui.QTextCursor.End) + if insert != self._insert_plain_text: self._flush_pending_stream() - cursor.movePosition(QtGui.QTextCursor.End) # Perform the insertion. result = insert(cursor, input, *args, **kwargs) + + # Remove insert mode tag + if hasattr(cursor, '_insert_mode'): + del cursor._insert_mode + return result def _append_block(self, block_format=None, before_prompt=False): @@ -1045,7 +1077,7 @@ def _clear_temporary_buffer(self): # Select and remove all text below the input buffer. cursor = self._get_prompt_cursor() prompt = self._continuation_prompt.lstrip() - if(self._temp_buffer_filled): + if self._temp_buffer_filled: self._temp_buffer_filled = False while cursor.movePosition(QtGui.QTextCursor.NextBlock): temp_cursor = QtGui.QTextCursor(cursor) @@ -1657,16 +1689,15 @@ def _event_filter_page_keypress(self, event): return False def _on_flush_pending_stream_timer(self): - """ Flush the pending stream output and change the - prompt position appropriately. + """ Flush pending text into the widget on console timer trigger. """ - cursor = self._control.textCursor() - cursor.movePosition(QtGui.QTextCursor.End) self._flush_pending_stream() - cursor.movePosition(QtGui.QTextCursor.End) def _flush_pending_stream(self): - """ Flush out pending text into the widget. """ + """ Flush pending text into the widget. Only applies to text that is pending + when the console is in the running state. Text printed when console is + not running is shown immediately, and does not wait to be flushed. + """ text = self._pending_insert_text self._pending_insert_text = [] buffer_size = self._control.document().maximumBlockCount() @@ -1674,7 +1705,7 @@ def _flush_pending_stream(self): text = self._get_last_lines_from_list(text, buffer_size) text = ''.join(text) t = time.time() - self._insert_plain_text(self._get_end_cursor(), text, flush=True) + self._insert_plain_text(self._insert_text_cursor, text, flush=True) # Set the flush interval to equal the maximum time to update text. self._pending_text_flush_interval.setInterval( int(max(100, (time.time() - t) * 1000)) @@ -2093,12 +2124,12 @@ def _insert_plain_text(self, cursor, text, flush=False): if (self._executing and not flush and self._pending_text_flush_interval.isActive() and - cursor.position() == self._get_end_pos()): + cursor.position() == self._insert_text_cursor.position()): # Queue the text to insert in case it is being inserted at end self._pending_insert_text.append(text) if buffer_size > 0: self._pending_insert_text = self._get_last_lines_from_list( - self._pending_insert_text, buffer_size) + self._pending_insert_text, buffer_size) return if self._executing and not self._pending_text_flush_interval.isActive(): @@ -2123,7 +2154,7 @@ def _insert_plain_text(self, cursor, text, flush=False): cursor.select(QtGui.QTextCursor.Document) remove = True if act.area == 'line': - if act.erase_to == 'all': + if act.erase_to == 'all': cursor.select(QtGui.QTextCursor.LineUnderCursor) remove = True elif act.erase_to == 'start': @@ -2137,7 +2168,7 @@ def _insert_plain_text(self, cursor, text, flush=False): QtGui.QTextCursor.EndOfLine, QtGui.QTextCursor.KeepAnchor) remove = True - if remove: + if remove: nspace=cursor.selectionEnd()-cursor.selectionStart() if fill else 0 cursor.removeSelectedText() if nspace>0: cursor.insertText(' '*nspace) # replace text by space, to keep cursor position as specified @@ -2174,15 +2205,17 @@ def _insert_plain_text(self, cursor, text, flush=False): # simulate replacement mode if substring is not None: format = self._ansi_processor.get_format() - if not (hasattr(cursor,'_insert_mode') and cursor._insert_mode): + + # Note that using _insert_mode means the \r ANSI sequence will not swallow characters. + if not (hasattr(cursor, '_insert_mode') and cursor._insert_mode): pos = cursor.position() cursor2 = QtGui.QTextCursor(cursor) # self._get_line_end_pos() is the previous line, don't use it cursor2.movePosition(QtGui.QTextCursor.EndOfLine) remain = cursor2.position() - pos # number of characters until end of line n=len(substring) swallow = min(n, remain) # number of character to swallow - cursor.setPosition(pos+swallow,QtGui.QTextCursor.KeepAnchor) - cursor.insertText(substring,format) + cursor.setPosition(pos + swallow, QtGui.QTextCursor.KeepAnchor) + cursor.insertText(substring, format) else: cursor.insertText(text) cursor.endEditBlock() @@ -2399,7 +2432,7 @@ def _readline(self, prompt='', callback=None, password=False): self._reading = True if password: - self._show_prompt('Warning: QtConsole does not support password mode, '\ + self._show_prompt('Warning: QtConsole does not support password mode, ' 'the text you type will be visible.', newline=True) if 'ipdb' not in prompt.lower(): @@ -2531,6 +2564,9 @@ def _show_prompt(self, prompt=None, html=False, newline=True, if move_forward: self._append_before_prompt_cursor.setPosition( self._append_before_prompt_cursor.position() + 1) + else: + # cursor position was 0, set before prompt cursor + self._append_before_prompt_cursor.setPosition(0) self._prompt_started() #------ Signal handlers ---------------------------------------------------- diff --git a/qtconsole/frontend_widget.py b/qtconsole/frontend_widget.py index 0a788bb9..8e2fbec8 100644 --- a/qtconsole/frontend_widget.py +++ b/qtconsole/frontend_widget.py @@ -728,7 +728,7 @@ def restart_kernel(self, message, now=False): def append_stream(self, text): """Appends text to the output stream.""" - self._append_plain_text(text, before_prompt=True) + self._append_plain_text(text, before_prompt = True) def flush_clearoutput(self): """If a clearoutput is pending, execute it.""" diff --git a/qtconsole/tests/test_00_console_widget.py b/qtconsole/tests/test_00_console_widget.py index 2745be6e..c9b571e8 100644 --- a/qtconsole/tests/test_00_console_widget.py +++ b/qtconsole/tests/test_00_console_widget.py @@ -371,6 +371,44 @@ def test_erase_in_line(self): # clear all the text cursor.insertText('') + def test_print_carriage_return(self): + """ Test that overwriting the current line works as intended, + before and after the cursor prompt. + """ + w = ConsoleWidget() + + # Show a prompt + w._prompt = "prompt>" + w._prompt_sep = "\n" + + w._show_prompt() + self.assert_text_equal(w._get_cursor(), '\u2029prompt>') + + test_inputs = ['Hello\n', 'World\r', + '*' * 10, '\r', + '0', '1', '2', '3', '4', + '5', '6', '7', '8', '9', + '\r\n'] + + for text in test_inputs: + w._append_plain_text(text, before_prompt=True) + w._flush_pending_stream() # emulate text being flushed + + self.assert_text_equal(w._get_cursor(), + "Hello\u20290123456789\u2029\u2029prompt>") + + # Print after prompt + w._executing = True + test_inputs = ['\nF', 'o', 'o', + '\r', 'Bar', '\n'] + + for text in test_inputs: + w._append_plain_text(text, before_prompt=False) + w._flush_pending_stream() # emulate text being flushed + + self.assert_text_equal(w._get_cursor(), + "Hello\u20290123456789\u2029\u2029prompt>\u2029Bar\u2029") + def test_link_handling(self): noButton = QtCore.Qt.NoButton noButtons = QtCore.Qt.NoButton