diff -r 3a4123edc944 -r 89cbc07f4bf0 AssistantEric/Assistant.py --- /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)))