Sat, 24 Apr 2021 10:56:08 +0200
- applied some code simplifications
- dropped support for some obsolete eric6 versions
# -*- coding: utf-8 -*- # Copyright (c) 2008 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the eric assistant, an alternative autocompletion and calltips system. """ import re import imp from PyQt5.QtCore import QRegExp, QObject from E5Gui.E5Application import e5App from .APIsManager import APIsManager, ApisNameProject AcsAPIs = 0x0001 AcsDocument = 0x0002 AcsProject = 0x0004 class Assistant(QObject): """ Class implementing the autocompletion and calltips system. """ def __init__(self, plugin, parent=None): """ Constructor @param plugin reference to the plugin object @param parent parent (QObject) """ QObject.__init__(self, parent) self.__plugin = plugin self.__ui = parent self.__project = e5App().getObject("Project") self.__viewmanager = e5App().getObject("ViewManager") self.__pluginManager = e5App().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 (string) @param enabled flag indicating the status (boolean) """ for editor in self.__editors[:]: 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 (QScintilla.Editor) """ if self.__plugin.getPreferences("AutoCompletionEnabled"): self.__setAutoCompletionHook(editor) if self.__plugin.getPreferences("CalltipsEnabled"): self.__setCalltipsHook(editor) editor.editorSaved.connect( 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 (QScintilla.Editor) """ if editor in self.__editors: editor.editorSaved.disconnect( 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 @rtype str """ filename = editor.getFileName() projectType = ( self.__project.getProjectType() 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) (integer) @param txt the selected text (string) """ 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 (QScintilla.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 (QScintilla.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 (QScintilla.Editor) @param context flag indicating to autocomplete a context (boolean) @return list of possible completions (list of strings) """ language = editor.getApiLanguage() completeFromDocumentOnly = False if language in ["", "Guessed"] or language.startswith("Pygments|"): 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]) ): ch = text[col - 1] 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 == ')': depth += 1 elif ch == '(': depth -= 1 if depth == 0: break col -= 1 elif ch == '(': break col -= 1 word = editor.getWordLeft(line, col) if context and not sep: # no separator was found -> no context completion context = False if context: self.__lastContext = word else: self.__lastContext = None prefix = "" mod = None beg = beg[:col + 1] if context else editor.text(line)[:col] col = len(beg) wseps = ( editor.getLexer().autoCompletionWordSeparators() if language else [] ) if wseps: wseps.append(" ") if col > 0 and beg[col - 1] in wseps: col -= 1 else: while col > 0 and beg[col - 1] not in wseps: col -= 1 if col > 0 and beg[col - 1] != " ": col -= 1 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 maxLines = 10 text = editor.text(line).strip() while maxLines and line > 0 and not text.startswith("from"): line -= 1 textm1 = editor.text(line).strip() if not textm1.endswith("\\"): break text = textm1[:-1] + text maxLines -= 1 if text.startswith("from"): tokens = text.split() if len(tokens) >= 3 and tokens[2] == "import": importCompletion = True prefix = tokens[1] col = len(prefix) - 1 wseps = editor.getLexer().autoCompletionWordSeparators() while col >= 0 and prefix[col] not in wseps: col -= 1 if col >= 0: 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) 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) return completionsList return [] 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 (string) @param context flag indicating to autocomplete a context (boolean) @param prefix prefix of the word to be completed (string) @param language programming language of the source (string) @param projectType type of the project (string) @param module reference to the scanned module info (Module) @param editor reference to the editor object (QScintilla.Editor.Editor) @param importCompletion flag indicating an import completion (boolean) @param documentOnly flag indicating to complete from the document only (boolean) @param sep separator string (string) @return list of possible completions (list of strings) """ apiCompletionsList = [] docCompletionsList = [] projectCompletionList = [] if not documentOnly: if self.__plugin.getPreferences("AutoCompletionSource") & AcsAPIs: api = self.__apisManager.getAPIs( language, projectType=projectType) apiCompletionsList = self.__getApiCompletions( 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) if ( self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument and not importCompletion ): docCompletionsList = self.__getDocumentCompletions( 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 (APIsManager.DbAPIs) @param word word (or wordpart) to complete (string) @param context flag indicating to autocomplete a context (boolean) @param prefix prefix of the word to be completed (string) @param module reference to the scanned module info (Module) @param editor reference to the editor object (QScintilla.Editor.Editor) @return list of possible completions (list of strings) """ completionsList = [] if api is not None: 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) ): completions = [] for superClass in cl.super: if prefix == word: completions.extend( api.getCompletions( context=superClass, followHierarchy=self.__plugin .getPreferences( "AutoCompletionFollowHierarchy" ) ) ) else: completions.extend( api.getCompletions( start=word, context=superClass, followHierarchy=self.__plugin .getPreferences( "AutoCompletionFollowHierarchy" ) ) ) for completion in completions: if not completion["context"]: entry = completion["completion"] else: entry = "{0} ({1})".format( completion["completion"], completion["context"] ) if entry in completionsList: completionsList.remove(entry) if completion["pictureId"]: entry += "?{0}".format(completion["pictureId"]) else: cont = False re = QRegExp( QRegExp.escape(entry) + "\?\d{,2}") for comp in completionsList: if re.exactMatch(comp): cont = True break if cont: continue if entry not in completionsList: completionsList.append(entry) break elif context: completions = api.getCompletions(context=word) for completion in completions: entry = completion["completion"] if completion["pictureId"]: entry += "?{0}".format(completion["pictureId"]) if entry not in completionsList: completionsList.append(entry) else: if 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) for completion in completions: if not completion["context"]: entry = completion["completion"] else: entry = "{0} ({1})".format( completion["completion"], completion["context"] ) if entry in completionsList: completionsList.remove(entry) if completion["pictureId"]: entry += "?{0}".format(completion["pictureId"]) else: cont = False re = QRegExp(QRegExp.escape(entry) + "\?\d{,2}") for comp in completionsList: if re.exactMatch(comp): cont = True break if cont: continue if entry not in completionsList: completionsList.append(entry) return completionsList 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 (QScintilla.Editor.Editor) @param word string to be completed (string) @param context flag indicating to autocomplete a context (boolean) @param sep separator string (string) @param prefix prefix of the word to be completed (string) @param module reference to the scanned module info (Module) @keyparam doHierarchy flag indicating a hierarchical search (boolean) @return list of possible completions (list of strings) """ 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) ): comps = [] for method in cl.methods.values(): if method.name == "__init__": continue # determine icon type if method.isPrivate(): iconID = Editor.MethodPrivateID elif method.isProtected(): iconID = Editor.MethodProtectedID else: iconID = Editor.MethodID if ( (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(): # determine icon type if attribute.isPrivate(): iconID = Editor.AttributePrivateID elif attribute.isProtected(): iconID = Editor.AttributeProtectedID else: iconID = Editor.AttributeID comps.append((attribute.name, cl.name, iconID)) for attribute in cl.globals.values(): # determine icon type if attribute.isPrivate(): iconID = Editor.AttributePrivateID elif attribute.isProtected(): iconID = Editor.AttributeProtectedID 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)]) else: completionsList.extend( ["{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: nword = sup else: nword = word completionsList.extend( self.__getDocumentCompletions( editor, nword, context, sep, sup, module, doHierarchy=True)) break else: # possibly completing a named class attribute or method if prefix in module.classes: prefixFound = True cl = module.classes[prefix] comps = [] for method in cl.methods.values(): if method.name == "__init__": continue if ( doHierarchy or method.modifier in [method.Class, method.Static] ): # determine icon type if method.isPrivate(): if doHierarchy: continue iconID = Editor.MethodPrivateID elif method.isProtected(): iconID = Editor.MethodProtectedID else: iconID = Editor.MethodID comps.append((method.name, cl.name, iconID)) for attribute in cl.globals.values(): # determine icon type if attribute.isPrivate(): iconID = Editor.AttributePrivateID elif attribute.isProtected(): iconID = Editor.AttributeProtectedID 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)]) else: completionsList.extend( ["{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: nword = sup else: nword = word completionsList.extend( self.__getDocumentCompletions( 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) while res: start, length = editor.getFoundTarget() pos = start + length if pos != currentPos: if context: completion = "" else: completion = word line, index = editor.lineIndexFromPosition(pos) curWord = editor.getWord(line, index, useWordChars=False) completion += curWord[len(completion):] if ( completion and completion not in completionsList and completion != word ): completionsList.append( "{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 (QScintilla.Editor) """ editor.addCallTipHook("Assistant", self.calltips) def __unsetCalltipsHook(self, editor): """ Private method to unset the calltip hook. @param editor reference to the editor (QScintilla.Editor) """ editor.removeCallTipHook("Assistant") def calltips(self, editor, pos, commas): """ Public method to return a list of calltips. @param editor reference to the editor (QScintilla.Editor) @param pos position in the text for the calltip (integer) @param commas minimum number of commas contained in the calltip (integer) @return list of possible calltips (list of strings) """ language = editor.getApiLanguage() completeFromDocumentOnly = False if language in ["", "Guessed"] or language.startswith("Pygments|"): 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))) text = editor.text(line) 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 [] ) if wseps: if col > 0 and beg[col - 1] in wseps: col -= 1 else: while col > 0 and beg[col - 1] not in wseps + [" ", "\t"]: col -= 1 if col >= 0: col -= 1 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) if api is not None: apiCalltips = self.__getApiCalltips( 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) if self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument: documentCalltips = self.__getDocumentCalltips( word, prefix, mod, editor) return list(sorted( 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 (APIsManager.DbAPIs) @param word function to get calltips for (string) @param commas minimum number of commas contained in the calltip (integer) @param prefix prefix of the word to be completed (string) @param module reference to the scanned module info (Module) @param editor reference to the editor object (QScintilla.Editor) @return list of calltips (list of string) """ calltips = [] 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) ): for superClass in cl.super: 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")) return calltips def __getDocumentCalltips(self, word, prefix, module, editor, doHierarchy=False): """ Private method to determine calltips from the document. @param word function to get calltips for (string) @param prefix prefix of the word to be completed (string) @param module reference to the scanned module info (Module) @param editor reference to the editor object (QScintilla.Editor) @keyparam doHierarchy flag indicating a hierarchical search (boolean) @return list of calltips (list of string) """ calltips = [] if module and bool(editor.getLexer()): if prefix: # prefix can be 'self', 'cls' or a class name sep = editor.getLexer().autoCompletionWordSeparators()[0] 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 word in cl.methods: method = cl.methods[word] 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:] ))) for sup in cl.super: calltips.extend(self.__getDocumentCalltips( word, sup, module, editor, doHierarchy=True)) break else: if prefix in module.classes: cl = module.classes[prefix] 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:]))) for sup in cl.super: 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))) 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:]))) return calltips # # eflag: noqa = M834, W605