diff -r 8bc870b7a8a6 -r e4f9552f7f93 QScintilla/Shell.py --- 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