AssistantEric/Assistant.py

Fri, 31 Dec 2010 15:51:08 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 31 Dec 2010 15:51:08 +0100
changeset 25
6a68405feb84
parent 19
7eb775bb326b
child 30
8f4d794d8ee0
permissions
-rw-r--r--

Updated copyright notice.

# -*- 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

from PyQt4.QtCore import *

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, newStyle, parent = None):
        """
        Constructor
        
        @param plugin reference to the plugin object
        @param newStyle flag indicating usage of new style signals (bool)
        @param parent parent (QObject)
        """
        QObject.__init__(self, parent)
        
        self.__plugin = plugin
        self.__newStyle = newStyle
        self.__ui = parent
        self.__project = e5App().getObject("Project")
        self.__viewmanager = e5App().getObject("ViewManager")
        self.__pluginManager = e5App().getObject("PluginManager")
        
        self.__apisManager = APIsManager(self.__newStyle, 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.
        """
        if self.__newStyle:
            self.__pluginManager.shutdown.connect(self.__shutdown)
            
            self.__ui.preferencesChanged.connect(self.__preferencesChanged)
            
            self.__viewmanager.editorOpenedEd.connect(self.__editorOpened)
            self.__viewmanager.editorClosedEd.connect(self.__editorClosed)
        else:
            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.
        """
        if self.__newStyle:
            self.__pluginManager.shutdown.disconnect(self.__shutdown)
            
            self.__ui.preferencesChanged.disconnect(self.__preferencesChanged)
            
            self.__viewmanager.editorOpenedEd.disconnect(self.__editorOpened)
            self.__viewmanager.editorClosedEd.disconnect(self.__editorClosed)
        else:
            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)
        if self.__newStyle:
            editor.editorSaved.connect(
                self.__apisManager.getAPIs(ApisNameProject).editorSaved)
        else:
            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:
            if self.__newStyle:
                editor.editorSaved.disconnect(
                    self.__apisManager.getAPIs(ApisNameProject).editorSaved)
            else:
                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)
        """
        if self.__newStyle:
            editor.userListActivated.connect(self.__completionListSelected)
        else:
            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()
        if self.__newStyle:
            editor.userListActivated.disconnect(self.__completionListSelected)
        else:
            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 = re.sub("\w", "", 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 \
                  (not text[col - 1].isalnum() or \
                   (wc 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
        
        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())
        text = editor.text(line)
        while col > 0 and \
              (not text[col - 1].isalnum() or \
               (wc 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