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

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
Loading