--- a/AssistantEric/Assistant.py Thu Dec 30 11:32:05 2021 +0100 +++ b/AssistantEric/Assistant.py Wed Sep 21 16:59:53 2022 +0200 @@ -26,75 +26,77 @@ """ Class implementing the autocompletion and calltips system. """ + def __init__(self, plugin, parent=None): """ Constructor - + @param plugin reference to the plugin object @type AssistantEricPlugin @param parent parent @type QObject """ QObject.__init__(self, parent) - + self.__plugin = plugin self.__ui = parent self.__project = ericApp().getObject("Project") self.__viewmanager = ericApp().getObject("ViewManager") self.__pluginManager = ericApp().getObject("PluginManager") - + self.__apisManager = APIsManager(self.__ui, self) - + self.__editors = [] self.__lastContext = None self.__lastFullContext = None - + from QScintilla.Editor import Editor + self.__fromDocumentID = Editor.FromDocumentID - + def activate(self): """ Public method to perform actions upon activation. """ self.__pluginManager.shutdown.connect(self.__shutdown) - + self.__ui.preferencesChanged.connect(self.__preferencesChanged) - + self.__viewmanager.editorOpenedEd.connect(self.__editorOpened) self.__viewmanager.editorClosedEd.connect(self.__editorClosed) - + # preload the project APIs object self.__apisManager.getAPIs(ApisNameProject) - + for editor in self.__viewmanager.getOpenEditors(): self.__editorOpened(editor) - + def deactivate(self): """ Public method to perform actions upon deactivation. """ self.__pluginManager.shutdown.disconnect(self.__shutdown) - + self.__ui.preferencesChanged.disconnect(self.__preferencesChanged) - + self.__viewmanager.editorOpenedEd.disconnect(self.__editorOpened) self.__viewmanager.editorClosedEd.disconnect(self.__editorClosed) - + self.__shutdown() - + def __shutdown(self): """ Private slot to handle the shutdown signal. """ for editor in self.__editors[:]: self.__editorClosed(editor) - + self.__apisManager.deactivate() - + def setEnabled(self, key, enabled): """ Public method to enable or disable a feature. - + @param key feature to set @type str @param enabled flag indicating the status @@ -104,11 +106,11 @@ self.__editorClosed(editor) for editor in self.__viewmanager.getOpenEditors(): self.__editorOpened(editor) - + def __editorOpened(self, editor): """ Private slot called, when a new editor was opened. - + @param editor reference to the new editor @type Editor """ @@ -117,41 +119,43 @@ if self.__plugin.getPreferences("CalltipsEnabled"): self.__setCalltipsHook(editor) editor.editorSaved.connect( - self.__apisManager.getAPIs(ApisNameProject).editorSaved) + self.__apisManager.getAPIs(ApisNameProject).editorSaved + ) self.__editors.append(editor) - + # preload the api to give the manager a chance to prepare the database language = editor.getApiLanguage() if language: projectType = self.__getProjectType(editor) self.__apisManager.getAPIs(language, projectType=projectType) - + def __editorClosed(self, editor): """ Private slot called, when an editor was closed. - + @param editor reference to the editor @type Editor """ if editor in self.__editors: editor.editorSaved.disconnect( - self.__apisManager.getAPIs(ApisNameProject).editorSaved) + self.__apisManager.getAPIs(ApisNameProject).editorSaved + ) self.__editors.remove(editor) if editor.getCompletionListHook("Assistant"): self.__unsetAutoCompletionHook(editor) if editor.getCallTipHook("Assistant"): self.__unsetCalltipsHook(editor) - + def __preferencesChanged(self): """ Private method to handle a change of the global configuration. """ self.__apisManager.reloadAPIs() - + def __getProjectType(self, editor): """ Private method to determine the project type to be used. - + @param editor reference to the editor to check @type Editor @return project type @@ -160,61 +164,63 @@ filename = editor.getFileName() projectType = ( self.__project.getProjectType() - if (self.__project.isOpen() and - filename and - self.__project.isProjectFile(filename)) else - "" + if ( + self.__project.isOpen() + and filename + and self.__project.isProjectFile(filename) + ) + else "" ) - + return projectType - + ################################# ## auto-completion methods below ################################# - + def __recordSelectedContext(self, userListId, txt): """ Private slot to handle the selection from the completion list to record the selected completion context. - + @param userListId the ID of the user list (should be 1) @type int @param txt the selected text @type str """ from QScintilla.Editor import EditorAutoCompletionListID - + if userListId == EditorAutoCompletionListID: lst = txt.split() if len(lst) > 1: self.__lastFullContext = lst[1][1:].split(")")[0] else: self.__lastFullContext = None - + def __setAutoCompletionHook(self, editor): """ Private method to set the autocompletion hook. - + @param editor reference to the editor @type Editor """ editor.userListActivated.connect(self.__recordSelectedContext) editor.addCompletionListHook("Assistant", self.getCompletionsList) - + def __unsetAutoCompletionHook(self, editor): """ Private method to unset the autocompletion hook. - + @param editor reference to the editor @type Editor """ editor.userListActivated.disconnect(self.__recordSelectedContext) editor.removeCompletionListHook("Assistant") - + def getCompletionsList(self, editor, context): """ Public method to get 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 @@ -225,54 +231,48 @@ language = editor.getApiLanguage() completeFromDocumentOnly = False if language in ["", "Guessed"] or language.startswith("Pygments|"): - if ( - self.__plugin.getPreferences("AutoCompletionSource") & - AcsDocument - ): + if self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument: completeFromDocumentOnly = True else: return [] - + projectType = self.__getProjectType(editor) - + line, col = editor.getCursorPosition() sep = "" if language and context: wc = re.sub("\w", "", editor.wordCharacters()) pat = re.compile("\w{0}".format(re.escape(wc))) text = editor.text(line) - + beg = text[:col] for wsep in editor.getLexer().autoCompletionWordSeparators(): if beg.endswith(wsep): sep = wsep break - + depth = 0 - while ( - col > 0 and - not pat.match(text[col - 1]) - ): + while col > 0 and not pat.match(text[col - 1]): ch = text[col - 1] - if ch == ')': + if ch == ")": depth = 1 - + # ignore everything back to the start of the # corresponding parenthesis col -= 1 while col > 0: ch = text[col - 1] - if ch == ')': + if ch == ")": depth += 1 - elif ch == '(': + elif ch == "(": depth -= 1 if depth == 0: break col -= 1 - elif ch == '(': + elif ch == "(": break col -= 1 - + word = editor.getWordLeft(line, col) if context and not sep: # no separator was found -> no context completion @@ -281,16 +281,12 @@ self.__lastContext = word else: self.__lastContext = None - + prefix = "" mod = None - beg = beg[:col + 1] if context else editor.text(line)[:col] + beg = beg[: col + 1] if context else editor.text(line)[:col] col = len(beg) - wseps = ( - editor.getLexer().autoCompletionWordSeparators() - if language else - [] - ) + wseps = editor.getLexer().autoCompletionWordSeparators() if language else [] if wseps: wseps.append(" ") if col > 0 and beg[col - 1] in wseps: @@ -303,13 +299,14 @@ prefix = editor.getWordLeft(line, col) if editor.isPyFile(): from Utilities.ModuleParser import Module + src = editor.text() fn = editor.getFileName() if fn is None: fn = "" mod = Module("", fn, imp.PY_SOURCE) mod.scan(src) - + importCompletion = False if editor.isPyFile(): # check, if we are completing a from import statement @@ -332,28 +329,57 @@ while col >= 0 and prefix[col] not in wseps: col -= 1 if col >= 0: - prefix = prefix[col + 1:] + prefix = prefix[col + 1 :] if word == tokens[2]: word = "" - + if word or importCompletion: completionsList = self.__getCompletions( - word, context, prefix, language, projectType, mod, editor, - importCompletion, completeFromDocumentOnly, sep) + word, + context, + prefix, + language, + projectType, + mod, + editor, + importCompletion, + completeFromDocumentOnly, + sep, + ) if len(completionsList) == 0 and prefix: # searching with prefix didn't return anything, try without completionsList = self.__getCompletions( - word, context, "", language, projectType, mod, editor, - importCompletion, completeFromDocumentOnly, sep) + word, + context, + "", + language, + projectType, + mod, + editor, + importCompletion, + completeFromDocumentOnly, + sep, + ) return completionsList - + return [] - - def __getCompletions(self, word, context, prefix, language, projectType, - module, editor, importCompletion, documentOnly, sep): + + def __getCompletions( + self, + word, + context, + prefix, + language, + projectType, + module, + editor, + importCompletion, + documentOnly, + sep, + ): """ Private method to get the list of possible completions. - + @param word word (or wordpart) to complete @type str @param context flag indicating to autocomplete a context @@ -380,41 +406,39 @@ apiCompletionsList = [] docCompletionsList = [] projectCompletionList = [] - + if not documentOnly: if self.__plugin.getPreferences("AutoCompletionSource") & AcsAPIs: - api = self.__apisManager.getAPIs( - language, projectType=projectType) + api = self.__apisManager.getAPIs(language, projectType=projectType) apiCompletionsList = self.__getApiCompletions( - api, word, context, prefix, module, editor) - - if ( - self.__plugin.getPreferences("AutoCompletionSource") & - AcsProject - ): + api, word, context, prefix, module, editor + ) + + if self.__plugin.getPreferences("AutoCompletionSource") & AcsProject: api = self.__apisManager.getAPIs(ApisNameProject) projectCompletionList = self.__getApiCompletions( - api, word, context, prefix, module, editor) - + api, word, context, prefix, module, editor + ) + if ( - self.__plugin.getPreferences("AutoCompletionSource") & - AcsDocument and - not importCompletion + self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument + and not importCompletion ): docCompletionsList = self.__getDocumentCompletions( - editor, word, context, sep, prefix, module) - + editor, word, context, sep, prefix, module + ) + completionsList = list( set(apiCompletionsList) .union(set(docCompletionsList)) .union(set(projectCompletionList)) ) return completionsList - + def __getApiCompletions(self, api, word, context, prefix, module, editor): """ Private method to determine a list of completions from an API object. - + @param api reference to the API object to be used @type APIsManager.DbAPIs @param word word (or wordpart) to complete @@ -435,9 +459,8 @@ if prefix and module and prefix == "self": line, col = editor.getCursorPosition() for cl in module.classes.values(): - if ( - line >= cl.lineno and - (cl.endlineno == -1 or line <= cl.endlineno) + if line >= cl.lineno and ( + cl.endlineno == -1 or line <= cl.endlineno ): completions = [] for superClass in cl.super: @@ -445,20 +468,19 @@ completions.extend( api.getCompletions( context=superClass, - followHierarchy=self.__plugin - .getPreferences( + followHierarchy=self.__plugin.getPreferences( "AutoCompletionFollowHierarchy" - ) + ), ) ) else: completions.extend( api.getCompletions( - start=word, context=superClass, - followHierarchy=self.__plugin - .getPreferences( + start=word, + context=superClass, + followHierarchy=self.__plugin.getPreferences( "AutoCompletionFollowHierarchy" - ) + ), ) ) for completion in completions: @@ -466,8 +488,7 @@ entry = completion["completion"] else: entry = "{0} ({1})".format( - completion["completion"], - completion["context"] + completion["completion"], completion["context"] ) if entry in completionsList: completionsList.remove(entry) @@ -475,8 +496,7 @@ entry += "?{0}".format(completion["pictureId"]) else: cont = False - regexp = re.compile( - re.escape(entry) + r"\?\d{,2}") + regexp = re.compile(re.escape(entry) + r"\?\d{,2}") for comp in completionsList: if regexp.fullmatch(comp): cont = True @@ -485,7 +505,7 @@ continue if entry not in completionsList: completionsList.append(entry) - + break elif context: completions = api.getCompletions(context=word) @@ -497,8 +517,7 @@ completionsList.append(entry) else: if prefix: - completions = api.getCompletions( - start=word, context=prefix) + completions = api.getCompletions(start=word, context=prefix) if not prefix or not completions: # if no completions were returned try without prefix completions = api.getCompletions(start=word) @@ -507,8 +526,7 @@ entry = completion["completion"] else: entry = "{0} ({1})".format( - completion["completion"], - completion["context"] + completion["completion"], completion["context"] ) if entry in completionsList: completionsList.remove(entry) @@ -526,12 +544,13 @@ if entry not in completionsList: completionsList.append(entry) return completionsList - - def __getDocumentCompletions(self, editor, word, context, sep, prefix, - module, doHierarchy=False): + + def __getDocumentCompletions( + self, editor, word, context, sep, prefix, module, doHierarchy=False + ): """ Private method to determine autocompletion proposals from the document. - + @param editor reference to the editor object @type Editor @param word string to be completed @@ -550,18 +569,17 @@ @rtype list of str """ completionsList = [] - + prefixFound = False if prefix and module: from QScintilla.Editor import Editor - + line, col = editor.getCursorPosition() if prefix in ["cls", "self"]: prefixFound = True for cl in module.classes.values(): - if ( - line >= cl.lineno and - (cl.endlineno == -1 or line <= cl.endlineno) + if line >= cl.lineno and ( + cl.endlineno == -1 or line <= cl.endlineno ): comps = [] for method in cl.methods.values(): @@ -575,10 +593,8 @@ else: iconID = Editor.MethodID if ( - (prefix == "cls" and - method.modifier == method.Class) or - prefix == "self" - ): + prefix == "cls" and method.modifier == method.Class + ) or prefix == "self": comps.append((method.name, cl.name, iconID)) if prefix != "cls": for attribute in cl.attributes.values(): @@ -599,16 +615,23 @@ else: iconID = Editor.AttributeID comps.append((attribute.name, cl.name, iconID)) - + if word != prefix: completionsList.extend( - ["{0} ({1})?{2}".format(c[0], c[1], c[2]) - for c in comps if c[0].startswith(word)]) + [ + "{0} ({1})?{2}".format(c[0], c[1], c[2]) + for c in comps + if c[0].startswith(word) + ] + ) else: completionsList.extend( - ["{0} ({1})?{2}".format(c[0], c[1], c[2]) - for c in comps]) - + [ + "{0} ({1})?{2}".format(c[0], c[1], c[2]) + for c in comps + ] + ) + for sup in cl.super: if sup in module.classes: if word == prefix: @@ -617,9 +640,16 @@ nword = word completionsList.extend( self.__getDocumentCompletions( - editor, nword, context, sep, sup, - module, doHierarchy=True)) - + editor, + nword, + context, + sep, + sup, + module, + doHierarchy=True, + ) + ) + break else: # possibly completing a named class attribute or method @@ -630,10 +660,10 @@ for method in cl.methods.values(): if method.name == "__init__": continue - if ( - doHierarchy or - method.modifier in [method.Class, method.Static] - ): + if doHierarchy or method.modifier in [ + method.Class, + method.Static, + ]: # determine icon type if method.isPrivate(): if doHierarchy: @@ -653,16 +683,20 @@ else: iconID = Editor.AttributeID comps.append((attribute.name, cl.name, iconID)) - + if word != prefix: completionsList.extend( - ["{0} ({1})?{2}".format(c[0], c[1], c[2]) - for c in comps if c[0].startswith(word)]) + [ + "{0} ({1})?{2}".format(c[0], c[1], c[2]) + for c in comps + if c[0].startswith(word) + ] + ) else: completionsList.extend( - ["{0} ({1})?{2}".format(c[0], c[1], c[2]) - for c in comps]) - + ["{0} ({1})?{2}".format(c[0], c[1], c[2]) for c in comps] + ) + for sup in cl.super: if sup in module.classes: if word == prefix: @@ -671,21 +705,34 @@ nword = word completionsList.extend( self.__getDocumentCompletions( - editor, nword, context, sep, sup, module, - doHierarchy=True)) - + editor, + nword, + context, + sep, + sup, + module, + doHierarchy=True, + ) + ) + if not prefixFound: currentPos = editor.currentPosition() if context: word += sep - + if editor.isUtf8(): sword = word.encode("utf-8") else: sword = word res = editor.findFirstTarget( - sword, False, editor.autoCompletionCaseSensitivity(), - False, begline=0, begindex=0, ws_=True) + sword, + False, + editor.autoCompletionCaseSensitivity(), + False, + begline=0, + begindex=0, + ws_=True, + ) while res: start, length = editor.getFoundTarget() pos = start + length @@ -696,47 +743,47 @@ completion = word line, index = editor.lineIndexFromPosition(pos) curWord = editor.getWord(line, index, useWordChars=False) - completion += curWord[len(completion):] + completion += curWord[len(completion) :] if ( - completion and - completion not in completionsList and - completion != word + completion + and completion not in completionsList + and completion != word ): completionsList.append( - "{0}?{1}".format( - completion, self.__fromDocumentID)) - + "{0}?{1}".format(completion, self.__fromDocumentID) + ) + res = editor.findNextTarget() - + completionsList.sort() return completionsList - + ########################### ## calltips methods below ########################### - + def __setCalltipsHook(self, editor): """ Private method to set the calltip hook. - + @param editor reference to the editor @type Editor """ editor.addCallTipHook("Assistant", self.calltips) - + def __unsetCalltipsHook(self, editor): """ Private method to unset the calltip hook. - + @param editor reference to the editor @type Editor """ editor.removeCallTipHook("Assistant") - + def calltips(self, editor, pos, commas): """ Public method to return a list of calltips. - + @param editor reference to the editor @type Editor @param pos position in the text for the calltip @@ -749,16 +796,13 @@ language = editor.getApiLanguage() completeFromDocumentOnly = False if language in ["", "Guessed"] or language.startswith("Pygments|"): - if ( - self.__plugin.getPreferences("AutoCompletionSource") & - AcsDocument - ): + if self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument: completeFromDocumentOnly = True else: return [] - + projectType = self.__getProjectType(editor) - + line, col = editor.lineIndexFromPosition(pos) wc = re.sub("\w", "", editor.wordCharacters()) pat = re.compile("\w{0}".format(re.escape(wc))) @@ -766,16 +810,12 @@ while col > 0 and not pat.match(text[col - 1]): col -= 1 word = editor.getWordLeft(line, col) - + prefix = "" mod = None beg = editor.text(line)[:col] col = len(beg) - wseps = ( - editor.getLexer().autoCompletionWordSeparators() - if language else - [] - ) + wseps = editor.getLexer().autoCompletionWordSeparators() if language else [] if wseps: if col > 0 and beg[col - 1] in wseps: col -= 1 @@ -787,47 +827,43 @@ prefix = editor.getWordLeft(line, col) if editor.isPyFile(): from Utilities.ModuleParser import Module + src = editor.text() fn = editor.getFileName() if fn is None: fn = "" mod = Module("", fn, imp.PY_SOURCE) mod.scan(src) - + apiCalltips = [] projectCalltips = [] documentCalltips = [] - + if not completeFromDocumentOnly: if self.__plugin.getPreferences("AutoCompletionSource") & AcsAPIs: - api = self.__apisManager.getAPIs( - language, projectType=projectType) + api = self.__apisManager.getAPIs(language, projectType=projectType) if api is not None: apiCalltips = self.__getApiCalltips( - api, word, commas, prefix, mod, editor) - - if ( - self.__plugin.getPreferences("AutoCompletionSource") & - AcsProject - ): + api, word, commas, prefix, mod, editor + ) + + if self.__plugin.getPreferences("AutoCompletionSource") & AcsProject: api = self.__apisManager.getAPIs(ApisNameProject) projectCalltips = self.__getApiCalltips( - api, word, commas, prefix, mod, editor) - + api, word, commas, prefix, mod, editor + ) + if self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument: - documentCalltips = self.__getDocumentCalltips( - word, prefix, mod, editor) - + documentCalltips = self.__getDocumentCalltips(word, prefix, mod, editor) + return sorted( - set(apiCalltips) - .union(set(projectCalltips)) - .union(set(documentCalltips)) + set(apiCalltips).union(set(projectCalltips)).union(set(documentCalltips)) ) - + def __getApiCalltips(self, api, word, commas, prefix, module, editor): """ Private method to determine calltips from APIs. - + @param api reference to the API object to be used @type APIsManager.DbAPIs @param word function to get calltips for @@ -847,30 +883,36 @@ if prefix and module and prefix == "self": line, col = editor.getCursorPosition() for cl in module.classes.values(): - if ( - line >= cl.lineno and - (cl.endlineno == -1 or line <= cl.endlineno) - ): + if line >= cl.lineno and (cl.endlineno == -1 or line <= cl.endlineno): for superClass in cl.super: - calltips.extend(api.getCalltips( - word, commas, superClass, None, - self.__plugin.getPreferences( - "CallTipsContextShown"), - followHierarchy=self.__plugin.getPreferences( - "CallTipsFollowHierarchy"))) + calltips.extend( + api.getCalltips( + word, + commas, + superClass, + None, + self.__plugin.getPreferences("CallTipsContextShown"), + followHierarchy=self.__plugin.getPreferences( + "CallTipsFollowHierarchy" + ), + ) + ) break else: calltips = api.getCalltips( - word, commas, self.__lastContext, self.__lastFullContext, - self.__plugin.getPreferences("CallTipsContextShown")) - + word, + commas, + self.__lastContext, + self.__lastFullContext, + self.__plugin.getPreferences("CallTipsContextShown"), + ) + return calltips - - def __getDocumentCalltips(self, word, prefix, module, editor, - doHierarchy=False): + + def __getDocumentCalltips(self, word, prefix, module, editor, doHierarchy=False): """ Private method to determine calltips from the document. - + @param word function to get calltips for @type str @param prefix prefix of the word to be completed @@ -892,30 +934,30 @@ if prefix in ["self", "cls"]: line, col = editor.getCursorPosition() for cl in module.classes.values(): - if ( - line >= cl.lineno and - (cl.endlineno == -1 or line <= cl.endlineno) + if line >= cl.lineno and ( + cl.endlineno == -1 or line <= cl.endlineno ): if word in cl.methods: method = cl.methods[word] - if ( - prefix == "self" or - (prefix == "cls" and - method.modifier == method.Class) + if prefix == "self" or ( + prefix == "cls" and method.modifier == method.Class ): calltips.append( "{0}{1}{2}({3})".format( cl.name, sep, word, - ', '.join(method.parameters[1:] - ))) - + ", ".join(method.parameters[1:]), + ) + ) + for sup in cl.super: - calltips.extend(self.__getDocumentCalltips( - word, sup, module, editor, - doHierarchy=True)) - + calltips.extend( + self.__getDocumentCalltips( + word, sup, module, editor, doHierarchy=True + ) + ) + break else: if prefix in module.classes: @@ -923,31 +965,36 @@ if word in cl.methods: method = cl.methods[word] if doHierarchy or method.modifier == method.Class: - calltips.append("{0}{1}{2}({3})".format( - cl.name, - sep, - word, - ', '.join(method.parameters[1:]))) - + calltips.append( + "{0}{1}{2}({3})".format( + cl.name, + sep, + word, + ", ".join(method.parameters[1:]), + ) + ) + for sup in cl.super: - calltips.extend(self.__getDocumentCalltips( - word, sup, module, editor, doHierarchy=True)) + calltips.extend( + self.__getDocumentCalltips( + word, sup, module, editor, doHierarchy=True + ) + ) else: # calltip for a module function or class if word in module.functions: fun = module.functions[word] - calltips.append("{0}({1})".format( - word, - ', '.join(fun.parameters))) + calltips.append("{0}({1})".format(word, ", ".join(fun.parameters))) elif word in module.classes: cl = module.classes[word] if "__init__" in cl.methods: method = cl.methods["__init__"] - calltips.append("{0}({1})".format( - word, - ', '.join(method.parameters[1:]))) - + calltips.append( + "{0}({1})".format(word, ", ".join(method.parameters[1:])) + ) + return calltips + # # eflag: noqa = M834, W605