src/eric7/JediInterface/JediServer.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9069
938039ea15ca
child 9221
bf71ee032bb4
diff -r 3fc8dfeb6ebe -r b99e7fd55fd3 src/eric7/JediInterface/JediServer.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/JediInterface/JediServer.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,929 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the autocompletion interface to jedi.
+"""
+
+import contextlib
+import os
+import uuid
+
+from PyQt6.QtCore import pyqtSlot, QCoreApplication, QTimer, QThread
+from PyQt6.QtWidgets import QInputDialog, QLineEdit, QDialog
+
+from EricWidgets.EricApplication import ericApp
+from EricWidgets import EricMessageBox
+
+from EricNetwork.EricJsonServer import EricJsonServer
+
+from QScintilla.Editor import Editor
+
+import Preferences
+import Globals
+
+from .RefactoringPreviewDialog import RefactoringPreviewDialog
+
+
+class JediServer(EricJsonServer):
+    """
+    Class implementing the interface to the jedi library.
+    """
+    IdProject = "Project"
+    
+    PictureIDs = {
+        "class": "?{0}".format(Editor.ClassID),
+        "_class": "?{0}".format(Editor.ClassProtectedID),
+        "__class": "?{0}".format(Editor.ClassPrivateID),
+        "instance": "?{0}".format(Editor.ClassID),
+        "_instance": "?{0}".format(Editor.ClassProtectedID),
+        "__instance": "?{0}".format(Editor.ClassPrivateID),
+        "function": "?{0}".format(Editor.MethodID),
+        "_function": "?{0}".format(Editor.MethodProtectedID),
+        "__function": "?{0}".format(Editor.MethodPrivateID),
+        "module": "?{0}".format(Editor.ModuleID),
+        "_module": "?{0}".format(Editor.ModuleID),
+        "__module": "?{0}".format(Editor.ModuleID),
+        "param": "?{0}".format(Editor.AttributeID),
+        "_param": "?{0}".format(Editor.AttributeProtectedID),
+        "__param": "?{0}".format(Editor.AttributePrivateID),
+        "statement": "?{0}".format(Editor.AttributeID),
+        "_statement": "?{0}".format(Editor.AttributeProtectedID),
+        "__statement": "?{0}".format(Editor.AttributePrivateID),
+        "import": "",
+        "None": "",
+    }
+    
+    def __init__(self, viewManager, project, ui):
+        """
+        Constructor
+        
+        @param viewManager reference to the viewmanager object
+        @type ViewManager
+        @param project reference to the project object
+        @type Project
+        @param ui reference to the user interface
+        @type UserInterface
+        """
+        super().__init__(
+            "JediServer", multiplex=True, parent=ui)
+        
+        self.__ui = ui
+        self.__vm = viewManager
+        self.__ericProject = project
+        
+        self.__editorLanguageMapping = {}
+        
+        self.__documentationViewer = None
+        
+        # attributes to store the resuls of the client side
+        self.__completions = None
+        self.__calltips = None
+        
+        self.__methodMapping = {
+            "CompletionsResult": self.__processCompletionsResult,
+            "CallTipsResult": self.__processCallTipsResult,
+            "DocumentationResult": self.__processDocumentationResult,
+            "HoverHelpResult": self.__processHoverHelpResult,
+            "GotoDefinitionResult": self.__processGotoDefinitionResult,
+            "GotoReferencesResult": self.__processGotoReferencesResult,
+            
+            "RefactoringDiff": self.__showRefactoringDiff,
+            "RefactoringApplyResult": self.__checkRefactoringResult,
+            
+            "ClientException": self.__processClientException,
+        }
+        
+        # temporary store for editor references indexed by Uuid
+        self.__editors = {}
+        
+        # Python 3
+        self.__ensureActive("Python3")
+    
+    def __updateEditorLanguageMapping(self):
+        """
+        Private method to update the editor language to connection mapping.
+        """
+        self.__editorLanguageMapping = {}
+        for name in self.connectionNames():
+            if name == "Python3":
+                self.__editorLanguageMapping.update({
+                    "Python3": "Python3",
+                    "MicroPython": "Python3",
+                    "Pygments|Python": "Python3",
+                    "Pygments|Python 2.x": "Python3",
+                    "Cython": "Python3",
+                })
+    
+    def isSupportedLanguage(self, language):
+        """
+        Public method to check, if the given language is supported.
+        
+        @param language editor programming language to check
+        @type str
+        @return flag indicating the support status
+        @rtype bool
+        """
+        return language in self.__editorLanguageMapping
+    
+    def __idString(self, editor):
+        """
+        Private method to determine the ID string for the back-end.
+        
+        @param editor reference to the editor to determine the ID string for
+        @type Editor
+        @return ID string
+        @rtype str
+        """
+        idString = ""
+        
+        language = editor.getLanguage()
+        if (
+            self.__ericProject.isOpen() and
+            self.__ericProject.getProjectLanguage() == language
+        ):
+            filename = editor.getFileName()
+            if self.__ericProject.isProjectSource(filename):
+                idString = JediServer.IdProject
+        
+        if not idString and language in self.__editorLanguageMapping:
+            idString = self.__editorLanguageMapping[language]
+        
+        return idString
+    
+    def __prepareData(self, editor):
+        """
+        Private method to gather data about current cursor position.
+        
+        @param editor reference to the editor object, that called this method
+        @type Editor
+        @return tuple of filename, line, index, source
+        @rtype tuple (str, int, int, str)
+        """
+        filename = editor.getFileName()
+        line, index = editor.getCursorPosition()
+        line += 1       # jedi line numbers are 1 based
+        source = editor.text()
+        return filename, line, index, source
+    
+    def requestCompletions(self, editor, context, acText):
+        """
+        Public method to request a list of possible completions.
+        
+        @param editor reference to the editor object, that called this method
+        @type Editor
+        @param context flag indicating to autocomplete a context
+        @type bool
+        @param acText text to be completed
+        @type str
+        """
+        if not Preferences.getJedi("JediCompletionsEnabled"):
+            return
+        
+        idString = self.__idString(editor)
+        if not idString:
+            return
+        
+        filename, line, index, source = self.__prepareData(editor)
+        fuzzy = Preferences.getJedi("JediFuzzyCompletionsEnabled")
+        
+        self.__ensureActive(idString)
+        
+        self.sendJson("getCompletions", {
+            "FileName": filename,
+            "Source": source,
+            "Line": line,
+            "Index": index,
+            "Fuzzy": fuzzy,
+            "CompletionText": acText,
+        }, idString=idString)
+    
+    def __processCompletionsResult(self, result):
+        """
+        Private method to process the completions sent by the client.
+        
+        @param result dictionary containing the result sent by the client
+        @type dict
+        """
+        names = []
+        for completion in result["Completions"]:
+            name = completion['Name']
+            context = completion['FullName']
+            if context:
+                if context.endswith(".{0}".format(name)):
+                    context = context.rsplit(".", 1)[0]
+                name = "{0} ({1})".format(name, context)
+            
+            name += JediServer.PictureIDs.get(completion['CompletionType'], '')
+            names.append(name)
+        
+        if "Error" not in result:
+            editor = self.__vm.getOpenEditor(result["FileName"])
+            if editor is not None:
+                editor.completionsListReady(names,
+                                            result["CompletionText"])
+    
+    def getCallTips(self, editor, pos, commas):
+        """
+        Public method to calculate calltips.
+        
+        @param editor reference to the editor object, that called this method
+        @type Editor
+        @param pos position in the text for the calltip
+        @type int
+        @param commas minimum number of commas contained in the calltip
+        @type int
+        @return list of possible calltips
+        @rtype list of str
+        """
+        if not Preferences.getJedi("JediCalltipsEnabled"):
+            return []
+        
+        # reset the calltips buffer
+        self.__calltips = None
+        
+        idString = self.__idString(editor)
+        if not idString:
+            return []
+        
+        filename, line, index, source = self.__prepareData(editor)
+        
+        self.__ensureActive(idString)
+        self.sendJson("getCallTips", {
+            "FileName": filename,
+            "Source": source,
+            "Line": line,
+            "Index": index,
+        }, idString=idString)
+        
+        # emulate the synchronous behaviour
+        timer = QTimer()
+        timer.setSingleShot(True)
+        timer.start(5000)           # 5s timeout
+        while self.__calltips is None and timer.isActive():
+            QCoreApplication.processEvents()
+            QThread.msleep(100)
+        
+        return [] if self.__calltips is None else self.__calltips
+    
+    def __processCallTipsResult(self, result):
+        """
+        Private method to process the calltips sent by the client.
+        
+        @param result dictionary containing the result sent by the client
+        @type dict
+        """
+        if "Error" in result:
+            self.__calltips = []
+        else:
+            self.__calltips = result["CallTips"]
+    
+    def requestCodeDocumentation(self, editor):
+        """
+        Public method to request source code documentation for the given
+        editor.
+        
+        @param editor reference to the editor to get source code documentation
+            for
+        @type Editor
+        """
+        if self.__documentationViewer is None:
+            return
+        
+        idString = self.__idString(editor)
+        
+        if not idString:
+            language = editor.getLanguage()
+            warning = (
+                self.tr("Language <b>{0}</b> is not supported.")
+                .format(language)
+            )
+            self.__documentationViewer.documentationReady(
+                warning, isWarning=True)
+            return
+        
+        filename, line, index, source = self.__prepareData(editor)
+        sourceLines = source.splitlines()
+        # Correct index if cursor is standing after an opening bracket
+        if line > 0 and index > 0 and sourceLines[line - 1][index - 1] == '(':
+            index -= 1
+        
+        self.__ensureActive(idString)
+        self.sendJson("getDocumentation", {
+            "FileName": filename,
+            "Source": source,
+            "Line": line,
+            "Index": index,
+        }, idString=idString)
+    
+    def __processDocumentationResult(self, result):
+        """
+        Private method to process the documentation sent by the client.
+        
+        @param result dictionary containing the result sent by the client
+        @type dict with keys 'name', 'module', 'argspec', 'docstring'
+        """
+        if self.__documentationViewer is None:
+            return
+    
+        docu = None
+        
+        if "Error" not in result:
+            docu = result["DocumentationDict"]
+            docu["note"] = (
+                self.tr("Present in <i>{0}</i> module")
+                .format(docu["module"]))
+        
+        if docu is None:
+            msg = self.tr("No documentation available.")
+            self.__documentationViewer.documentationReady(
+                msg, isDocWarning=True)
+        else:
+            self.__documentationViewer.documentationReady(docu)
+    
+    def gotoDefinition(self, editor):
+        """
+        Public slot to find the definition for the word at the cursor position
+        and go to it.
+        
+        Note: This is executed upon a mouse click sequence.
+        
+        @param editor reference to the calling editor
+        @type Editor
+        """
+        if not Preferences.getJedi("MouseClickEnabled"):
+            return
+        
+        idString = self.__idString(editor)
+        if not idString:
+            return
+        
+        filename, line, index, source = self.__prepareData(editor)
+        
+        self.__ensureActive(idString)
+        
+        euuid = str(uuid.uuid4())
+        self.__editors[euuid] = editor
+        
+        self.sendJson("gotoDefinition", {
+            "FileName": filename,
+            "Source": source,
+            "Line": line,
+            "Index": index,
+            "Uuid": euuid,
+        }, idString=idString)
+    
+    def __processGotoDefinitionResult(self, result):
+        """
+        Private method callback for the goto definition result.
+        
+        @param result dictionary containing the result data
+        @type dict
+        """
+        euuid = result["Uuid"]
+        if "Error" not in result:
+            # ignore errors silently
+            location = result["GotoDefinitionDict"]
+            if location:
+                self.__vm.openSourceFile(location["ModulePath"],
+                                         location["Line"],
+                                         addNext=True)
+            else:
+                ericApp().getObject("UserInterface").statusBar().showMessage(
+                    self.tr('Jedi: No definition found'), 5000)
+        
+        with contextlib.suppress(KeyError):
+            del self.__editors[euuid]
+    
+    def __processGotoReferencesResult(self, result):
+        """
+        Private method callback for the goto references result.
+        
+        @param result dictionary containing the result data
+        @type dict
+        """
+        euuid = result["Uuid"]
+        with contextlib.suppress(ImportError):
+            from QScintilla.Editor import ReferenceItem
+            
+            if "Error" not in result:
+                # ignore errors silently
+                references = result["GotoReferencesList"]
+                if references:
+                    try:
+                        editor = self.__editors[euuid]
+                    except KeyError:
+                        editor = None
+                    if editor is not None:
+                        referenceItemsList = [
+                            ReferenceItem(
+                                modulePath=ref["ModulePath"],
+                                codeLine=ref["Code"],
+                                line=ref["Line"],
+                                column=ref["Column"],
+                            ) for ref in references
+                        ]
+                        editor.gotoReferenceHandler(referenceItemsList)
+        
+        with contextlib.suppress(KeyError):
+            del self.__editors[euuid]
+    
+    def hoverHelp(self, editor, line, index):
+        """
+        Public method to initiate the display of mouse hover help.
+        
+        @param editor reference to the calling editor
+        @type Editor
+        @param line line number (zero based)
+        @type int
+        @param index index within the line (zero based)
+        @type int
+        """
+        idString = self.__idString(editor)
+        if not idString:
+            return
+        
+        filename = editor.getFileName()
+        line += 1       # jedi line numbers are 1 based
+        source = editor.text()
+        
+        self.__ensureActive(idString)
+        
+        euuid = str(uuid.uuid4())
+        self.__editors[euuid] = editor
+        
+        self.sendJson("hoverHelp", {
+            "FileName": filename,
+            "Source": source,
+            "Line": line,
+            "Index": index,
+            "Uuid": euuid,
+        }, idString=idString)
+    
+    def __processHoverHelpResult(self, result):
+        """
+        Private method callback for the goto definition result.
+        
+        @param result dictionary containing the result data
+        @type dict
+        """
+        euuid = result["Uuid"]
+        if "Error" not in result:
+            # ignore errors silently
+            helpText = result["HoverHelp"]
+            if helpText:
+                with contextlib.suppress(KeyError):
+                    self.__editors[euuid].showMouseHoverHelpData(
+                        result["Line"] - 1,
+                        result["Index"],
+                        helpText
+                    )
+            else:
+                ericApp().getObject("UserInterface").statusBar().showMessage(
+                    self.tr('Jedi: No mouse hover help found'), 5000)
+        
+        with contextlib.suppress(KeyError):
+            del self.__editors[euuid]
+    
+    #######################################################################
+    ## Refactoring methods below
+    #######################################################################
+    
+    @pyqtSlot()
+    def refactoringRenameVariable(self):
+        """
+        Public slot to rename the selected variable.
+        """
+        editor = self.__vm.activeWindow()
+        if editor:
+            idString = self.__idString(editor)
+            if not idString:
+                return
+            
+            newName, ok = QInputDialog.getText(
+                None,
+                self.tr("Rename Variable"),
+                self.tr("Enter the new name for the variable:"),
+                QLineEdit.EchoMode.Normal,
+                editor.selectedText()
+            )
+            
+            if ok and newName and self.__vm.checkAllDirty():
+                filename = editor.getFileName()
+                line, index = editor.getCursorPosition()
+                source = editor.text()
+                
+                self.__ensureActive(idString)
+                
+                euuid = str(uuid.uuid4())
+                self.__editors[euuid] = editor
+                
+                self.sendJson("renameVariable", {
+                    "FileName": filename,
+                    "Source": source,
+                    "Line": line + 1,
+                    "Index": index,
+                    "Uuid": euuid,
+                    "NewName": newName,
+                }, idString=idString)
+    
+    @pyqtSlot()
+    def refactoringExtractNewVariable(self):
+        """
+        Public slot to extract a statement to a new variable.
+        """
+        editor = self.__vm.activeWindow()
+        if editor:
+            idString = self.__idString(editor)
+            if not idString:
+                return
+            
+            newName, ok = QInputDialog.getText(
+                None,
+                self.tr("Extract Variable"),
+                self.tr("Enter the name for the new variable:"),
+                QLineEdit.EchoMode.Normal
+            )
+            
+            if ok and newName and editor.checkDirty():
+                filename = editor.getFileName()
+                sLine, sIndex, eLine, eIndex = editor.getSelection()
+                source = editor.text()
+                    
+                self.__ensureActive(idString)
+                
+                euuid = str(uuid.uuid4())
+                self.__editors[euuid] = editor
+                
+                self.sendJson("extractVariable", {
+                    "FileName": filename,
+                    "Source": source,
+                    "Line": sLine + 1,
+                    "Index": sIndex,
+                    "EndLine": eLine + 1,
+                    "EndIndex": eIndex,
+                    "Uuid": euuid,
+                    "NewName": newName,
+                }, idString=idString)
+    
+    @pyqtSlot()
+    def refactoringInlineVariable(self):
+        """
+        Public slot to inline the selected variable.
+        
+        Note: This is the opposite to Extract New Variable.
+        """
+        editor = self.__vm.activeWindow()
+        if editor:
+            idString = self.__idString(editor)
+            if not idString:
+                return
+            
+            if editor.checkDirty():
+                filename = editor.getFileName()
+                line, index = editor.getCursorPosition()
+                source = editor.text()
+                
+                self.__ensureActive(idString)
+                
+                euuid = str(uuid.uuid4())
+                self.__editors[euuid] = editor
+                
+                self.sendJson("inlineVariable", {
+                    "FileName": filename,
+                    "Source": source,
+                    "Line": line + 1,
+                    "Index": index,
+                    "Uuid": euuid,
+                }, idString=idString)
+    
+    @pyqtSlot()
+    def refactoringExtractFunction(self):
+        """
+        Public slot to extract an expression to a function.
+        """
+        editor = self.__vm.activeWindow()
+        if editor:
+            idString = self.__idString(editor)
+            if not idString:
+                return
+            
+            newName, ok = QInputDialog.getText(
+                None,
+                self.tr("Extract Function"),
+                self.tr("Enter the name for the function:"),
+                QLineEdit.EchoMode.Normal
+            )
+            
+            if ok and newName and editor.checkDirty():
+                filename = editor.getFileName()
+                sLine, sIndex, eLine, eIndex = editor.getSelection()
+                source = editor.text()
+                    
+                self.__ensureActive(idString)
+                
+                euuid = str(uuid.uuid4())
+                self.__editors[euuid] = editor
+                
+                self.sendJson("extractFunction", {
+                    "FileName": filename,
+                    "Source": source,
+                    "Line": sLine + 1,
+                    "Index": sIndex,
+                    "EndLine": eLine + 1,
+                    "EndIndex": eIndex,
+                    "Uuid": euuid,
+                    "NewName": newName,
+                }, idString=idString)
+    
+    def __showRefactoringDiff(self, result):
+        """
+        Private method to show the diff of a refactoring.
+        
+        @param result dictionary containing the result data
+        @type dict
+        """
+        if "Error" not in result:
+            euuid = result["Uuid"]
+            diff = result["Diff"]
+            dlg = RefactoringPreviewDialog(self.tr("Rename Variable"), diff)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                self.__applyRefactoring(euuid)
+            else:
+                self.__cancelRefactoring(euuid)
+        else:
+            EricMessageBox.critical(
+                None,
+                self.tr("Refactoring"),
+                self.tr("<p>The refactoring could not be performed.</p>"
+                        "<p>Reason: {0}</p>").format(result["ErrorString"])
+            )
+    
+    def __applyRefactoring(self, uid):
+        """
+        Private method to apply a given refactoring.
+        
+        @param uid UID of the calculated refactoring
+        @type str
+        """
+        with contextlib.suppress(KeyError):
+            editor = self.__editors[uid]
+            idString = self.__idString(editor)
+            
+            self.sendJson("applyRefactoring", {
+                "Uuid": uid,
+            }, idString=idString)
+            
+            del self.__editors[uid]
+    
+    def __cancelRefactoring(self, uid):
+        """
+        Private method to cancel a given refactoring.
+        
+        @param uid UID of the calculated refactoring
+        @type str
+        """
+        with contextlib.suppress(KeyError):
+            editor = self.__editors[uid]
+            idString = self.__idString(editor)
+            
+            self.sendJson("cancelRefactoring", {
+                "Uuid": uid,
+            }, idString=idString)
+            
+            del self.__editors[uid]
+    
+    def __checkRefactoringResult(self, result):
+        """
+        Private method to check the refactoring result for errors.
+        
+        @param result dictionary containing the result data
+        @type dict
+        """
+        if "Error" in result:
+            EricMessageBox.critical(
+                None,
+                self.tr("Apply Refactoring"),
+                self.tr("<p>The refactoring could not be applied.</p>"
+                        "<p>Reason: {0}</p>").format(result["ErrorString"])
+            )
+    
+    #######################################################################
+    ## Methods below handle the network connection
+    #######################################################################
+    
+    def handleCall(self, method, params):
+        """
+        Public method to handle a method call from the client.
+        
+        @param method requested method name
+        @type str
+        @param params dictionary with method specific parameters
+        @type dict
+        """
+        self.__methodMapping[method](params)
+    
+    def __processClientException(self, params):
+        """
+        Private method to handle exceptions of the refactoring client.
+        
+        @param params dictionary containing the exception data
+        @type dict
+        """
+        if params["ExceptionType"] == "ProtocolError":
+            self.__ui.appendToStderr(
+                self.tr("The data received from the Jedi server could not be"
+                        " decoded. Please report this issue with the received"
+                        " data to the eric bugs email address.\n"
+                        "Error: {0}\n"
+                        "Data:\n{1}\n").format(
+                    params["ExceptionValue"],
+                    params["ProtocolData"]))
+        else:
+            self.__ui.appendToStderr(
+                self.tr("An exception happened in the Jedi client. Please"
+                        " report it to the eric bugs email address.\n"
+                        "Exception: {0}\n"
+                        "Value: {1}\n"
+                        "Traceback: {2}\n").format(
+                    params["ExceptionType"],
+                    params["ExceptionValue"],
+                    params["Traceback"]))
+    
+    def __startJediClient(self, interpreter, idString, clientEnv):
+        """
+        Private method to start the Jedi client with the given interpreter.
+        
+        @param interpreter interpreter to be used for the Jedi client
+        @type str
+        @param idString id of the client to be started
+        @type str
+        @param clientEnv dictionary with environment variables to run the
+            interpreter with
+        @type dict
+        @return flag indicating a successful start of the client
+        @rtype bool
+        """
+        ok = False
+        
+        if interpreter:
+            client = os.path.join(os.path.dirname(__file__),
+                                  "JediClient.py")
+            ok, exitCode = self.startClient(
+                interpreter, client,
+                [Globals.getPythonLibraryDirectory()],
+                idString=idString, environment=clientEnv)
+            if not ok:
+                if exitCode == 42:
+                    self.__ui.appendToStderr("JediServer: " + self.tr(
+                        "The jedi and/or parso library is not installed.\n"
+                    ))
+                else:
+                    self.__ui.appendToStderr("JediServer: " + self.tr(
+                        "'{0}' is not supported because the configured"
+                        " interpreter could not be started.\n"
+                    ).format(idString))
+        else:
+            self.__ui.appendToStderr("JediServer: " + self.tr(
+                "'{0}' is not supported because no suitable interpreter is"
+                " configured.\n"
+            ).format(idString))
+        
+        return ok
+    
+    def __ensureActive(self, idString):
+        """
+        Private method to ensure, that the requested client is active.
+        
+        A non-active client will be started.
+        
+        @param idString id of the client to be checked
+        @type str
+        @return flag indicating an active client
+        @rtype bool
+        """
+        ok = idString in self.connectionNames()
+        if not ok:
+            # client is not running
+            if idString == JediServer.IdProject:
+                interpreter, clientEnv = self.__interpreterForProject()
+            else:
+                interpreter = ""
+                venvName = ""
+                clientEnv = os.environ.copy()
+                if "PATH" in clientEnv:
+                    clientEnv["PATH"] = self.__ui.getOriginalPathString()
+                # new code using virtual environments
+                venvManager = ericApp().getObject("VirtualEnvManager")
+                if idString == "Python3":
+                    venvName = Preferences.getDebugger("Python3VirtualEnv")
+                    if not venvName:
+                        venvName, _ = venvManager.getDefaultEnvironment()
+                if venvName:
+                    interpreter = venvManager.getVirtualenvInterpreter(
+                        venvName)
+                    execPath = venvManager.getVirtualenvExecPath(venvName)
+                    
+                    # build a suitable environment
+                    if execPath:
+                        if "PATH" in clientEnv:
+                            clientEnv["PATH"] = os.pathsep.join(
+                                [execPath, clientEnv["PATH"]])
+                        else:
+                            clientEnv["PATH"] = execPath
+            if interpreter:
+                ok = self.__startJediClient(interpreter, idString, clientEnv)
+            else:
+                ok = False
+        return ok
+    
+    def __interpreterForProject(self):
+        """
+        Private method to determine the interpreter for the current project and
+        the environment to run it.
+        
+        @return tuple containing the interpreter of the current project and the
+            environment variables
+        @rtype tuple of (str, dict)
+        """
+        projectLanguage = self.__ericProject.getProjectLanguage()
+        interpreter = ""
+        clientEnv = os.environ.copy()
+        if "PATH" in clientEnv:
+            clientEnv["PATH"] = self.__ui.getOriginalPathString()
+        
+        if projectLanguage in ("Python3", "MicroPython", "Cython"):
+            interpreter = self.__ericProject.getProjectInterpreter(
+                resolveGlobal=False)
+            if interpreter:
+                execPath = self.__ericProject.getProjectExecPath()
+                
+                # build a suitable environment
+                if execPath:
+                    if "PATH" in clientEnv:
+                        clientEnv["PATH"] = os.pathsep.join(
+                            [execPath, clientEnv["PATH"]])
+                    else:
+                        clientEnv["PATH"] = execPath
+        
+        return interpreter, clientEnv
+    
+    @pyqtSlot()
+    def handleNewConnection(self):
+        """
+        Public slot for new incoming connections from a client.
+        """
+        super().handleNewConnection()
+        
+        self.__updateEditorLanguageMapping()
+    
+    def activate(self):
+        """
+        Public method to activate the Jedi server.
+        """
+        self.__documentationViewer = self.__ui.documentationViewer()
+        if self.__documentationViewer is not None:
+            self.__documentationViewer.registerProvider(
+                "jedi", self.tr("Jedi"), self.requestCodeDocumentation,
+                self.isSupportedLanguage)
+        
+        self.__ericProject.projectOpened.connect(self.__projectOpened)
+        self.__ericProject.projectClosed.connect(self.__projectClosed)
+    
+    def deactivate(self):
+        """
+        Public method to deactivate the code assist server.
+        """
+        """
+        Public method to shut down the code assist server.
+        """
+        if self.__documentationViewer is not None:
+            self.__documentationViewer.unregisterProvider("jedi")
+        
+        with contextlib.suppress(TypeError):
+            self.__ericProject.projectOpened.disconnect(self.__projectOpened)
+            self.__ericProject.projectClosed.disconnect(self.__projectClosed)
+        
+        self.stopAllClients()
+    
+    @pyqtSlot()
+    def __projectOpened(self):
+        """
+        Private slot to handle the projectOpened signal.
+        """
+        self.__ensureActive(JediServer.IdProject)
+        self.sendJson("openProject", {
+            "ProjectPath": self.__ericProject.getProjectPath(),
+        }, idString=JediServer.IdProject)
+    
+    @pyqtSlot()
+    def __projectClosed(self):
+        """
+        Private slot to handle the projectClosed signal.
+        """
+        self.__ensureActive(JediServer.IdProject)
+        self.sendJson("closeProject", {}, idString=JediServer.IdProject)
+        
+        self.stopClient(idString=JediServer.IdProject)

eric ide

mercurial