--- a/QScintilla/Editor.py Wed Nov 01 19:22:02 2017 +0100 +++ b/QScintilla/Editor.py Fri Nov 03 12:10:16 2017 +0100 @@ -27,6 +27,7 @@ from E5Gui.E5Application import e5App from E5Gui import E5FileDialog, E5MessageBox +from E5Utilities.E5Cache import E5Cache from .QsciScintillaCompat import QsciScintillaCompat, QSCINTILLA_VERSION from .EditorMarkerMap import EditorMarkerMap @@ -126,6 +127,8 @@ AttributeProtectedID = 8 AttributePrivateID = 9 EnumID = 10 + KeywordsID = 11 + ModuleID = 12 FromDocumentID = 99 @@ -258,6 +261,7 @@ self.cursorPositionChanged.connect(self.__cursorPositionChanged) self.modificationAttempted.connect(self.__modificationReadOnly) self.userListActivated.connect(self.__completionListSelected) + self.SCN_CHARADDED.connect(self.__charAddedPermanent) # margins layout if QSCINTILLA_VERSION() >= 0x020301: @@ -380,10 +384,32 @@ # set the text display again self.__setTextDisplay() - # set the autocompletion and calltips function + # set the auto-completion function + self.__acContext = True + self.__acText = "" + self.__acCompletions = set() + self.__acCompletionsFinished = 0 + self.__acCache = E5Cache( + size=Preferences.getEditor("AutoCompletionCacheSize")) + self.__acCache.setMaximumCacheTime( + Preferences.getEditor("AutoCompletionCacheTime")) + self.__acTimer = QTimer(self) + self.__acTimer.setSingleShot(True) + self.__acTimer.setInterval( + Preferences.getEditor("AutoCompletionTimeout")) + self.__acTimer.timeout.connect(self.__autoComplete) + + self.__acWatchdog = QTimer(self) + self.__acWatchdog.setSingleShot(True) + self.__acWatchdog.setInterval( + Preferences.getEditor("AutoCompletionWatchdogTime")) + self.__acWatchdog.timeout.connect(self.autoCompleteQScintilla) + self.__completionListHookFunctions = {} + self.__completionListAsyncHookFunctions = {} self.__setAutoCompletion() + # set the call-tips function self.__ctHookFunctions = {} self.__setCallTips() @@ -506,6 +532,10 @@ UI.PixmapCache.getPixmap("attribute_private.png")) self.registerImage(self.EnumID, UI.PixmapCache.getPixmap("enum.png")) + self.registerImage(self.KeywordsID, + UI.PixmapCache.getPixmap("keywords.png")) + self.registerImage(self.ModuleID, + UI.PixmapCache.getPixmap("module.png")) self.registerImage(self.FromDocumentID, UI.PixmapCache.getPixmap("editor.png")) @@ -749,6 +779,8 @@ self.menu.addMenu(self.autocompletionMenu) self.menuActs["calltip"] = self.menu.addAction( self.tr('Calltip'), self.callTip) + self.menuActs["codeInfo"] = self.menu.addAction( + self.tr('Code Info'), self.__showCodeInfo) self.menu.addSeparator() if self.isResourcesFile: self.menu.addMenu(self.resourcesMenu) @@ -820,6 +852,9 @@ self.menuActs["acDynamic"] = menu.addAction( self.tr('Complete'), self.autoComplete) menu.addSeparator() + self.menuActs["acClearCache"] = menu.addAction( + self.tr("Clear Completions Cache"), self.__clearCompletionsCache) + menu.addSeparator() menu.addAction( self.tr('Complete from Document'), self.autoCompleteFromDocument) self.menuActs["acAPI"] = menu.addAction( @@ -4032,8 +4067,17 @@ # set margin 0 and 2 configuration self.__setMarginsDisplay() - # set the autocompletion and calltips function + # set the auto-completion function + self.__acCache.setSize( + Preferences.getEditor("AutoCompletionCacheSize")) + self.__acCache.setMaximumCacheTime( + Preferences.getEditor("AutoCompletionCacheTime")) + acTimeout = Preferences.getEditor("AutoCompletionTimeout") + if acTimeout != self.__acTimer.interval: + self.__acTimer.setInterval(acTimeout) self.__setAutoCompletion() + + # set the calltips function self.__setCallTips() # set the autosave flags @@ -4367,7 +4411,8 @@ else: self.setAutoCompletionSource(QsciScintilla.AcsAll) if Preferences.getEditor("AutoCompletionEnabled"): - if not self.__completionListHookFunctions: + if not self.__completionListHookFunctions and \ + not self.__completionListAsyncHookFunctions: self.setAutoCompletionThreshold( Preferences.getEditor("AutoCompletionThreshold")) else: @@ -4418,6 +4463,11 @@ """ Public method to perform an autocompletion using QScintilla methods. """ + self.__acText = ' ' # Prevent long running ACs to add results + self.__acWatchdog.stop() + if self.__acCompletions: + return + acs = Preferences.getEditor("AutoCompletionSource") if acs == QsciScintilla.AcsDocument: self.autoCompleteFromDocument() @@ -4521,7 +4571,8 @@ ## auto-completion hook interfaces ################################################################# - def addCompletionListHook(self, key, func): + # TODO: document this hook in chapter 6.2 of plug-in document + def addCompletionListHook(self, key, func, async=False): """ Public method to set an auto-completion list provider. @@ -4531,9 +4582,14 @@ should be a function taking a reference to the editor and a boolean indicating to complete a context. It should return the possible completions as a list of strings. - @type function(editor, bool) -> list of str - """ - if key in self.__completionListHookFunctions: + @type function(editor, bool) -> list of str in case async is False + and function(editor, bool, str) returning nothing in case async + is True + @param async flag indicating an asynchroneous function + @type bool + """ + if key in self.__completionListHookFunctions or \ + key in self.__completionListAsyncHookFunctions: # it was already registered E5MessageBox.warning( self, @@ -4543,12 +4599,18 @@ .format(key)) return - if not self.__completionListHookFunctions: + if not self.__completionListHookFunctions and \ + not self.__completionListAsyncHookFunctions: if self.autoCompletionThreshold() > 0: self.setAutoCompletionThreshold(0) self.SCN_CHARADDED.connect(self.__charAdded) - self.__completionListHookFunctions[key] = func - + + if async: + self.__completionListAsyncHookFunctions[key] = func + else: + self.__completionListHookFunctions[key] = func + + # TODO: document this hook in chapter 6.2 of plug-in document def removeCompletionListHook(self, key): """ Public method to remove a previously registered completion list @@ -4559,13 +4621,17 @@ """ if key in self.__completionListHookFunctions: del self.__completionListHookFunctions[key] - - if not self.__completionListHookFunctions: + elif key in self.__completionListAsyncHookFunctions: + del self.__completionListAsyncHookFunctions[key] + + if not self.__completionListHookFunctions and \ + not self.__completionListAsyncHookFunctions: self.SCN_CHARADDED.disconnect(self.__charAdded) if self.autoCompletionThreshold() == 0: self.setAutoCompletionThreshold( Preferences.getEditor("AutoCompletionThreshold")) + # TODO: document this hook in chapter 6.2 of plug-in document def getCompletionListHook(self, key): """ Public method to get the registered completion list provider. @@ -4575,45 +4641,154 @@ @return function providing completion list @rtype function or None """ - if key in self.__completionListHookFunctions: - return self.__completionListHookFunctions[key] - else: - return None + return (self.__completionListHookFunctions.get(key) or + self.__completionListAsyncHookFunctions.get(key)) def autoComplete(self, auto=False, context=True): """ - Public method to start autocompletion. + Public method to start auto-completion. @keyparam auto flag indicating a call from the __charAdded method (boolean) @keyparam context flag indicating to complete a context (boolean) """ if auto and self.autoCompletionThreshold() == -1: - # autocompletion is disabled + # auto-completion is disabled return - if self.__completionListHookFunctions: - if self.isListActive(): - self.cancelList() - completionsList = [] - for key in self.__completionListHookFunctions: - completionsList.extend( - self.__completionListHookFunctions[key](self, context)) - completionsList = list(set(completionsList)) - if len(completionsList) == 0: - if Preferences.getEditor("AutoCompletionScintillaOnFail") and \ - (self.autoCompletionSource() != QsciScintilla.AcsNone or - not auto): - self.autoCompleteQScintilla() + if self.__completionListHookFunctions or \ + self.__completionListAsyncHookFunctions: + if auto and Preferences.getEditor("AutoCompletionTimeout"): + self.__acTimer.stop() + self.__acContext = context + self.__acTimer.start() else: - completionsList.sort() - self.showUserList(EditorAutoCompletionListID, - completionsList) + self.__autoComplete(auto, context) elif not auto: self.autoCompleteQScintilla() elif self.autoCompletionSource() != QsciScintilla.AcsNone: self.autoCompleteQScintilla() + def __autoComplete(self, auto=True, context=None): + """ + Private method to start auto-completion via plug-ins. + + @keyparam auto flag indicating a call from the __charAdded method + (boolean) + @keyparam context flag indicating to complete a context + @type bool or None + """ + line, col = self.getCursorPosition() + text = self.text(line) + if self.__isStartChar(text[col - 1]): + self.__acText = self.getWordLeft(line, col - 1) + text[col - 1] + else: + self.__acText = self.getWordLeft(line, col) + self.__acCompletions.clear() + self.__acCompletionsFinished = 0 + + # Suppress empty completions + if auto and self.__acText == '': + return + + completions = self.__acCache.get(self.__acText) + if completions is not None: + # show list with cached entries + if self.isListActive(): + self.cancelList() + + self.__showCompletionsList(completions) + else: + if context is None: + context = self.__acContext + + for key in self.__completionListAsyncHookFunctions: + self.__completionListAsyncHookFunctions[key]( + self, context, self.__acText) + + for key in self.__completionListHookFunctions: + completions = self.__completionListHookFunctions[key]( + self, context) + self.completionsListReady(completions, self.__acText) + + if Preferences.getEditor("AutoCompletionScintillaOnFail"): + self.__acWatchdog.start() + + # TODO: document this hook in chapter 6.2 of plug-in document + def completionsListReady(self, completions, acText): + """ + Public method to show the completions determined by a completions + provider. + + @param completions list of possible completions + @type list of str or set of str + @param acText text to be completed + @type str + """ + # process the list only, if not already obsolete ... + if acText != self.__acText: + return + + self.__acCompletions.update(set(completions)) + + self.__acCompletionsFinished += 1 + # Got all results from auto completer? + if self.__acCompletionsFinished >= len( + self.__completionListAsyncHookFunctions): + self.__acWatchdog.stop() + + # Autocomplete with QScintilla if no results present + if (Preferences.getEditor("AutoCompletionScintillaOnFail") + and not self.__acCompletions): + self.autoCompleteQScintilla() + return + + # ... or completions are not empty + if not bool(completions): + return + + if self.isListActive(): + self.cancelList() + + if self.__acCompletions: + self.__acCache.add(acText, set(self.__acCompletions)) + self.__showCompletionsList(self.__acCompletions) + + def __showCompletionsList(self, completions): + """ + Private method to show the completions list. + + @param completions completions to be shown + @type list of str or set of str + """ + if Preferences.getEditor("AutoCompletionReversedList"): + acCompletions = sorted( + list(completions), + key=self.__replaceLeadingUnderscores) + else: + acCompletions = sorted(list(completions)) + self.showUserList(EditorAutoCompletionListID, acCompletions) + + def __replaceLeadingUnderscores(self, txt): + """ + Private method to replace the first two underlines for invers sorting. + + @param txt completion text + @type str + @return modified completion text + @rtype str + """ + if txt.startswith('_'): + return txt[:2].replace('_', '~') + txt[2:] + else: + return txt + + def __clearCompletionsCache(self): + """ + Private method to clear the auto-completions cache. + """ + self.__acCache.clear() + def __completionListSelected(self, listId, txt): """ Private slot to handle the selection from the completion list. @@ -4652,7 +4827,73 @@ (boolean) """ return (self.acAPI or - bool(self.__completionListHookFunctions)) + bool(self.__completionListHookFunctions) or + bool(self.__completionListAsyncHookFunctions)) + + ################################################################# + ## call-tip hook interfaces + ################################################################# + + # TODO: document this hook in chapter 6.2 of plug-in document + def addCallTipHook(self, key, func): + """ + Public method to set a calltip provider. + + @param key name of the provider + @type str + @param func function providing calltips. func + should be a function taking a reference to the editor, + a position into the text and the amount of commas to the + left of the cursor. It should return the possible + calltips as a list of strings. + @type function(editor, int, int) -> list of str + """ + if key in self.__ctHookFunctions: + # it was already registered + E5MessageBox.warning( + self, + self.tr("Call-Tips Provider"), + self.tr("""The call-tips provider '{0}' was already""" + """ registered. Ignoring duplicate request.""") + .format(key)) + return + + self.__ctHookFunctions[key] = func + + # TODO: document this hook in chapter 6.2 of plug-in document + def removeCallTipHook(self, key): + """ + Public method to remove a previously registered calltip provider. + + @param key name of the provider + @type str + """ + if key in self.__ctHookFunctions: + del self.__ctHookFunctions[key] + + # TODO: document this hook in chapter 6.2 of plug-in document + def getCallTipHook(self, key): + """ + Public method to get the registered calltip provider. + + @param key name of the provider + @type str + @return function providing calltips + @rtype function or None + """ + if key in self.__ctHookFunctions: + return self.__ctHookFunctions[key] + else: + return None + + def canProvideCallTipps(self): + """ + Public method to test the calltips availability. + + @return flag indicating the availability of calltips (boolean) + """ + return (self.acAPI or + bool(self.__ctHookFunctions)) def callTip(self): """ @@ -4784,66 +5025,28 @@ return ct ################################################################# - ## call-tip hook interfaces + ## Methods needed by the code documentation viewer ################################################################# - def addCallTipHook(self, key, func): - """ - Public method to set a calltip provider. - - @param key name of the provider - @type str - @param func function providing calltips. func - should be a function taking a reference to the editor, - a position into the text and the amount of commas to the - left of the cursor. It should return the possible - calltips as a list of strings. - @type function(editor, int, int) -> list of str - """ - if key in self.__ctHookFunctions: - # it was already registered - E5MessageBox.warning( - self, - self.tr("Call-Tips Provider"), - self.tr("""The call-tips provider '{0}' was already""" - """ registered. Ignoring duplicate request.""") - .format(key)) - return - - self.__ctHookFunctions[key] = func - - def removeCallTipHook(self, key): - """ - Public method to remove a previously registered calltip provider. - - @param key name of the provider - @type str - """ - if key in self.__ctHookFunctions: - del self.__ctHookFunctions[key] - - def getCallTipHook(self, key): - """ - Public method to get the registered calltip provider. - - @param key name of the provider - @type str - @return function providing calltips - @rtype function or None - """ - if key in self.__ctHookFunctions: - return self.__ctHookFunctions[key] - else: - return None - - def canProvideCallTipps(self): - """ - Public method to test the calltips availability. - - @return flag indicating the availability of calltips (boolean) - """ - return (self.acAPI or - bool(self.__ctHookFunctions)) + def __charAddedPermanent(self, charNumber): + """ + Private slot called to handle the user entering a character. + + Note: This slot is always connected independent of the auto-completion + and calltips handling __charAdded() slot. + + @param charNumber value of the character entered (integer) + """ + char = chr(charNumber) + if char == "(" and \ + Preferences.getDocuViewer("ShowInfoOnOpenParenthesis"): + self.vm.showEditorInfo(self) + + def __showCodeInfo(self): + """ + Private slot to handle the context menu action to show code info. + """ + self.vm.showEditorInfo(self) ################################################################# ## Methods needed by the context menu @@ -4936,7 +5139,9 @@ self.completer is not None and self.completer.isEnabled()) if not self.isResourcesFile: - self.menuActs["calltip"].setEnabled(self.acAPI) + self.menuActs["calltip"].setEnabled(self.canProvideCallTipps()) + self.menuActs["codeInfo"].setEnabled( + self.vm.isEditorInfoSupported(self.getLanguage())) from .SpellChecker import SpellChecker spellingAvailable = SpellChecker.isAvailable() @@ -4973,9 +5178,10 @@ """ self.menuActs["acDynamic"].setEnabled( self.canProvideDynamicAutoCompletion()) + self.menuActs["acClearCache"].setEnabled( + self.canProvideDynamicAutoCompletion()) self.menuActs["acAPI"].setEnabled(self.acAPI) self.menuActs["acAPIDocument"].setEnabled(self.acAPI) - self.menuActs["calltip"].setEnabled(self.canProvideCallTipps()) self.showMenu.emit("Autocompletion", self.autocompletionMenu, self)