RefactoringRope/CodeAssistServer.py

branch
server_client_variant
changeset 212
f05681349336
parent 209
c1dce8630555
child 217
874115c79ca7
--- a/RefactoringRope/CodeAssistServer.py	Fri Oct 06 18:49:50 2017 +0200
+++ b/RefactoringRope/CodeAssistServer.py	Sun Oct 08 17:54:29 2017 +0200
@@ -14,6 +14,7 @@
 from PyQt5.QtCore import pyqtSlot, QCoreApplication, QTimer
 
 from E5Gui.E5Application import e5App
+from E5Gui import E5MessageBox
 
 from .JsonServer import JsonServer
 
@@ -39,14 +40,20 @@
         
         self.__plugin = plugin
         self.__ui = parent
+        self.__vm = e5App().getObject("ViewManager")
         
         self.__editorLanguageMapping = {}
+        self.__clientConfigs = {}
+        self.__editors = {}
+        
+        self.__asyncCompletions = False
         
         # attributes to store the resuls of the client side
         self.__completions = None
         self.__calltips = None
         
         self.__methodMapping = {
+            "Config": self.__setConfig,
             "CompletionsResult": self.__processCompletionsResult,
             "CallTipsResult": self.__processCallTipsResult,
             
@@ -77,6 +84,76 @@
                     "Pygments|Python 3": "Python3",
                 })
     
+    def __getConfigs(self):
+        """
+        Private method to get the configurations of all connected clients.
+        """
+        for idString in self.connectionNames():
+            self.sendJson("getConfig", {}, idString=idString)
+    
+    def __setConfig(self, params):
+        """
+        Private method to set the rope client configuration data.
+        
+        @param params dictionary containing the configuration data
+        @type dict
+        """
+        idString = params["Id"]
+        ropeFolder = params["RopeFolderName"]
+        
+        self.__clientConfigs[idString] = ropeFolder
+    
+    def __ropeConfigFile(self, idString):
+        """
+        Private method to get the name of the rope configuration file.
+        
+        @param idString id for which to get the configuration file
+        @type str
+        @return name of the rope configuration file
+        @rtype str
+        """
+        configfile = None
+        if idString in self.__clientConfigs:
+            ropedir = self.__clientConfigs[idString]
+            if ropedir:
+                configfile = os.path.join(ropedir, "config.py")
+                if not os.path.exists(configfile):
+                    configfile = None
+        return configfile
+    
+    def __configChanged(self, idString):
+        """
+        Private slot called, when the rope config file has changed.
+        
+        @param idString id for which to get the configuration file
+        @type str
+        """
+        self.sendJson("configChanged", {}, idString=idString)
+    
+    def editConfig(self, idString):
+        """
+        Public slot to open the rope configuration file in an editor.
+        
+        @param idString id for which to get the configuration file
+        @type str
+        """
+        configfile = self.__ropeConfigFile(idString)
+        if configfile:
+            if os.path.exists(configfile):
+                from QScintilla.MiniEditor import MiniEditor
+                editor = MiniEditor(configfile)
+                editor.show()
+                editor.editorSaved.connect(
+                    lambda: self.__configChanged(idString))
+                self.__editors[idString] = editor
+                return
+        else:
+            E5MessageBox.critical(
+                self.__ui,
+                self.tr("Configure Rope"),
+                self.tr("""The Rope configuration file '{0}' does"""
+                        """ not exist.""").format(configfile))
+    
     def isSupportedLanguage(self, language):
         """
         Public method to check, if the given language is supported.
@@ -88,13 +165,17 @@
         """
         return language in self.__editorLanguageMapping
     
-    def getCompletions(self, editor):
+    def getCompletions(self, editor, context):
         """
         Public method to calculate the possible completions.
         
+        Note: This is the synchronous variant for eric6 before 17.11.
+        
         @param editor reference to the editor object, that called this method
         @type QScintilla.Editor
-        @return list of proposals
+        @param context flag indicating to autocomplete a context
+        @type bool
+        @return list of possible completions
         @rtype list of str
         """
         # reset the completions buffer
@@ -103,6 +184,35 @@
         language = editor.getLanguage()
         if language not in self.__editorLanguageMapping:
             return []
+        
+        self.requestCompletions(editor, context, "")
+        
+        # emulate the synchronous behaviour
+        timer = QTimer()
+        timer.setSingleShot(True)
+        timer.start(5000)           # 5s timeout
+        while self.__completions is None and timer.isActive():
+            QCoreApplication.processEvents()
+        
+        return [] if self.__completions is None else self.__completions
+    
+    def requestCompletions(self, editor, context, acText):
+        """
+        Public method to request a list of possible completions.
+        
+        Note: This is part of the asynchronous variant for eric6 17.11 and
+              later.
+        
+        @param editor reference to the editor object, that called this method
+        @type QScintilla.Editor
+        @param context flag indicating to autocomplete a context
+        @type bool
+        @param acText text to be completed
+        @type str
+        """
+        language = editor.getLanguage()
+        if language not in self.__editorLanguageMapping:
+            return
         idString = self.__editorLanguageMapping[language]
         
         filename = editor.getFileName()
@@ -117,16 +227,8 @@
             "Source": source,
             "Offset": offset,
             "MaxFixes": maxfixes,
+            "CompletionText": acText,
         }, idString=idString)
-        
-        # emulate the synchronous behaviour
-        timer = QTimer()
-        timer.setSingleShot(True)
-        timer.start(5000)           # 5s timeout
-        while self.__completions is None and timer.isActive():
-            QCoreApplication.processEvents()
-        
-        return [] if self.__completions is None else self.__completions
     
     def __processCompletionsResult(self, result):
         """
@@ -135,19 +237,30 @@
         @param result dictionary containing the result sent by the client
         @type dict
         """
-        if "Error" in result:
-            self.__completions = []
+        if self.__asyncCompletions:
+            # asynchronous variant for eric6 17.11 and later
+            if "Error" not in result:
+                editor = self.__vm.getOpenEditor(result["FileName"])
+                if editor is not None:
+                    editor.completionsListReady(result["Completions"],
+                                                result["CompletionText"])
         else:
-            self.__completions = result["Completions"]
+            # synchronous variant for eric6 before 17.11
+            if "Error" in result:
+                self.__completions = []
+            else:
+                self.__completions = result["Completions"]
     
-    def getCallTips(self, pos, editor):
+    def getCallTips(self, editor, pos, commas):
         """
         Public method to calculate calltips.
         
+        @param editor reference to the editor object, that called this method
+        @type QScintilla.Editor
         @param pos position in the text for the calltip
         @type int
-        @param editor reference to the editor object, that called this method
-        @type QScintilla.Editor
+        @param commas minimum number of commas contained in the calltip
+        @type int
         @return list of possible calltips
         @rtype list of str
         """
@@ -203,7 +316,7 @@
         @param oldSource source code before the change
         @type str
         """
-        editor = e5App().getObject("ViewManager").getOpenEditor(filename)
+        editor = self.__vm.getOpenEditor(filename)
         if editor is not None:
             language = editor.getLanguage()
             if language in self.__editorLanguageMapping:
@@ -274,10 +387,13 @@
         ok = False
         
         if interpreter:
+            configDir = os.path.join(Globals.getConfigDir(), "rope", idString)
+            if not os.path.exists(configDir):
+                os.makedirs(configDir)
+            
             client = os.path.join(os.path.dirname(__file__),
                                   "CodeAssistClient.py")
-            ok = self.startClient(interpreter, client,
-                                  [Globals.getConfigDir()],
+            ok = self.startClient(interpreter, client, [configDir],
                                   idString=idString)
             if not ok:
                 self.__ui.appendToStderr(self.tr(
@@ -320,7 +436,10 @@
         Public slot for new incoming connections from a client.
         """
         super(CodeAssistServer, self).handleNewConnection()
+        
         self.__updateEditorLanguageMapping()
+        
+        self.__getConfigs()
     
     def activate(self):
         """
@@ -338,4 +457,85 @@
         """
         Public method to shut down the code assist server.
         """
+        for idString in self.connectionNames():
+            self.sendJson("closeProject", {}, flush=True, idString=idString)
+        
         self.stopAllClients()
+    
+    #######################################################################
+    ## Methods below handle setting/unsetting the hook methods
+    #######################################################################
+    
+    def connectEditor(self, editor):
+        """
+        Public method to connect an editor.
+        
+        @param editor reference to the editor
+        @type QScintilla.Editor
+        """
+        if self.isSupportedLanguage(editor.getLanguage()):
+            if self.__plugin.getPreferences("CodeAssistEnabled") and \
+               editor.getCompletionListHook("rope") is None:
+                self.__setAutoCompletionHook(editor)
+            if self.__plugin.getPreferences("CodeAssistCalltipsEnabled") and \
+               editor.getCallTipHook("rope") is None:
+                self.__setCalltipsHook(editor)
+        else:
+            self.disconnectEditor(editor)
+    
+    def disconnectEditor(self, editor):
+        """
+        Public method to disconnect an editor.
+        
+        @param editor reference to the editor
+        @type QScintilla.Editor
+        """
+        if editor.getCompletionListHook("rope"):
+            self.__unsetAutoCompletionHook(editor)
+        if editor.getCallTipHook("rope"):
+            self.__unsetCalltipsHook(editor)
+    
+    def __setAutoCompletionHook(self, editor):
+        """
+        Private method to set the auto-completion hook.
+        
+        @param editor reference to the editor
+        @type QScintilla.Editor
+        """
+        try:
+            editor.addCompletionListHook("rope", self.requestCompletions,
+                                         async=True)
+            self.__asyncCompletions = True
+        except TypeError:
+            # backward compatibility for eric6 before 17.11
+            editor.addCompletionListHook("rope", self.getCompletions)
+            self.__asyncCompletions = False
+    
+    def __unsetAutoCompletionHook(self, editor):
+        """
+        Private method to unset the auto-completion hook.
+        
+        @param editor reference to the editor
+        @type QScintilla.Editor
+        """
+        editor.removeCompletionListHook("rope")
+    
+    def __setCalltipsHook(self, editor):
+        """
+        Private method to set the calltip hook.
+        
+        @param editor reference to the editor
+        @type QScintilla.Editor
+        """
+        editor.addCallTipHook("rope", self.getCallTips)
+    
+    def __unsetCalltipsHook(self, editor):
+        """
+        Private method to unset the calltip hook.
+        
+        @param editor reference to the editor
+        @type QScintilla.Editor
+        """
+        editor.removeCallTipHook("rope")
+    
+    # TODO: add method to edit the codeassist python2 and 3 config files

eric ide

mercurial