Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Add Emacs-style kill ring to Qt console.

Closes gh-366.
  • Loading branch information...
commit f8b2edabace694e16beba3e6c28f369e676e1ceb 1 parent 55f24ba
Evan Patterson authored April 16, 2011
17  IPython/frontend/qt/console/console_widget.py
@@ -22,6 +22,7 @@
22 22
 from IPython.utils.traitlets import Bool, Enum, Int
23 23
 from ansi_code_processor import QtAnsiCodeProcessor
24 24
 from completion_widget import CompletionWidget
  25
+from kill_ring import QtKillRing
25 26
 
26 27
 #-----------------------------------------------------------------------------
27 28
 # Functions
@@ -173,6 +174,7 @@ def __init__(self, parent=None, **kw):
173 174
         self._filter_drag = False
174 175
         self._filter_resize = False
175 176
         self._html_exporter = HtmlExporter(self._control)
  177
+        self._kill_ring = QtKillRing(self._control)
176 178
         self._prompt = ''
177 179
         self._prompt_html = None
178 180
         self._prompt_pos = 0
@@ -953,7 +955,7 @@ def _event_filter_console_keypress(self, event):
953 955
                         cursor.movePosition(QtGui.QTextCursor.Right,
954 956
                                             QtGui.QTextCursor.KeepAnchor,
955 957
                                             len(self._continuation_prompt))
956  
-                    cursor.removeSelectedText()
  958
+                    self._kill_ring.kill_cursor(cursor)
957 959
                 intercepted = True
958 960
 
959 961
             elif key == QtCore.Qt.Key_L:
@@ -976,11 +978,12 @@ def _event_filter_console_keypress(self, event):
976 978
                                         QtGui.QTextCursor.KeepAnchor)
977 979
                     cursor.movePosition(QtGui.QTextCursor.Right,
978 980
                                         QtGui.QTextCursor.KeepAnchor, offset)
979  
-                    cursor.removeSelectedText()
  981
+                    self._kill_ring.kill_cursor(cursor)
980 982
                 intercepted = True
981 983
 
982 984
             elif key == QtCore.Qt.Key_Y:
983  
-                self.paste()
  985
+                self._keep_cursor_in_buffer()
  986
+                self._kill_ring.yank()
984 987
                 intercepted = True
985 988
 
986 989
             elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
@@ -1005,16 +1008,20 @@ def _event_filter_console_keypress(self, event):
1005 1008
                 self._set_cursor(self._get_word_end_cursor(position))
1006 1009
                 intercepted = True
1007 1010
 
  1011
+            elif key == QtCore.Qt.Key_Y:
  1012
+                self._kill_ring.rotate()
  1013
+                intercepted = True
  1014
+
1008 1015
             elif key == QtCore.Qt.Key_Backspace:
1009 1016
                 cursor = self._get_word_start_cursor(position)
1010 1017
                 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1011  
-                cursor.removeSelectedText()
  1018
+                self._kill_ring.kill_cursor(cursor)
1012 1019
                 intercepted = True
1013 1020
 
1014 1021
             elif key == QtCore.Qt.Key_D:
1015 1022
                 cursor = self._get_word_end_cursor(position)
1016 1023
                 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1017  
-                cursor.removeSelectedText()
  1024
+                self._kill_ring.kill_cursor(cursor)
1018 1025
                 intercepted = True
1019 1026
 
1020 1027
             elif key == QtCore.Qt.Key_Delete:
128  IPython/frontend/qt/console/kill_ring.py
... ...
@@ -0,0 +1,128 @@
  1
+""" A generic Emacs-style kill ring, as well as a Qt-specific version.
  2
+"""
  3
+#-----------------------------------------------------------------------------
  4
+# Imports
  5
+#-----------------------------------------------------------------------------
  6
+
  7
+# System library imports
  8
+from IPython.external.qt import QtCore, QtGui
  9
+
  10
+#-----------------------------------------------------------------------------
  11
+# Classes
  12
+#-----------------------------------------------------------------------------
  13
+
  14
+class KillRing(object):
  15
+    """ A generic Emacs-style kill ring.
  16
+    """
  17
+    
  18
+    def __init__(self):
  19
+        self.clear()
  20
+
  21
+    def clear(self):
  22
+        """ Clears the kill ring.
  23
+        """
  24
+        self._index = -1
  25
+        self._ring = []
  26
+
  27
+    def kill(self, text):
  28
+        """ Adds some killed text to the ring.
  29
+        """
  30
+        self._ring.append(text)
  31
+
  32
+    def yank(self):
  33
+        """ Yank back the most recently killed text.
  34
+
  35
+        Returns:
  36
+        --------
  37
+        A text string or None.
  38
+        """
  39
+        self._index = len(self._ring)
  40
+        return self.rotate()
  41
+
  42
+    def rotate(self):
  43
+        """ Rotate the kill ring, then yank back the new top.
  44
+        
  45
+        Returns:
  46
+        --------
  47
+        A text string or None.
  48
+        """
  49
+        self._index -= 1
  50
+        if self._index >= 0:
  51
+            return self._ring[self._index]
  52
+        return None
  53
+        
  54
+class QtKillRing(QtCore.QObject):
  55
+    """ A kill ring attached to Q[Plain]TextEdit.
  56
+    """
  57
+
  58
+    #--------------------------------------------------------------------------
  59
+    # QtKillRing interface
  60
+    #--------------------------------------------------------------------------
  61
+
  62
+    def __init__(self, text_edit):
  63
+        """ Create a kill ring attached to the specified Qt text edit.
  64
+        """
  65
+        assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
  66
+        super(QtKillRing, self).__init__()
  67
+
  68
+        self._ring = KillRing()
  69
+        self._prev_yank = None
  70
+        self._skip_cursor = False
  71
+        self._text_edit = text_edit
  72
+
  73
+        text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
  74
+
  75
+    def clear(self):
  76
+        """ Clears the kill ring.
  77
+        """
  78
+        self._ring.clear()
  79
+        self._prev_yank = None
  80
+
  81
+    def kill(self, text):
  82
+        """ Adds some killed text to the ring.
  83
+        """
  84
+        self._ring.kill(text)
  85
+
  86
+    def kill_cursor(self, cursor):
  87
+        """ Kills the text selected by the give cursor.
  88
+        """
  89
+        text = cursor.selectedText()
  90
+        if text:
  91
+            cursor.removeSelectedText()
  92
+            self.kill(text)
  93
+
  94
+    def yank(self):
  95
+        """ Yank back the most recently killed text.
  96
+        """
  97
+        text = self._ring.yank()
  98
+        if text:
  99
+            self._skip_cursor = True
  100
+            cursor = self._text_edit.textCursor()
  101
+            cursor.insertText(text)
  102
+            self._prev_yank = text
  103
+
  104
+    def rotate(self):
  105
+        """ Rotate the kill ring, then yank back the new top.
  106
+        """
  107
+        if self._prev_yank:
  108
+            text = self._ring.rotate()
  109
+            if text:
  110
+                self._skip_cursor = True
  111
+                cursor = self._text_edit.textCursor()
  112
+                cursor.movePosition(QtGui.QTextCursor.Left, 
  113
+                                    QtGui.QTextCursor.KeepAnchor, 
  114
+                                    n = len(self._prev_yank))
  115
+                cursor.insertText(text)
  116
+                self._prev_yank = text
  117
+        
  118
+    #--------------------------------------------------------------------------
  119
+    # Protected interface
  120
+    #--------------------------------------------------------------------------
  121
+
  122
+    #------ Signal handlers ----------------------------------------------------
  123
+
  124
+    def _cursor_position_changed(self):
  125
+        if self._skip_cursor:
  126
+            self._skip_cursor = False
  127
+        else:
  128
+            self._prev_yank = None
83  IPython/frontend/qt/console/tests/test_kill_ring.py
... ...
@@ -0,0 +1,83 @@
  1
+# Standard library imports
  2
+import unittest
  3
+
  4
+# System library imports
  5
+from IPython.external.qt import QtCore, QtGui
  6
+
  7
+# Local imports
  8
+from IPython.frontend.qt.console.kill_ring import KillRing, QtKillRing
  9
+
  10
+
  11
+class TestKillRing(unittest.TestCase):
  12
+
  13
+    @classmethod
  14
+    def setUpClass(cls):
  15
+        """ Create the application for the test case.
  16
+        """
  17
+        cls._app = QtGui.QApplication([])
  18
+        cls._app.setQuitOnLastWindowClosed(False)
  19
+
  20
+    @classmethod
  21
+    def tearDownClass(cls):
  22
+        """ Exit the application.
  23
+        """
  24
+        QtGui.QApplication.quit()
  25
+
  26
+    def test_generic(self):
  27
+        """ Does the generic kill ring work?
  28
+        """
  29
+        ring = KillRing()
  30
+        self.assert_(ring.yank() is None)
  31
+        self.assert_(ring.rotate() is None)
  32
+
  33
+        ring.kill('foo')
  34
+        self.assertEqual(ring.yank(), 'foo')
  35
+        self.assert_(ring.rotate() is None)
  36
+        self.assertEqual(ring.yank(), 'foo')
  37
+
  38
+        ring.kill('bar')
  39
+        self.assertEqual(ring.yank(), 'bar')
  40
+        self.assertEqual(ring.rotate(), 'foo')
  41
+
  42
+        ring.clear()
  43
+        self.assert_(ring.yank() is None)
  44
+        self.assert_(ring.rotate() is None)
  45
+
  46
+    def test_qt_basic(self):
  47
+        """ Does the Qt kill ring work?
  48
+        """
  49
+        text_edit = QtGui.QPlainTextEdit()
  50
+        ring = QtKillRing(text_edit)
  51
+
  52
+        ring.kill('foo')
  53
+        ring.kill('bar')
  54
+        ring.yank()
  55
+        ring.rotate()
  56
+        ring.yank()
  57
+        self.assertEqual(text_edit.toPlainText(), 'foobar')
  58
+
  59
+        text_edit.clear()
  60
+        ring.kill('baz')
  61
+        ring.yank()
  62
+        ring.rotate()
  63
+        ring.rotate()
  64
+        ring.rotate()
  65
+        self.assertEqual(text_edit.toPlainText(), 'foo')
  66
+
  67
+    def test_qt_cursor(self):
  68
+        """ Does the Qt kill ring maintain state with cursor movement?
  69
+        """
  70
+        text_edit = QtGui.QPlainTextEdit()
  71
+        ring = QtKillRing(text_edit)
  72
+        
  73
+        ring.kill('foo')
  74
+        ring.kill('bar')
  75
+        ring.yank()
  76
+        text_edit.moveCursor(QtGui.QTextCursor.Left)
  77
+        ring.rotate()
  78
+        self.assertEqual(text_edit.toPlainText(), 'bar')
  79
+
  80
+
  81
+if __name__ == '__main__':
  82
+    import nose
  83
+    nose.main()

0 notes on commit f8b2eda

Please sign in to comment.
Something went wrong with that request. Please try again.