Skip to content
This repository

Add Emacs-style kill ring to Qt console #388

Merged
merged 1 commit into from almost 3 years ago

2 participants

Evan Patterson Min RK
Evan Patterson
Collaborator

Addressing #366.

Evan Patterson
Collaborator

Any feedback on this? Since this is a non-trivial code addition, I'd prefer to have some before committing. Thanks.

Min RK
Owner
minrk commented May 09, 2011

How is this supposed to behave? Reading the code it looks like there is support for more than one item in the ring, but I can't find a way to get anything other than the most recent kill out. (not an emacs user).

Evan Patterson
Collaborator

Use Ctrl-Y to retrieve the most recent item from the kill ring. Use Meta-Y to cycle through previous items in the kill ring. Note that M-Y only has an affect when used immediately after C-Y. So the sequence is "C-Y once, plus M-Y zero or more times".

By the way, all of this also works in bash, although I don't use it there very often myself.

Min RK
Owner
minrk commented May 09, 2011

okay, thanks. So ctrl-y yanks, and meta-y lets you select which entry to yank. That seems to work.

kill doesn't seem to include the newline char, so it doesn't match terminal behavior. Can you fix that?

Evan Patterson
Collaborator

In the terminal and in Emacs, Ctrl-K kills to the end of line, but does not include the newline. (In Emacs, Ctrl-K on an empty line kills the newline. The Qt console supports this for multiline input. In a terminal, there is no such thing as multiline input, so that's not even an option.)

Is Ctrl-K getting a newline in your terminal?

Min RK
Owner
minrk commented May 10, 2011

Ah, I was playing around with too many apps at once. ctrl-k includes newline in pico/nano, but not emacs.

In that case, it does seem to match emacs (even if it's not as nice as the pico behavior).

I don't see any reason not to go ahead and merge, then.

Evan Patterson
Collaborator

Great, thanks.

Evan Patterson epatters merged commit a4b5f34 into from May 10, 2011
Evan Patterson epatters closed this May 10, 2011
Brian E. Granger ellisonbg referenced this pull request from a commit January 10, 2012
Commit has since been removed from the repository and is no longer available.
Damián Avila damianavila referenced this pull request from a commit July 30, 2013
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Apr 16, 2011
Evan Patterson Add Emacs-style kill ring to Qt console.
Closes gh-366.
f8b2eda
This page is out of date. Refresh to see the latest.
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()
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.