Sun, 22 Mar 2015 17:37:55 +0100
Corrected a whitespace error.
# -*- coding: utf-8 -*- # Copyright (c) 2008 - 2015 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the eric assistant, an alternative autocompletion and calltips system. """ from __future__ import unicode_literals import re import imp from PyQt5.QtCore import QRegExp, QObject from E5Gui.E5Application import e5App from .APIsManager import APIsManager, ApisNameProject import Preferences AcsAPIs = 0x0001 AcsDocument = 0x0002 AcsProject = 0x0004 AcsOther = 0x1000 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.__completingContext = False 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) if enabled: 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.getLanguage() if language == "": return self.__apisManager.getAPIs(language) 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.autoCompletionHook() == self.autocomplete: self.__unsetAutoCompletionHook(editor) if editor.callTipHook() == self.calltips: self.__unsetCalltipsHook(editor) def __preferencesChanged(self): """ Private method to handle a change of the global configuration. """ self.__apisManager.reloadAPIs() def __getCharacter(self, pos, editor): """ Private method to get the character to the left of the current position in the current line. @param pos position to get character at (integer) @param editor reference to the editor object to work with (QScintilla.Editor) @return requested character or "", if there are no more (string) and the next position (i.e. pos - 1) """ if pos <= 0: return "", pos pos -= 1 ch = editor.charAt(pos) # Don't go past the end of the previous line if ch == '\n' or ch == '\r': return "", pos return ch, pos ################################# ## autocompletion methods below ################################# def __completionListSelected(self, id, txt): """ Private slot to handle the selection from the completion list. @param id the ID of the user list (should be 1) (integer) @param txt the selected text (string) """ from QScintilla.Editor import EditorAutoCompletionListID editor = self.sender() if id == EditorAutoCompletionListID: lst = txt.split() if len(lst) > 1: txt = lst[0] self.__lastFullContext = lst[1][1:].split(")")[0] else: self.__lastFullContext = None if Preferences.getEditor("AutoCompletionReplaceWord"): editor.selectCurrentWord() editor.removeSelectedText() line, col = editor.getCursorPosition() else: line, col = editor.getCursorPosition() wLeft = editor.getWordLeft(line, col) if not txt.startswith(wLeft): editor.selectCurrentWord() editor.removeSelectedText() line, col = editor.getCursorPosition() elif wLeft: txt = txt[len(wLeft):] editor.insert(txt) editor.setCursorPosition(line, col + len(txt)) def __setAutoCompletionHook(self, editor): """ Private method to set the autocompletion hook. @param editor reference to the editor (QScintilla.Editor) """ editor.userListActivated.connect(self.__completionListSelected) editor.setAutoCompletionHook(self.autocomplete) def __unsetAutoCompletionHook(self, editor): """ Private method to unset the autocompletion hook. @param editor reference to the editor (QScintilla.Editor) """ editor.unsetAutoCompletionHook() editor.userListActivated.disconnect(self.__completionListSelected) def autocomplete(self, editor, context): """ Public method to determine the autocompletion proposals. @param editor reference to the editor object, that called this method (QScintilla.Editor) @param context flag indicating to autocomplete a context (boolean) """ from QScintilla.Editor import EditorAutoCompletionListID if editor.isListActive(): editor.cancelList() completionsList = self.getCompletionsList(editor, context) if len(completionsList) > 0: completionsList.sort() editor.showUserList(EditorAutoCompletionListID, completionsList) 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.getLanguage() if language == "": return [] line, col = editor.getCursorPosition() self.__completingContext = context sep = "" if 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 if context: beg = beg[:col + 1] else: beg = editor.text(line)[:col] col = len(beg) wsep = editor.getLexer().autoCompletionWordSeparators() if wsep: wsep.append(" ") if col > 0 and beg[col - 1] in wsep: col -= 1 else: while col > 0 and beg[col - 1] not in wsep: col -= 1 if col > 0 and beg[col - 1] != " ": col -= 1 prefix = editor.getWordLeft(line, col) if editor.isPy2File() or editor.isPy3File(): 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.isPy2File() or editor.isPy3File(): # 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 wsep = editor.getLexer().autoCompletionWordSeparators() while col >= 0 and prefix[col] not in wsep: 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, mod, editor, importCompletion, sep) if len(completionsList) == 0 and prefix: # searching with prefix didn't return anything, try without completionsList = self.__getCompletions( word, context, "", language, mod, editor, importCompletion, sep) return completionsList return [] def __getCompletions(self, word, context, prefix, language, module, editor, importCompletion, 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 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 sep separator string (string) @return list of possible completions (list of strings) """ apiCompletionsList = [] docCompletionsList = [] projectCompletionList = [] if self.__plugin.getPreferences("AutoCompletionSource") & AcsAPIs: api = self.__apisManager.getAPIs(language) 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 super in cl.super: if prefix == word: completions.extend( api.getCompletions( context=super, followHierarchy= self.__plugin.getPreferences( "AutoCompletionFollowHierarchy" ) ) ) else: completions.extend( api.getCompletions( start=word, context=super, 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.setCallTipHook(self.calltips) def __unsetCalltipsHook(self, editor): """ Private method to unset the calltip hook. @param editor reference to the editor (QScintilla.Editor) """ editor.unsetCallTipHook() 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.getLanguage() if language == "": return 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) wsep = editor.getLexer().autoCompletionWordSeparators() if wsep: if col > 0 and beg[col - 1] in wsep: col -= 1 else: while col > 0 and beg[col - 1] not in wsep + [" ", "\t"]: col -= 1 if col >= 0: col -= 1 prefix = editor.getWordLeft(line, col) if editor.isPy2File() or editor.isPy3File(): 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 self.__plugin.getPreferences("AutoCompletionSource") & AcsAPIs: api = self.__apisManager.getAPIs(language) 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 super in cl.super: calltips.extend(api.getCalltips( word, commas, super, 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: 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