Implemented support for asynchroneous completion lists in the editor.

Mon, 02 Oct 2017 14:24:58 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 02 Oct 2017 14:24:58 +0200
changeset 5886
ba6d27371e25
parent 5884
7a6f30e7f79c
child 5887
9a6ce5faed7a

Implemented support for asynchroneous completion lists in the editor.

Preferences/ConfigurationPages/EditorAutocompletionPage.py file | annotate | diff | comparison | revisions
Preferences/ConfigurationPages/EditorAutocompletionPage.ui file | annotate | diff | comparison | revisions
Preferences/__init__.py file | annotate | diff | comparison | revisions
QScintilla/Editor.py file | annotate | diff | comparison | revisions
--- a/Preferences/ConfigurationPages/EditorAutocompletionPage.py	Wed Sep 27 18:00:37 2017 +0200
+++ b/Preferences/ConfigurationPages/EditorAutocompletionPage.py	Mon Oct 02 14:24:58 2017 +0200
@@ -15,6 +15,7 @@
 import Preferences
 
 
+# TODO: add entry for auto-completion timeout
 class EditorAutocompletionPage(ConfigurationPageBase,
                                Ui_EditorAutocompletionPage):
     """
@@ -33,12 +34,14 @@
             Preferences.getEditor("AutoCompletionEnabled"))
         self.acCaseSensitivityCheckBox.setChecked(
             Preferences.getEditor("AutoCompletionCaseSensitivity"))
+        self.acReversedCheckBox.setChecked(
+            Preferences.getEditor("AutoCompletionReversedList"))
         self.acReplaceWordCheckBox.setChecked(
             Preferences.getEditor("AutoCompletionReplaceWord"))
         self.acThresholdSlider.setValue(
             Preferences.getEditor("AutoCompletionThreshold"))
-        self.acScintillaCheckBox.setChecked(
-            Preferences.getEditor("AutoCompletionScintillaOnFail"))
+        self.acTimeoutSpinBox.setValue(
+            Preferences.getEditor("AutoCompletionTimeout"))
         
     def save(self):
         """
@@ -50,6 +53,10 @@
         Preferences.setEditor(
             "AutoCompletionCaseSensitivity",
             self.acCaseSensitivityCheckBox.isChecked())
+            
+        Preferences.setEditor(
+            "AutoCompletionReversedList",
+            self.acReversedCheckBox.isChecked())
         Preferences.setEditor(
             "AutoCompletionReplaceWord",
             self.acReplaceWordCheckBox.isChecked())
@@ -57,8 +64,8 @@
             "AutoCompletionThreshold",
             self.acThresholdSlider.value())
         Preferences.setEditor(
-            "AutoCompletionScintillaOnFail",
-            self.acScintillaCheckBox.isChecked())
+            "AutoCompletionTimeout",
+            self.acTimeoutSpinBox.value())
     
 
 def create(dlg):
--- a/Preferences/ConfigurationPages/EditorAutocompletionPage.ui	Wed Sep 27 18:00:37 2017 +0200
+++ b/Preferences/ConfigurationPages/EditorAutocompletionPage.ui	Mon Oct 02 14:24:58 2017 +0200
@@ -10,7 +10,7 @@
     <height>398</height>
    </rect>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout_2">
+  <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QLabel" name="headerLabel">
      <property name="text">
@@ -74,6 +74,16 @@
        </widget>
       </item>
       <item row="1" column="0" colspan="2">
+       <widget class="QCheckBox" name="acReversedCheckBox">
+        <property name="toolTip">
+         <string>Select to show the list of completions in reversed order</string>
+        </property>
+        <property name="text">
+         <string>Show completions reversed</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0" colspan="2">
        <layout class="QHBoxLayout" name="_2">
         <item>
          <widget class="QLabel" name="textLabel1_2">
@@ -119,27 +129,54 @@
         </item>
        </layout>
       </item>
-     </layout>
-    </widget>
-   </item>
-   <item>
-    <widget class="QGroupBox" name="groupBox_2">
-     <property name="title">
-      <string>Plug-In Behavior</string>
-     </property>
-     <layout class="QVBoxLayout" name="verticalLayout">
-      <item>
-       <widget class="QCheckBox" name="acScintillaCheckBox">
-        <property name="toolTip">
-         <string>Select to show QScintilla provided completions, if the selected plug-ins fail</string>
-        </property>
-        <property name="whatsThis">
-         <string>Qscintilla provided completions are shown, if this option is enabled and completions shall be provided by plug-ins (see completions sub-page of the plug-in) and the plugin-ins don't deliver any completions.</string>
-        </property>
-        <property name="text">
-         <string>Show QScintilla completions, if plug-ins fail</string>
-        </property>
-       </widget>
+      <item row="3" column="0" colspan="2">
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <item>
+         <widget class="QLabel" name="label">
+          <property name="text">
+           <string>Time to wait until completion:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QSpinBox" name="acTimeoutSpinBox">
+          <property name="toolTip">
+           <string>Enter the time in milliseconds after which a list with completion proposals shall be shown</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+          </property>
+          <property name="correctionMode">
+           <enum>QAbstractSpinBox::CorrectToNearestValue</enum>
+          </property>
+          <property name="suffix">
+           <string> ms</string>
+          </property>
+          <property name="minimum">
+           <number>0</number>
+          </property>
+          <property name="maximum">
+           <number>999</number>
+          </property>
+          <property name="singleStep">
+           <number>10</number>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
       </item>
      </layout>
     </widget>
@@ -163,8 +200,9 @@
   <tabstop>acEnabledCheckBox</tabstop>
   <tabstop>acCaseSensitivityCheckBox</tabstop>
   <tabstop>acReplaceWordCheckBox</tabstop>
+  <tabstop>acReversedCheckBox</tabstop>
   <tabstop>acThresholdSlider</tabstop>
-  <tabstop>acScintillaCheckBox</tabstop>
+  <tabstop>acTimeoutSpinBox</tabstop>
  </tabstops>
  <resources/>
  <connections>
--- a/Preferences/__init__.py	Wed Sep 27 18:00:37 2017 +0200
+++ b/Preferences/__init__.py	Mon Oct 02 14:24:58 2017 +0200
@@ -416,9 +416,10 @@
         "AutoCompletionShowSingle": False,
         "AutoCompletionSource": QsciScintilla.AcsDocument,
         "AutoCompletionThreshold": 2,
+        "AutoCompletionTimeout": 200,
+        # timeout in ms before auto-completion is started
         "AutoCompletionFillups": False,
-        "AutoCompletionScintillaOnFail": False,
-        # show QScintilla completions, if plug-in fails
+        "AutoCompletionReversedList": False,
         
         "CallTipsEnabled": False,
         "CallTipsVisible": 0,
@@ -2063,8 +2064,8 @@
     elif key in ["AutosaveInterval", "TabWidth", "IndentWidth",
                  "FoldingStyle", "WarnFilesize", "EdgeMode", "EdgeColumn",
                  "CaretWidth", "AutoCompletionSource",
-                 "AutoCompletionThreshold", "CallTipsVisible",
-                 "CallTipsStyle", "MarkOccurrencesTimeout",
+                 "AutoCompletionThreshold", "AutoCompletionTimeout",
+                 "CallTipsVisible", "CallTipsStyle", "MarkOccurrencesTimeout",
                  "AutoSpellCheckChunkSize", "SpellCheckingMinWordSize",
                  "PostScriptLevel", "EOLMode", "ZoomFactor", "WhitespaceSize",
                  "OnlineSyntaxCheckInterval", "OnlineChangeTraceInterval",
@@ -2087,6 +2088,10 @@
         elif value in ["false", "False"]:
             value = 0
         return QsciLexerPython.IndentationWarning(int(value))
+    elif key == "AutoCompletionScintillaOnFail":
+        # This is obsolete, return default value for backward compatibility
+        # with old plug-ins.
+        return False
     else:
         return toBool(prefClass.settings.value(
             "Editor/" + key, prefClass.editorDefaults[key]))
--- a/QScintilla/Editor.py	Wed Sep 27 18:00:37 2017 +0200
+++ b/QScintilla/Editor.py	Mon Oct 02 14:24:58 2017 +0200
@@ -380,10 +380,21 @@
         # 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.__acTimer = QTimer(self)
+        self.__acTimer.setSingleShot(True)
+        self.__acTimer.setInterval(
+            Preferences.getEditor("AutoCompletionTimeout"))
+        self.__acTimer.timeout.connect(self.__autoComplete)
+        
         self.__completionListHookFunctions = {}
+        self.__completionListAsyncHookFunctions = {}
         self.__setAutoCompletion()
         
+        # set the call-tips function
         self.__ctHookFunctions = {}
         self.__setCallTips()
         
@@ -4521,7 +4532,8 @@
     ## auto-completion hook interfaces
     #################################################################
     
-    def addCompletionListHook(self, key, func):
+    # TODO: add support for asynchroneous auto-completion lists
+    def addCompletionListHook(self, key, func, async=False):
         """
         Public method to set an auto-completion list provider.
         
@@ -4531,9 +4543,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,11 +4560,16 @@
                 .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
     
     def removeCompletionListHook(self, key):
         """
@@ -4559,8 +4581,11 @@
         """
         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(
@@ -4575,45 +4600,81 @@
         @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 Preferences.getEditor("AutoCompletionTimeout"):
+                self.__acTimer.stop()
+                self.__acContext = context
+                self.__acTimer.start()
             else:
-                completionsList.sort()
-                self.showUserList(EditorAutoCompletionListID,
-                                  completionsList)
+                self.__autoComplete(context)
         elif not auto:
             self.autoCompleteQScintilla()
         elif self.autoCompletionSource() != QsciScintilla.AcsNone:
             self.autoCompleteQScintilla()
     
+    def __autoComplete(self, context=None):
+        """
+        Private method to start auto-completion via plug-ins.
+        
+        @keyparam context flag indicating to complete a context
+        @type bool or None
+        """
+        # TODO: add a cache for recent completions
+        if context is None:
+            context = self.__acContext
+        
+        line, col = self.getCursorPosition()
+        self.__acText = self.getWordLeft(line, col)
+        self.__acCompletions.clear()
+        
+        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)
+    
+    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
+        @param acText text to be completed
+        @type str
+        """
+        if acText == self.__acText:
+            # process the list only, if not already obsolete
+            if self.isListActive():
+                self.cancelList()
+            
+            self.__acCompletions.update(set(completions))
+            if self.__acCompletions:
+                self.showUserList(
+                    EditorAutoCompletionListID,
+                    sorted(list(self.__acCompletions),
+                           reverse=Preferences.getEditor(
+                        "AutoCompletionReversedList")))
+    
     def __completionListSelected(self, listId, txt):
         """
         Private slot to handle the selection from the completion list.

eric ide

mercurial