Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add Emacs-style kill ring to Qt console.

Closes gh-366.
  • Loading branch information...
commit f8b2edabace694e16beba3e6c28f369e676e1ceb 1 parent 55f24ba
@epatters epatters authored
View
17 IPython/frontend/qt/console/console_widget.py
@@ -22,6 +22,7 @@
from IPython.utils.traitlets import Bool, Enum, Int
from ansi_code_processor import QtAnsiCodeProcessor
from completion_widget import CompletionWidget
+from kill_ring import QtKillRing
#-----------------------------------------------------------------------------
# Functions
@@ -173,6 +174,7 @@ def __init__(self, parent=None, **kw):
self._filter_drag = False
self._filter_resize = False
self._html_exporter = HtmlExporter(self._control)
+ self._kill_ring = QtKillRing(self._control)
self._prompt = ''
self._prompt_html = None
self._prompt_pos = 0
@@ -953,7 +955,7 @@ def _event_filter_console_keypress(self, event):
cursor.movePosition(QtGui.QTextCursor.Right,
QtGui.QTextCursor.KeepAnchor,
len(self._continuation_prompt))
- cursor.removeSelectedText()
+ self._kill_ring.kill_cursor(cursor)
intercepted = True
elif key == QtCore.Qt.Key_L:
@@ -976,11 +978,12 @@ def _event_filter_console_keypress(self, event):
QtGui.QTextCursor.KeepAnchor)
cursor.movePosition(QtGui.QTextCursor.Right,
QtGui.QTextCursor.KeepAnchor, offset)
- cursor.removeSelectedText()
+ self._kill_ring.kill_cursor(cursor)
intercepted = True
elif key == QtCore.Qt.Key_Y:
- self.paste()
+ self._keep_cursor_in_buffer()
+ self._kill_ring.yank()
intercepted = True
elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
@@ -1005,16 +1008,20 @@ def _event_filter_console_keypress(self, event):
self._set_cursor(self._get_word_end_cursor(position))
intercepted = True
+ elif key == QtCore.Qt.Key_Y:
+ self._kill_ring.rotate()
+ intercepted = True
+
elif key == QtCore.Qt.Key_Backspace:
cursor = self._get_word_start_cursor(position)
cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
- cursor.removeSelectedText()
+ self._kill_ring.kill_cursor(cursor)
intercepted = True
elif key == QtCore.Qt.Key_D:
cursor = self._get_word_end_cursor(position)
cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
- cursor.removeSelectedText()
+ self._kill_ring.kill_cursor(cursor)
intercepted = True
elif key == QtCore.Qt.Key_Delete:
View
128 IPython/frontend/qt/console/kill_ring.py
@@ -0,0 +1,128 @@
+""" A generic Emacs-style kill ring, as well as a Qt-specific version.
+"""
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+# System library imports
+from IPython.external.qt import QtCore, QtGui
+
+#-----------------------------------------------------------------------------
+# Classes
+#-----------------------------------------------------------------------------
+
+class KillRing(object):
+ """ A generic Emacs-style kill ring.
+ """
+
+ def __init__(self):
+ self.clear()
+
+ def clear(self):
+ """ Clears the kill ring.
+ """
+ self._index = -1
+ self._ring = []
+
+ def kill(self, text):
+ """ Adds some killed text to the ring.
+ """
+ self._ring.append(text)
+
+ def yank(self):
+ """ Yank back the most recently killed text.
+
+ Returns:
+ --------
+ A text string or None.
+ """
+ self._index = len(self._ring)
+ return self.rotate()
+
+ def rotate(self):
+ """ Rotate the kill ring, then yank back the new top.
+
+ Returns:
+ --------
+ A text string or None.
+ """
+ self._index -= 1
+ if self._index >= 0:
+ return self._ring[self._index]
+ return None
+
+class QtKillRing(QtCore.QObject):
+ """ A kill ring attached to Q[Plain]TextEdit.
+ """
+
+ #--------------------------------------------------------------------------
+ # QtKillRing interface
+ #--------------------------------------------------------------------------
+
+ def __init__(self, text_edit):
+ """ Create a kill ring attached to the specified Qt text edit.
+ """
+ assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
+ super(QtKillRing, self).__init__()
+
+ self._ring = KillRing()
+ self._prev_yank = None
+ self._skip_cursor = False
+ self._text_edit = text_edit
+
+ text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
+
+ def clear(self):
+ """ Clears the kill ring.
+ """
+ self._ring.clear()
+ self._prev_yank = None
+
+ def kill(self, text):
+ """ Adds some killed text to the ring.
+ """
+ self._ring.kill(text)
+
+ def kill_cursor(self, cursor):
+ """ Kills the text selected by the give cursor.
+ """
+ text = cursor.selectedText()
+ if text:
+ cursor.removeSelectedText()
+ self.kill(text)
+
+ def yank(self):
+ """ Yank back the most recently killed text.
+ """
+ text = self._ring.yank()
+ if text:
+ self._skip_cursor = True
+ cursor = self._text_edit.textCursor()
+ cursor.insertText(text)
+ self._prev_yank = text
+
+ def rotate(self):
+ """ Rotate the kill ring, then yank back the new top.
+ """
+ if self._prev_yank:
+ text = self._ring.rotate()
+ if text:
+ self._skip_cursor = True
+ cursor = self._text_edit.textCursor()
+ cursor.movePosition(QtGui.QTextCursor.Left,
+ QtGui.QTextCursor.KeepAnchor,
+ n = len(self._prev_yank))
+ cursor.insertText(text)
+ self._prev_yank = text
+
+ #--------------------------------------------------------------------------
+ # Protected interface
+ #--------------------------------------------------------------------------
+
+ #------ Signal handlers ----------------------------------------------------
+
+ def _cursor_position_changed(self):
+ if self._skip_cursor:
+ self._skip_cursor = False
+ else:
+ self._prev_yank = None
View
83 IPython/frontend/qt/console/tests/test_kill_ring.py
@@ -0,0 +1,83 @@
+# Standard library imports
+import unittest
+
+# System library imports
+from IPython.external.qt import QtCore, QtGui
+
+# Local imports
+from IPython.frontend.qt.console.kill_ring import KillRing, QtKillRing
+
+
+class TestKillRing(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ """ Create the application for the test case.
+ """
+ cls._app = QtGui.QApplication([])
+ cls._app.setQuitOnLastWindowClosed(False)
+
+ @classmethod
+ def tearDownClass(cls):
+ """ Exit the application.
+ """
+ QtGui.QApplication.quit()
+
+ def test_generic(self):
+ """ Does the generic kill ring work?
+ """
+ ring = KillRing()
+ self.assert_(ring.yank() is None)
+ self.assert_(ring.rotate() is None)
+
+ ring.kill('foo')
+ self.assertEqual(ring.yank(), 'foo')
+ self.assert_(ring.rotate() is None)
+ self.assertEqual(ring.yank(), 'foo')
+
+ ring.kill('bar')
+ self.assertEqual(ring.yank(), 'bar')
+ self.assertEqual(ring.rotate(), 'foo')
+
+ ring.clear()
+ self.assert_(ring.yank() is None)
+ self.assert_(ring.rotate() is None)
+
+ def test_qt_basic(self):
+ """ Does the Qt kill ring work?
+ """
+ text_edit = QtGui.QPlainTextEdit()
+ ring = QtKillRing(text_edit)
+
+ ring.kill('foo')
+ ring.kill('bar')
+ ring.yank()
+ ring.rotate()
+ ring.yank()
+ self.assertEqual(text_edit.toPlainText(), 'foobar')
+
+ text_edit.clear()
+ ring.kill('baz')
+ ring.yank()
+ ring.rotate()
+ ring.rotate()
+ ring.rotate()
+ self.assertEqual(text_edit.toPlainText(), 'foo')
+
+ def test_qt_cursor(self):
+ """ Does the Qt kill ring maintain state with cursor movement?
+ """
+ text_edit = QtGui.QPlainTextEdit()
+ ring = QtKillRing(text_edit)
+
+ ring.kill('foo')
+ ring.kill('bar')
+ ring.yank()
+ text_edit.moveCursor(QtGui.QTextCursor.Left)
+ ring.rotate()
+ self.assertEqual(text_edit.toPlainText(), 'bar')
+
+
+if __name__ == '__main__':
+ import nose
+ nose.main()
Please sign in to comment.
Something went wrong with that request. Please try again.