Fri, 31 Dec 2010 15:51:08 +0100
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)))