AssistantEric/Assistant.py

changeset 2
89cbc07f4bf0
child 5
e1a56c9a9c9d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AssistantEric/Assistant.py	Sun Jan 17 19:22:18 2010 +0000
@@ -0,0 +1,461 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2008 - 2010 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the eric assistant, an alternative autocompletion and
+calltips system.
+"""
+
+from PyQt4.QtCore import *
+from PyQt4.Qsci import QsciScintilla
+
+from E5Gui.E5Application import e5App
+
+from .APIsManager import APIsManager, ApisNameProject
+
+from QScintilla.Editor import Editor
+
+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)
+        
+        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.connect(self.__pluginManager, SIGNAL("shutdown()"), 
+                     self.__shutdown)
+        
+        self.connect(self.__ui, SIGNAL('preferencesChanged'), 
+                     self.__preferencesChanged)
+        
+        self.connect(self.__viewmanager, SIGNAL("editorOpenedEd"), 
+                     self.__editorOpened)
+        self.connect(self.__viewmanager, SIGNAL("editorClosedEd"), 
+                     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.disconnect(self.__pluginManager, SIGNAL("shutdown()"), 
+                        self.__shutdown)
+        
+        self.disconnect(self.__ui, SIGNAL('preferencesChanged'), 
+                        self.__preferencesChanged)
+        
+        self.disconnect(self.__viewmanager, SIGNAL("editorOpenedEd"), 
+                        self.__editorOpened)
+        self.disconnect(self.__viewmanager, SIGNAL("editorClosedEd"), 
+                        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)
+        self.connect(editor, SIGNAL("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.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:
+            self.disconnect(editor, SIGNAL("editorSaved"), 
+                            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)
+        """
+        self.connect(editor, SIGNAL('userListActivated(int, const QString)'),
+                     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()
+        self.disconnect(editor, SIGNAL('userListActivated(int, const QString)'),
+                        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 = editor.wordCharacters()
+            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 text[col - 1] not in wc:
+                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:
+            self.__lastContext = word
+        else:
+            self.__lastContext = None
+        
+        if word:
+            if self.__plugin.getPreferences("AutoCompletionSource") & AcsAPIs:
+                api = self.__apisManager.getAPIs(language)
+                apiCompletionsList = self.__getApiCompletions(api, word, context)
+            
+            if self.__plugin.getPreferences("AutoCompletionSource") & AcsProject:
+                api = self.__apisManager.getAPIs(ApisNameProject)
+                projectCompletionList = self.__getApiCompletions(api, word, context)
+            
+            if self.__plugin.getPreferences("AutoCompletionSource") & AcsDocument:
+                docCompletionsList = \
+                    self.getCompletionsFromDocument(editor, word, context, sep)
+            
+            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):
+        """
+        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)
+        @return list of possible completions (list of strings)
+        """
+        completionsList = []
+        if api is not None:
+            if 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:
+                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 getCompletionsFromDocument(self, editor, word, context, sep):
+        """
+        Public 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)
+        @return list of possible completions (list of strings)
+        """
+        currentPos = editor.currentPosition()
+        completionsList = []
+        if context:
+            word += sep
+        
+        res = editor.findFirstTarget(word, 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
+                ch = editor.charAt(pos)
+                while editor.isWordCharacter(ch):
+                    completion += ch
+                    pos += 1
+                    ch = editor.charAt(pos)
+                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 = editor.wordCharacters()
+        text = editor.text(line)
+        while col > 0 and text[col - 1] not in wc:
+            col -= 1
+        word = editor.getWordLeft(line, col)
+        
+        apiCalltips = []
+        projectCalltips = []
+        
+        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"))
+        
+        if self.__plugin.getPreferences("AutoCompletionSource") & AcsProject:
+            api = self.__apisManager.getAPIs(ApisNameProject)
+            projectCalltips = api.getCalltips(word, commas, self.__lastContext, 
+                self.__lastFullContext, 
+                self.__plugin.getPreferences("CallTipsContextShown"))
+        
+        return sorted(set(apiCalltips).union(set(projectCalltips)))

eric ide

mercurial