QScintilla/Shell.py

changeset 5798
e4f9552f7f93
parent 5736
000ea446ff4b
child 5799
e87f52c0374a
--- a/QScintilla/Shell.py	Sun Jul 09 19:42:44 2017 +0200
+++ b/QScintilla/Shell.py	Sun Jul 09 19:44:33 2017 +0200
@@ -12,6 +12,11 @@
 import sys
 import re
 
+try:
+    from enum import Enum
+except ImportError:
+    from ThirdParty.enum import Enum
+
 from PyQt5.QtCore import pyqtSignal, QFileInfo, Qt, QEvent
 from PyQt5.QtGui import QClipboard, QPalette, QFont
 from PyQt5.QtWidgets import QDialog, QInputDialog, QApplication, QMenu, \
@@ -83,6 +88,15 @@
         return self.__shell
 
 
+class ShellHistoryStyle(Enum):
+    """
+    Class defining the shell history styles.
+    """
+    Disabled = 0
+    LinuxStyle = 1
+    WindowsStyle = 2
+
+
 class Shell(QsciScintillaCompat):
     """
     Class implementing a graphical Python shell.
@@ -90,10 +104,13 @@
     A user can enter commands that are executed in the remote
     Python interpreter.
     
-    @signal searchStringFound(found) emitted to indicate the search
-        result (boolean)
+    @signal searchStringFound(bool) emitted to indicate the search
+        result
+    @signal historyStyleChanged(int) emitted to indicate a change of
+        the history style
     """
     searchStringFound = pyqtSignal(bool)
+    historyStyleChanged = pyqtSignal(int)
     
     def __init__(self, dbs, vm, windowedVariant, parent=None):
         """
@@ -127,9 +144,9 @@
                 """<b>The Shell Window</b>"""
                 """<p>You can use the cursor keys while entering commands."""
                 """ There is also a history of commands that can be recalled"""
-                """ using the up and down cursor keys. Pressing the up or"""
-                """ down key after some text has been entered will start an"""
-                """ incremental search.</p>"""
+                """ using the up and down cursor keys while holding down the"""
+                """ Ctrl-key. Pressing these keys after some text has been"""
+                """ entered will start an incremental search.</p>"""
                 """<p>The shell has some special commands. 'reset' kills the"""
                 """ shell and starts a new one. 'clear' clears the display"""
                 """ of the shell window. 'start' is used to switch the shell"""
@@ -152,9 +169,9 @@
                 """ command while the program being debugged is running.</p>"""
                 """<p>You can use the cursor keys while entering commands."""
                 """ There is also a history of commands that can be recalled"""
-                """ using the up and down cursor keys. Pressing the up or"""
-                """ down key after some text has been entered will start an"""
-                """ incremental search.</p>"""
+                """ using the up and down cursor keys while holding down the"""
+                """ Ctrl-key. Pressing these keys after some text has been"""
+                """ entered will start an incremental search.</p>"""
                 """<p>The shell has some special commands. 'reset' kills the"""
                 """ shell and starts a new one. 'clear' clears the display"""
                 """ of the shell window. 'start' is used to switch the shell"""
@@ -204,10 +221,11 @@
         self.completionText = ""
         
         # Initialize history
-        self.historyLists = {}
-        self.maxHistoryEntries = Preferences.getShell("MaxHistoryEntries")
-        self.history = []
-        self.histidx = -1
+        self.__historyLists = {}
+        self.__maxHistoryEntries = Preferences.getShell("MaxHistoryEntries")
+        self.__historyStyle = Preferences.getShell("HistoryStyle")
+        self.__history = []
+        self.__setHistoryIndex()
         # remove obsolete shell histories (Python and Ruby)
         for clientType in ["Python", "Ruby"]:
             Preferences.Prefs.settings.remove("Shell/Histories/" + clientType)
@@ -253,7 +271,8 @@
             self.menu.addAction(self.tr('Cut'), self.cut)
             self.menu.addAction(self.tr('Copy'), self.copy)
             self.menu.addAction(self.tr('Paste'), self.paste)
-            self.menu.addMenu(self.hmenu)
+            self.menu.addMenu(self.hmenu).setEnabled(self.isHistoryEnabled())
+            
             self.menu.addSeparator()
             self.menu.addAction(self.tr('Find'), self.__find)
             self.menu.addSeparator()
@@ -312,6 +331,8 @@
             QsciScintilla.SCI_WORDRIGHTEXTEND: self.extendSelectionWordRight,
             QsciScintilla.SCI_VCHOMEEXTEND: self.__QScintillaVCHomeExtend,
             QsciScintilla.SCI_LINEENDEXTEND: self.extendSelectionToEOL,
+            
+            QsciScintilla.SCI_CANCEL: self.__QScintillaCancel,
         }
         
         self.grabGesture(Qt.PinchGesture)
@@ -338,7 +359,7 @@
         """
         Public method to shutdown the shell.
         """
-        for clientType in self.historyLists:
+        for clientType in self.__historyLists:
             self.saveHistory(clientType)
         
     def __bindLexer(self, language='Python3'):
@@ -549,21 +570,54 @@
         self.clientCapabilities = cap
         if clType != self.clientType:
             self.clientType = clType
-            self.__bindLexer(clType)
+            self.__bindLexer(self.clientType)
             self.__setTextDisplay()
             self.__setMargin0()
-            self.__setAutoCompletion(clType)
-            self.__setCallTips(clType)
+            self.__setAutoCompletion(self.clientType)
+            self.__setCallTips(self.clientType)
             self.racEnabled = \
                 Preferences.getShell("AutoCompletionEnabled") and \
                 (cap & HasCompleter) > 0
             
-            if clType not in self.historyLists:
+            if self.clientType not in self.__historyLists:
                 # load history list
-                self.loadHistory(clType)
-            self.history = self.historyLists[clType]
-            self.histidx = -1
+                self.loadHistory(self.clientType)
+            self.__history = self.__historyLists[self.clientType]
+            self.__setHistoryIndex()
+    
+    def __setHistoryIndex(self, index=None):
+        """
+        Private method to set the initial history index.
         
+        @param index index value to be set
+        @type int or None
+        """
+        if index is None:
+            # determine based on history style
+            if self.__historyStyle == ShellHistoryStyle.WindowsStyle:
+                idx = int(Preferences.Prefs.settings.value(
+                    "Shell/HistoryIndexes/" + self.clientType, -1))
+                self.__histidx = idx
+            else:
+                self.__histidx = -1
+        else:
+            self.__histidx = index
+            if self.__histidx >= len(self.__history):
+                self.__histidx = -1
+            if self.clientType and \
+                    self.__historyStyle == ShellHistoryStyle.WindowsStyle:
+                Preferences.Prefs.settings.setValue(
+                    "Shell/HistoryIndexes/" + self.clientType, self.__histidx)
+    
+    def __isHistoryIndexValid(self):
+        """
+        Private method to test, if the history index is valid.
+        
+        @return flag indicating validity
+        @rtype bool
+        """
+        return (0 <= self.__histidx < len(self.__history))
+    
     def loadHistory(self, clientType):
         """
         Public method to load the history for the given client type.
@@ -572,9 +626,9 @@
         """
         hl = Preferences.Prefs.settings.value("Shell/Histories/" + clientType)
         if hl is not None:
-            self.historyLists[clientType] = hl[-self.maxHistoryEntries:]
+            self.__historyLists[clientType] = hl[-self.__maxHistoryEntries:]
         else:
-            self.historyLists[clientType] = []
+            self.__historyLists[clientType] = []
         
     def reloadHistory(self):
         """
@@ -582,8 +636,8 @@
         type.
         """
         self.loadHistory(self.clientType)
-        self.history = self.historyLists[self.clientType]
-        self.histidx = -1
+        self.__history = self.__historyLists[self.clientType]
+        self.__setHistoryIndex()
         
     def saveHistory(self, clientType):
         """
@@ -591,9 +645,10 @@
         
         @param clientType type of the debug client (string)
         """
-        if clientType in self.historyLists:
+        if clientType in self.__historyLists:
             Preferences.Prefs.settings.setValue(
-                "Shell/Histories/" + clientType, self.historyLists[clientType])
+                "Shell/Histories/" + clientType,
+                self.__historyLists[clientType])
         
     def getHistory(self, clientType):
         """
@@ -604,9 +659,9 @@
         @return reference to the history list (list of strings)
         """
         if clientType is None:
-            return self.history
-        elif clientType in self.historyLists:
-            return self.historyLists[clientType]
+            return self.__history
+        elif clientType in self.__historyLists:
+            return self.__historyLists[clientType]
         else:
             return []
         
@@ -615,23 +670,26 @@
         Public slot to clear the current history.
         """
         if self.clientType:
-            self.historyLists[self.clientType] = []
-            self.history = self.historyLists[self.clientType]
+            self.__historyLists[self.clientType] = []
+            self.__history = self.__historyLists[self.clientType]
         else:
-            self.history = []
-        self.histidx = -1
+            self.__history = []
+        self.__setHistoryIndex(index=-1)
         
     def selectHistory(self):
         """
         Public slot to select a history entry to execute.
         """
+        current = self.__histidx
+        if current == -1:
+            current = len(self.__history) - 1
         cmd, ok = QInputDialog.getItem(
             self,
             self.tr("Select History"),
             self.tr("Select the history entry to execute"
                     " (most recent shown last)."),
-            self.history,
-            0, False)
+            self.__history,
+            current, False)
         if ok:
             self.__insertHistory(cmd)
         
@@ -640,11 +698,11 @@
         Public slot to show the shell history dialog.
         """
         from .ShellHistoryDialog import ShellHistoryDialog
-        dlg = ShellHistoryDialog(self.history, self.vm, self)
+        dlg = ShellHistoryDialog(self.__history, self.vm, self)
         if dlg.exec_() == QDialog.Accepted:
-            self.historyLists[self.clientType] = dlg.getHistory()
-            self.history = self.historyLists[self.clientType]
-            self.histidx = -1
+            self.__historyLists[self.clientType], idx = dlg.getHistory()
+            self.__history = self.__historyLists[self.clientType]
+            self.__setHistoryIndex(index=idx)
         
     def clearAllHistories(self):
         """
@@ -652,7 +710,7 @@
         """
         Preferences.Prefs.settings.beginGroup("Shell/Histories")
         for clientType in Preferences.Prefs.settings.childKeys():
-            self.historyLists[clientType] = []
+            self.__historyLists[clientType] = []
             self.saveHistory(clientType)
         Preferences.Prefs.settings.endGroup()
         
@@ -1343,62 +1401,78 @@
         
         @param cmd QScintilla command
         """
-        line, col = self.__getEndPos()
-        buf = self.text(line)
-        if buf.startswith(sys.ps1):
-            buf = buf.replace(sys.ps1, "")
-        if buf.startswith(sys.ps2):
-            buf = buf.replace(sys.ps2, "")
-        if buf and self.incrementalSearchActive:
-            if self.incrementalSearchString:
-                idx = self.__rsearchHistory(self.incrementalSearchString,
-                                            self.histidx)
-                if idx >= 0:
-                    self.histidx = idx
-                    self.__useHistory()
+        if self.isHistoryEnabled():
+            line, col = self.__getEndPos()
+            buf = self.text(line)
+            if buf.startswith(sys.ps1):
+                buf = buf.replace(sys.ps1, "")
+            if buf.startswith(sys.ps2):
+                buf = buf.replace(sys.ps2, "")
+            if buf and self.incrementalSearchActive:
+                if self.incrementalSearchString and \
+                   buf.startswith(self.incrementalSearchString):
+                    idx, found = self.__rsearchHistory(
+                        self.incrementalSearchString, self.__histidx)
+                    if found and idx >= 0:
+                        self.__setHistoryIndex(index=idx)
+                        self.__useHistory()
+                else:
+                    idx, found = self.__rsearchHistory(buf)
+                    if found and idx >= 0:
+                        self.__setHistoryIndex(index=idx)
+                        self.incrementalSearchString = buf
+                        self.__useHistory()
             else:
-                idx = self.__rsearchHistory(buf)
-                if idx >= 0:
-                    self.histidx = idx
-                    self.incrementalSearchString = buf
-                    self.__useHistory()
-        else:
-            if self.histidx < 0:
-                self.histidx = len(self.history)
-            if self.histidx > 0:
-                self.histidx = self.histidx - 1
+                if self.__histidx < 0:
+                    # wrap around
+                    self.__setHistoryIndex(index=len(self.__history) - 1)
+                else:
+                    self.__setHistoryIndex(index=self.__histidx - 1)
                 self.__useHistory()
-        
+    
     def __QScintillaHistoryDown(self, cmd):
         """
         Private method to handle the Ctrl+Down key.
         
         @param cmd QScintilla command
         """
-        line, col = self.__getEndPos()
-        buf = self.text(line)
-        if buf.startswith(sys.ps1):
-            buf = buf.replace(sys.ps1, "")
-        if buf.startswith(sys.ps2):
-            buf = buf.replace(sys.ps2, "")
-        if buf and self.incrementalSearchActive:
-            if self.incrementalSearchString:
-                idx = self.__searchHistory(
-                    self.incrementalSearchString, self.histidx)
-                if idx >= 0:
-                    self.histidx = idx
-                    self.__useHistory()
+        if self.isHistoryEnabled():
+            line, col = self.__getEndPos()
+            buf = self.text(line)
+            if buf.startswith(sys.ps1):
+                buf = buf.replace(sys.ps1, "")
+            if buf.startswith(sys.ps2):
+                buf = buf.replace(sys.ps2, "")
+            if buf and self.incrementalSearchActive:
+                if self.incrementalSearchString and \
+                   buf.startswith(self.incrementalSearchString):
+                    idx, found = self.__searchHistory(
+                        self.incrementalSearchString, self.__histidx)
+                    if found and idx >= 0:
+                        self.__setHistoryIndex(index=idx)
+                        self.__useHistory()
+                else:
+                    idx, found = self.__searchHistory(buf)
+                    if found and idx >= 0:
+                        self.__setHistoryIndex(index=idx)
+                        self.incrementalSearchString = buf
+                        self.__useHistory()
             else:
-                idx = self.__searchHistory(buf)
-                if idx >= 0:
-                    self.histidx = idx
-                    self.incrementalSearchString = buf
-                    self.__useHistory()
-        else:
-            if self.histidx >= 0 and self.histidx < len(self.history):
-                self.histidx += 1
+                if self.__histidx >= len(self.__history) - 1:
+                    # wrap around
+                    self.__setHistoryIndex(index=0)
+                else:
+                    self.__setHistoryIndex(index=self.__histidx + 1)
                 self.__useHistory()
-        
+    
+    def __QScintillaCancel(self):
+        """
+        Private method to handle the ESC command.
+        """
+        if self.incrementalSearchActive:
+            self.__resetIncrementalHistorySearch()
+        self.__insertHistory("")
+    
     def __QScintillaCharLeftExtend(self):
         """
         Private method to handle the Extend Selection Left command.
@@ -1447,12 +1521,18 @@
             self.inCommandExecution = True
             self.interruptCommandExecution = False
             if not cmd:
+                # make sure cmd is a string
                 cmd = ''
-            if len(self.history) == 0 or self.history[-1] != cmd:
-                if len(self.history) == self.maxHistoryEntries:
-                    del self.history[0]
-                self.history.append(cmd)
-            self.histidx = -1
+            # TODO: change according to history style
+            #       Linux - append (i.e. keep as is)
+            #       Windows - modify current entry if index not at end, add otherwise
+            if self.isHistoryEnabled():
+                if cmd != "" and (
+                        len(self.__history) == 0 or self.__history[-1] != cmd):
+                    if len(self.__history) == self.__maxHistoryEntries:
+                        del self.__history[0]
+                    self.__history.append(cmd)
+                self.__histidx = -1
             if cmd.startswith('start '):
                 if not self.passive:
                     cmdList = cmd.split(None, 1)
@@ -1522,12 +1602,11 @@
         """
         Private method to display a command from the history.
         """
-        if self.histidx < len(self.history):
-            cmd = self.history[self.histidx]
+        if self.__isHistoryIndexValid():
+            cmd = self.__history[self.__histidx]
         else:
             cmd = ""
-            self.incrementalSearchString = ""
-            self.incrementalSearchActive = False
+            self.__resetIncrementalHistorySearch()
         
         self.__insertHistory(cmd)
 
@@ -1542,40 +1621,58 @@
                           self.prline, self.lineLength(self.prline))
         self.removeSelectedText()
         self.__insertText(cmd)
-        
+    
+    def __resetIncrementalHistorySearch(self):
+        """
+        Private method to reset the incremental history search.
+        """
+        self.incrementalSearchString = ""
+        self.incrementalSearchActive = False
+    
     def __searchHistory(self, txt, startIdx=-1):
         """
         Private method used to search the history.
         
-        @param txt text to match at the beginning (string)
-        @param startIdx index to start search from (integer)
-        @return index of found entry (integer)
+        @param txt text to match at the beginning
+        @type str
+        @param startIdx index to start search from
+        @type int
+        @return tuple containing the index of found entry and a flag indicating
+            that something was found
+        @rtype tuple of (int, bool)
         """
         if startIdx == -1:
             idx = 0
         else:
             idx = startIdx + 1
-        while idx < len(self.history) and \
-                not self.history[idx].startswith(txt):
+        while idx < len(self.__history) and \
+                not self.__history[idx].startswith(txt):
             idx += 1
-        return idx
+        found = (idx < len(self.__history) and
+                 self.__history[idx].startswith(txt))
+        return idx, found
         
     def __rsearchHistory(self, txt, startIdx=-1):
         """
         Private method used to reverse search the history.
         
-        @param txt text to match at the beginning (string)
-        @param startIdx index to start search from (integer)
-        @return index of found entry (integer)
+        @param txt text to match at the beginning
+        @type str
+        @param startIdx index to start search from
+        @type int
+        @return tuple containing the index of found entry and a flag indicating
+            that something was found
+        @rtype tuple of (int, bool)
         """
         if startIdx == -1:
-            idx = len(self.history) - 1
+            idx = len(self.__history) - 1
         else:
             idx = startIdx - 1
         while idx >= 0 and \
-                not self.history[idx].startswith(txt):
+                not self.__history[idx].startswith(txt):
             idx -= 1
-        return idx
+        found = idx >= 0 and self.__history[idx].startswith(txt)
+        return idx, found
         
     def focusNextPrevChild(self, nextChild):
         """
@@ -1649,10 +1746,15 @@
         self.__setCallTips()
         
         # do the history related stuff
-        self.maxHistoryEntries = Preferences.getShell("MaxHistoryEntries")
-        for key in list(self.historyLists.keys()):
-            self.historyLists[key] = \
-                self.historyLists[key][-self.maxHistoryEntries:]
+        self.__maxHistoryEntries = Preferences.getShell("MaxHistoryEntries")
+        for key in list(self.__historyLists.keys()):
+            self.__historyLists[key] = \
+                self.__historyLists[key][-self.__maxHistoryEntries:]
+        self.__historyStyle = Preferences.getShell("HistoryStyle")
+        self.__setHistoryIndex()
+        self.historyStyleChanged.emit(self.__historyStyle)
+        if not self.__windowed:
+            self.hmenu.menuAction().setEnabled(self.isHistoryEnabled())
         
         # do stdout /stderr stuff
         showStdOutErr = Preferences.getShell("ShowStdOutErr")
@@ -1898,3 +2000,21 @@
             txt, False, caseSensitive, wholeWord, False,
             forward=False, line=line, index=index)
         self.searchStringFound.emit(ok)
+    
+    def historyStyle(self):
+        """
+        Public method to get the shell history style.
+        
+        @return shell history style
+        @rtype ShellHistoryStyle
+        """
+        return self.__historyStyle
+    
+    def isHistoryEnabled(self):
+        """
+        Public method to check, if the history is enabled.
+        
+        @return flag indicating if history is enabled
+        @rtype bool
+        """
+        return self.__historyStyle != ShellHistoryStyle.Disabled

eric ide

mercurial