--- 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