Skip to content

Commit

Permalink
Merge pull request #607 from TheMatt2/fix-carriage-return
Browse files Browse the repository at this point in the history
Fix Carriage Return Handling in QtConsole
  • Loading branch information
dalthviz authored Aug 12, 2024
2 parents 03cc68a + dbf7562 commit 564dcf9
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 24 deletions.
82 changes: 59 additions & 23 deletions qtconsole/console_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1657,24 +1689,23 @@ 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()
if buffer_size > 0:
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))
Expand Down Expand Up @@ -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():
Expand All @@ -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':
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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 ----------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion qtconsole/frontend_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
38 changes: 38 additions & 0 deletions qtconsole/tests/test_00_console_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 564dcf9

Please sign in to comment.