QScintilla/Editor.py

branch
maintenance
changeset 5948
6f958d5765f4
parent 5885
5228afbb870f
child 5949
22657f04f332
--- 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)
         

eric ide

mercurial