AssistantEric/Assistant.py

changeset 32
68ef15fe34c3
parent 30
8f4d794d8ee0
child 35
6b706b02c2dd
--- a/AssistantEric/Assistant.py	Sat Jul 30 10:55:32 2011 +0200
+++ b/AssistantEric/Assistant.py	Sun Sep 25 17:18:48 2011 +0200
@@ -9,6 +9,7 @@
 """
 
 import re
+import imp
 
 from PyQt4.QtCore import QRegExp, QObject
 
@@ -19,6 +20,7 @@
 from QScintilla.Editor import Editor
 
 import Preferences
+from Utilities.ModuleParser import Module
 
 AcsAPIs = 0x0001
 AcsDocument = 0x0002
@@ -250,6 +252,7 @@
         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]
@@ -260,8 +263,7 @@
             
             depth = 0
             while col > 0 and \
-                  (not text[col - 1].isalnum() or \
-                   (wc and text[col - 1] not in wc)):
+                  not pat.match(text[col - 1]):
                 ch = text[col - 1]
                 if ch == ')':
                     depth = 1
@@ -288,18 +290,45 @@
         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 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)
+        
         if word:
             if self.__plugin.getPreferences("AutoCompletionSource") & AcsAPIs:
                 api = self.__apisManager.getAPIs(language)
-                apiCompletionsList = self.__getApiCompletions(api, word, context)
+                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)
+                projectCompletionList = self.__getApiCompletions(
+                    api, word, context, prefix, mod, editor)
             
             if self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument:
-                docCompletionsList = \
-                    self.getCompletionsFromDocument(editor, word, context, sep)
+                docCompletionsList = self.getCompletionsFromDocument(
+                    editor, word, context, sep, prefix, mod)
             
             completionsList = list(
                 set(apiCompletionsList)
@@ -311,18 +340,59 @@
                 completionsList.sort()
                 editor.showUserList(EditorAutoCompletionListID, completionsList)
 
-    def __getApiCompletions(self, api, word, context):
+    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 context:
+            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))
+                            else:
+                                completions.extend(
+                                    api.getCompletions(start=word, context=super))
+                        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"]
@@ -357,7 +427,7 @@
                         completionsList.append(entry)
         return completionsList
     
-    def getCompletionsFromDocument(self, editor, word, context, sep):
+    def getCompletionsFromDocument(self, editor, word, context, sep, prefix, module):
         """
         Public method to determine autocompletion proposals from the document.
         
@@ -365,36 +435,136 @@
         @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)
         @return list of possible completions (list of strings)
         """
-        currentPos = editor.currentPosition()
         completionsList = []
-        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))
+        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, iconID))
+                            else:
+                                # eric 5.1 cannot differentiate method types
+                                comps.append((method.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.AttributePrivateID
+                                comps.append((attribute.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.AttributePrivateID
+                            comps.append((attribute.name, iconID))
+                        
+                        if word != prefix:
+                            completionsList.extend(
+                                ["{0}?{1}".format(c[0], c[1])
+                                 for c in comps if c[0].startswith(word)])
+                        else:
+                            completionsList.extend(
+                                ["{0}?{1}".format(c[0], c[1])
+                                 for c in comps])
+                        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 hasattr(method, "modifier"):
+                            # eric 5.1 cannot differentiate method types
+                            continue
+                        if method.modifier in [method.Class, method.Static]:
+                            # determine icon type
+                            if method.isPrivate():
+                                iconID = Editor.MethodPrivateID
+                            elif method.isProtected():
+                                iconID = Editor.MethodProtectedID
+                            else:
+                                iconID = Editor.MethodID
+                            comps.append((method.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.AttributePrivateID
+                        comps.append((attribute.name, iconID))
+                    
+                    if word != prefix:
+                        completionsList.extend(
+                            ["{0}?{1}".format(c[0], c[1])
+                             for c in comps if c[0].startswith(word)])
+                    else:
+                        completionsList.extend(
+                            ["{0}?{1}".format(c[0], c[1])
+                             for c in comps])
+        
+        if not prefixFound:
+            currentPos = editor.currentPosition()
+            if context:
+                word += sep
             
-            res = editor.findNextTarget()
+            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
@@ -434,27 +604,154 @@
         
         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 text[col - 1].isalnum() or \
-               (wc and text[col - 1] not in wc)):
+              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 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 = api.getCalltips(word, commas, self.__lastContext,
-                    self.__lastFullContext,
-                    self.__plugin.getPreferences("CallTipsContextShown"))
+                apiCalltips = self.__getApiCalltips(
+                    api, word, commas, prefix, mod, editor)
         
         if self.__plugin.getPreferences("AutoCompletionSource") & AcsProject:
             api = self.__apisManager.getAPIs(ApisNameProject)
-            projectCalltips = api.getCalltips(word, commas, self.__lastContext,
+            projectCalltips = self.__getApiCalltips(
+                api, word, commas, prefix, mod, editor)
+        
+        if self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument:
+            documentCalltips = self.__getCalltipsFromDocument(
+                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")))
+                    break
+        else:
+            calltips = api.getCalltips(word, commas, self.__lastContext,
                 self.__lastFullContext,
                 self.__plugin.getPreferences("CallTipsContextShown"))
         
-        return sorted(set(apiCalltips).union(set(projectCalltips)))
+        return calltips
+    
+    def __getCalltipsFromDocument(self, word, prefix, module, editor):
+        """
+        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)
+        @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:])))
+                            break
+                else:
+                    if prefix in module.classes:
+                        cl = module.classes[prefix]
+                        if word in cl.methods:
+                            method = cl.methods[word]
+                            if 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:])))
+            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[1:])))
+                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

eric ide

mercurial