Skip to content

Commit

Permalink
Merge branch 'GraphicalExecutionHistory' of https://github.com/jsbaut…
Browse files Browse the repository at this point in the history
…ista/qtconsole into GraphicalExecutionHistory
  • Loading branch information
jsbautista committed Nov 12, 2024
2 parents 0a899cd + 084b9e6 commit 0da3fc2
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 8 deletions.
7 changes: 7 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ Changes in Jupyter Qt console
5.6
~~~

5.6.1
-----

`5.6.1 on GitHub <https://github.com/jupyter/qtconsole/milestones/5.6.1>`__

* Handle ANSI escape sequences that move the cursor.

5.6.0
-----

Expand Down
25 changes: 19 additions & 6 deletions qtconsole/ansi_code_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,7 @@ def split_string(self, string):
self.actions = []
start = 0

# strings ending with \r are assumed to be ending in \r\n since
# \n is appended to output strings automatically. Accounting
# for that, here.
last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None
last_char = None
string = string[:-1] if last_char is not None else string

for match in ANSI_OR_SPECIAL_PATTERN.finditer(string):
Expand All @@ -122,7 +119,7 @@ def split_string(self, string):
self.actions = []
elif g0 == '\n' or g0 == '\r\n':
self.actions.append(NewLineAction('newline'))
yield g0
yield None
self.actions = []
else:
params = [ param for param in groups[1].split(';') if param ]
Expand All @@ -147,7 +144,7 @@ def split_string(self, string):

if last_char is not None:
self.actions.append(NewLineAction('newline'))
yield last_char
yield None

def set_csi_code(self, command, params=[]):
""" Set attributes based on CSI (Control Sequence Introducer) code.
Expand Down Expand Up @@ -185,6 +182,22 @@ def set_csi_code(self, command, params=[]):
count = params[0] if params else 1
self.actions.append(ScrollAction('scroll', dir, 'line', count))

elif command == 'A': # Move N lines Up
dir = 'up'
count = params[0] if params else 1
self.actions.append(MoveAction('move', dir, 'line', count))

elif command == 'B': # Move N lines Down
dir = 'down'
count = params[0] if params else 1
self.actions.append(MoveAction('move', dir, 'line', count))

elif command == 'F': # Goes back to the begining of the n-th previous line
dir = 'leftup'
count = params[0] if params else 1
self.actions.append(MoveAction('move', dir, 'line', count))


def set_osc_code(self, params):
""" Set attributes based on OSC (Operating System Command) parameters.
Expand Down
35 changes: 34 additions & 1 deletion qtconsole/console_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,27 @@ def _insert_plain_text(self, cursor, text, flush=False):
cursor.select(QtGui.QTextCursor.Document)
cursor.removeSelectedText()

elif act.action == 'move' and act.unit == 'line':
if act.dir == 'up':
for i in range(act.count):
cursor.movePosition(
QtGui.QTextCursor.Up
)
elif act.dir == 'down':
for i in range(act.count):
cursor.movePosition(
QtGui.QTextCursor.Down
)
elif act.dir == 'leftup':
for i in range(act.count):
cursor.movePosition(
QtGui.QTextCursor.Up
)
cursor.movePosition(
QtGui.QTextCursor.StartOfLine,
QtGui.QTextCursor.MoveAnchor
)

elif act.action == 'carriage-return':
cursor.movePosition(
QtGui.QTextCursor.StartOfLine,
Expand All @@ -2203,7 +2224,19 @@ def _insert_plain_text(self, cursor, text, flush=False):
QtGui.QTextCursor.MoveAnchor)

elif act.action == 'newline':
cursor.movePosition(QtGui.QTextCursor.EndOfLine)
if (
cursor.block() != cursor.document().lastBlock()
and not cursor.document()
.toPlainText()
.endswith(self._prompt)
):
cursor.movePosition(QtGui.QTextCursor.NextBlock)
else:
cursor.movePosition(
QtGui.QTextCursor.EndOfLine,
QtGui.QTextCursor.MoveAnchor,
)
cursor.insertText("\n")

# simulate replacement mode
if substring is not None:
Expand Down
45 changes: 44 additions & 1 deletion qtconsole/tests/test_ansi_code_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def test_carriage_return_newline(self):
for split in self.processor.split_string(string):
splits.append(split)
actions.append([action.action for action in self.processor.actions])
self.assertEqual(splits, ['foo', None, 'bar', '\r\n', 'cat', '\r\n', '\n'])
self.assertEqual(splits, ['foo', None, 'bar', None, 'cat', None, None])
self.assertEqual(actions, [[], ['carriage-return'], [], ['newline'], [], ['newline'], ['newline']])

def test_beep(self):
Expand Down Expand Up @@ -182,6 +182,49 @@ def test_combined(self):
self.assertEqual(splits, ['abc', None, 'def', None])
self.assertEqual(actions, [[], ['carriage-return'], [], ['backspace']])

def test_move_cursor_up(self):
"""Are the ANSI commands for the cursor movement actions
(movement up and to the beginning of the line) processed correctly?
"""
# This line moves the cursor up once, then moves it up five more lines.
# Next, it moves the cursor to the beginning of the previous line, and
# finally moves it to the beginning of the fifth line above the current
# position
string = '\x1b[A\x1b[5A\x1b[F\x1b[5F'
i = -1
for i, substring in enumerate(self.processor.split_string(string)):
if i == 0:
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'move')
self.assertEqual(action.dir, 'up')
self.assertEqual(action.unit, 'line')
self.assertEqual(action.count, 1)
elif i == 1:
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'move')
self.assertEqual(action.dir, 'up')
self.assertEqual(action.unit, 'line')
self.assertEqual(action.count, 5)
elif i == 2:
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'move')
self.assertEqual(action.dir, 'leftup')
self.assertEqual(action.unit, 'line')
self.assertEqual(action.count, 1)
elif i == 3:
self.assertEqual(len(self.processor.actions), 1)
action = self.processor.actions[0]
self.assertEqual(action.action, 'move')
self.assertEqual(action.dir, 'leftup')
self.assertEqual(action.unit, 'line')
self.assertEqual(action.count, 5)
else:
self.fail('Too many substrings.')
self.assertEqual(i, 3, 'Too few substrings.')


if __name__ == '__main__':
unittest.main()

0 comments on commit 0da3fc2

Please sign in to comment.