Sun, 02 Oct 2011 18:18:43 +0200
Added Python from import completion function.
# -*- coding: utf-8 -*- # Copyright (c) 2008 - 2011 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the eric assistant, an alternative autocompletion and calltips system. """ import re import imp from PyQt4.QtCore import QRegExp, QObject from E5Gui.E5Application import e5App from .APIsManager import APIsManager, ApisNameProject from QScintilla.Editor import Editor import Preferences from Utilities.ModuleParser import Module 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) self.__editors = [] self.__completingContext = False self.__lastContext = None self.__lastFullContext = None 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() language = editor.getLanguage() if language == "": return line, col = editor.getCursorPosition() self.__completingContext = context apiCompletionsList = [] docCompletionsList = [] projectCompletionList = [] 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: 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: col -= 1 prefix = editor.getWordLeft(line, col) if editor.isPy2File() or editor.isPy3File(): 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: if self.__plugin.getPreferences("AutoCompletionSource") & AcsAPIs: api = self.__apisManager.getAPIs(language) apiCompletionsList = self.__getApiCompletions( api, word, context, prefix, mod, editor) if self.__plugin.getPreferences("AutoCompletionSource") & AcsProject: api = self.__apisManager.getAPIs(ApisNameProject) projectCompletionList = self.__getApiCompletions( api, word, context, prefix, mod, editor) if self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument and \ not importCompletion: docCompletionsList = self.__getDocumentCompletions( editor, word, context, sep, prefix, mod) completionsList = list( set(apiCompletionsList) .union(set(docCompletionsList)) .union(set(projectCompletionList)) ) if len(completionsList) > 0: completionsList.sort() editor.showUserList(EditorAutoCompletionListID, 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) @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=True)) else: completions.extend( api.getCompletions(start=word, context=super, followHierarchy=True)) 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) else: 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) @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: 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 hasattr(method, "modifier"): if (prefix == "cls" and \ method.modifier == method.Class) or \ prefix == "self": comps.append((method.name, cl.name, iconID)) else: # eric 5.1 cannot differentiate method types 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 not doHierarchy and not hasattr(method, "modifier"): # eric 5.1 cannot differentiate method types 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: 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(): 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 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=True)) 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 hasattr(method, "modifier"): 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:]))) else: # eric 5.1 cannot differentiate method types 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 \ (hasattr(method, "modifier") and \ method.modifier == method.Class): # only eric 5.2 and newer can differentiate method types 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