Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Carriage Return Handling in QtConsole #607

Merged
merged 10 commits into from
Aug 12, 2024
64 changes: 49 additions & 15 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 @@ -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
ccordoba12 marked this conversation as resolved.
Show resolved Hide resolved
# 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
ccordoba12 marked this conversation as resolved.
Show resolved Hide resolved
# 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)
dalthviz marked this conversation as resolved.
Show resolved Hide resolved

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"):
TheMatt2 marked this conversation as resolved.
Show resolved Hide resolved
del cursor._insert_mode

return result

def _append_block(self, block_format=None, before_prompt=False):
Expand Down Expand Up @@ -1660,10 +1692,7 @@ def _on_flush_pending_stream_timer(self):
""" Flush the pending stream output and change the
prompt position appropriately.
"""
TheMatt2 marked this conversation as resolved.
Show resolved Hide resolved
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. """
Expand All @@ -1674,7 +1703,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))
Expand Down Expand Up @@ -2093,12 +2122,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 +2152,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 +2166,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 +2203,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 @@ -2531,6 +2562,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)
TheMatt2 marked this conversation as resolved.
Show resolved Hide resolved

def flush_clearoutput(self):
"""If a clearoutput is pending, execute it."""
Expand Down
40 changes: 39 additions & 1 deletion 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)
TheMatt2 marked this conversation as resolved.
Show resolved Hide resolved
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)
TheMatt2 marked this conversation as resolved.
Show resolved Hide resolved
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 Expand Up @@ -448,7 +486,7 @@ def test_prompt_cursors(self):
w._prompt_pos - len(w._prompt))

# insert some text before the prompt
w._append_plain_text('line', before_prompt=True)
w._append_plain_text('line', before_prompt = True)
TheMatt2 marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(w._prompt_pos, w._get_end_pos())
self.assertEqual(w._append_before_prompt_pos,
w._prompt_pos - len(w._prompt))
Expand Down
Loading