--- a/PluginRefactoringRope.py Sat Feb 28 12:33:32 2015 +0100 +++ b/PluginRefactoringRope.py Sat Feb 28 15:09:53 2015 +0100 @@ -12,10 +12,13 @@ import os import sys -from PyQt5.QtCore import QObject, QTranslator +from PyQt5.QtCore import QObject, QTranslator, QCoreApplication, QTimer from E5Gui.E5Application import e5App +import Preferences +import Utilities + from Preferences.Shortcuts import readShortcuts # Start-Of-Header @@ -23,14 +26,16 @@ author = "Detlev Offenbach <detlev@die-offenbachs.de>" autoactivate = True deactivateable = True -version = "3.1.0" +version = "4.0.0" className = "RefactoringRopePlugin" packageName = "RefactoringRope" internalPackages = "rope" shortDescription = "Refactoring using the Rope library." -longDescription = """This plugin implements refactoring functionality""" \ - """ using the Rope refactoring library. Only refactoring in the same""" \ - """ Python version as Eric is running is allowed.""" +longDescription = """This plug-in implements refactoring functionality""" \ + """ using the Rope refactoring library. Additonally it implements an """ \ + """ alternative auto-completion and calltips provider. Only""" \ + """ refactoring, completions and calltips in the same Python variant""" \ + """ as Eric is running is allowed.""" pyqtApi = 2 doNotCompile = True python2Compatible = True @@ -38,11 +43,70 @@ error = "" +refactoringRopePluginObject = None + + +def createAutoCompletionPage(configDlg): + """ + Module function to create the autocompletion configuration page. + + @param configDlg reference to the configuration dialog + @return reference to the configuration page + """ + global refactoringRopePluginObject + from RefactoringRope.ConfigurationPage.AutoCompletionRopePage \ + import AutoCompletionRopePage + page = AutoCompletionRopePage(refactoringRopePluginObject) + return page + + +def createCallTipsPage(configDlg): + """ + Module function to create the calltips configuration page. + + @param configDlg reference to the configuration dialog + @return reference to the configuration page + """ + global refactoringRopePluginObject + from RefactoringRope.ConfigurationPage.CallTipsRopePage \ + import CallTipsRopePage + page = CallTipsRopePage(refactoringRopePluginObject) + return page + + +def getConfigData(): + """ + Module function returning data as required by the configuration dialog. + + @return dictionary containing the relevant data + """ + return { + "ropeAutoCompletionPage" : [ + QCoreApplication.translate("RefactoringRopePlugin", "Rope"), + os.path.join("RefactoringRope", "ConfigurationPage", + "preferences-refactoring.png"), + createAutoCompletionPage, "editorAutocompletionPage", None], + "ropeCallTipsPage" : \ + [QCoreApplication.translate("RefactoringRopePlugin", "Rope"), + os.path.join("RefactoringRope", "ConfigurationPage", + "preferences-refactoring.png"), + createCallTipsPage, "editorCalltipsPage", None], + } + + +def prepareUninstall(): + """ + Module function to prepare for an uninstallation. + """ + Preferences.Prefs.settings.remove(RefactoringRopePlugin.PreferencesKey) + class RefactoringRopePlugin(QObject): """ Class implementing the Rope refactoring plugin. """ + PreferencesKey = "RefactoringRope" + def __init__(self, ui): """ Constructor @@ -53,8 +117,23 @@ self.__ui = ui self.__initialize() + self.__defaults = { + "CodeAssistEnabled" : False, + "MaxFixes" : 10, + "CodeAssistTimeout" : 100, + "ShowQScintillaCompletions": True, + + "CodeAssistCalltipsEnabled" : False, + "CalltipsMaxFixes" : 10, + } + self.__translator = None self.__loadTranslator() + + self.__acTimer = QTimer(self) + self.__acTimer.setSingleShot(True) + self.__acTimer.setInterval(self.getPreferences("CodeAssistTimeout")) + self.__acTimer.timeout.connect(self.__codeAssist) def __initialize(self): """ @@ -65,6 +144,12 @@ self.__mainAct = None self.__mainMenu = None self.__projectIsOpen = False + + self.__editors = [] + + self.__currentEditor = None + self.__savedEditorName = None + self.__oldEditorText = "" def __checkUiVersion(self): """ @@ -91,6 +176,9 @@ if not self.__checkUiVersion(): return None, False + global refactoringRopePluginObject + refactoringRopePluginObject = self + from RefactoringRope.Refactoring import Refactoring self.__object = Refactoring(self, self.__ui) @@ -113,6 +201,11 @@ if self.__projectIsOpen: self.__object.projectOpened() + e5App().getObject("ViewManager").editorOpenedEd.connect( + self.__editorOpened) + e5App().getObject("ViewManager").editorClosedEd.connect( + self.__editorClosed) + e5App().getObject("Project").projectOpened.connect( self.__object.projectOpened) e5App().getObject("Project").projectPropertiesChanged.connect( @@ -131,6 +224,10 @@ e5App().getObject("Project").newProject.connect( self.__projectOpened) + if e5App().getObject("Project").isOpen(): + for editor in e5App().getObject("ViewManager").getOpenEditors(): + self.__editorOpened(editor) + return None, True def deactivate(self): @@ -139,6 +236,11 @@ """ e5App().unregisterPluginObject("RefactoringRope") + e5App().getObject("ViewManager").editorOpenedEd.disconnect( + self.__editorOpened) + e5App().getObject("ViewManager").editorClosedEd.disconnect( + self.__editorClosed) + e5App().getObject("Project").projectOpened.disconnect( self.__object.projectOpened) e5App().getObject("Project").projectPropertiesChanged.disconnect( @@ -159,6 +261,9 @@ self.__ui.menuBar().removeAction(self.__mainAct) + for editor in self.__editors[:]: + self.__editorClosed(editor) + self.__initialize() def __loadTranslator(self): @@ -182,9 +287,51 @@ " be loaded.".format(translation)) print("Using default.") - def __projectOpened(self): + def getPreferences(self, key): + """ + Public method to retrieve the various refactoring settings. + + @param key the key of the value to get + @param prefClass preferences class used as the storage area + @return the requested refactoring setting + """ + if key in ["CodeAssistEnabled", "CodeAssistCalltipsEnabled", + "ShowQScintillaCompletions"]: + return Preferences.toBool(Preferences.Prefs.settings.value( + self.PreferencesKey + "/" + key, self.__defaults[key])) + else: + return int(Preferences.Prefs.settings.value( + self.PreferencesKey + "/" + key, self.__defaults[key])) + + def setPreferences(self, key, value): """ - Private slot to handle the projectOpened signal. + Public method to store the various refactoring settings. + + @param key the key of the setting to be set (string) + @param value the value to be set + @param prefClass preferences class used as the storage area + """ + Preferences.Prefs.settings.setValue( + self.PreferencesKey + "/" + key, value) + + if key in ["CodeAssistEnabled", "CodeAssistCalltipsEnabled"]: + if value: + if e5App().getObject("Project").isOpen(): + for editor in e5App().getObject("ViewManager")\ + .getOpenEditors(): + if editor not in self.__editors: + self.__editorOpened(editor) + else: + for editor in self.__editors[:]: + self.__editorClosed(editor) + elif key == "CodeAssistTimeout": + self.__acTimer.setInterval(value) + + def __determineLanguage(self): + """ + Private method to determine the valid language strings. + + @return list of valid language strings (list of string) """ if sys.version_info[0] == 3: lang = ["Python3"] @@ -193,9 +340,26 @@ else: lang = [] + return lang + + def __projectOpened(self): + """ + Private slot to handle the projectOpened signal. + """ + lang = self.__determineLanguage() + enabled = e5App().getObject("Project").getProjectLanguage() in lang self.__mainAct.setEnabled(enabled) self.__projectIsOpen = enabled + + for editor in self.__editors: + if editor.getLanguage() in lang and \ + self.__projectIsOpen: + self.__connectEditorSignals(editor) + if self.getPreferences("CodeAssistEnabled"): + self.__setAutoCompletionHook(editor) + if self.getPreferences("CodeAssistCalltipsEnabled"): + self.__setCalltipsHook(editor) def __projectClosed(self): """ @@ -203,3 +367,230 @@ """ self.__mainAct.setEnabled(False) self.__projectIsOpen = False + + for editor in self.__editors: + self.__disconnectEditorSignals(editor) + if editor.autoCompletionHook() == self.codeAssist: + self.__unsetAutoCompletionHook(editor) + if editor.callTipHook() == self.codeAssistCallTip: + self.__unsetCalltipsHook(editor) + + def __editorOpened(self, editor): + """ + Private slot called, when a new editor was opened. + + @param editor reference to the new editor (QScintilla.Editor) + """ + lang = self.__determineLanguage() + + if editor.getLanguage() in lang and self.__projectIsOpen: + self.__connectEditorSignals(editor) + if self.getPreferences("CodeAssistEnabled"): + self.__setAutoCompletionHook(editor) + if self.getPreferences("CodeAssistCalltipsEnabled"): + self.__setCalltipsHook(editor) + + editor.languageChanged.connect(self.__editorLanguageChanged) + self.__editors.append(editor) + + 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: + editor.languageChanged.disconnect(self.__editorLanguageChanged) + self.__disconnectEditorSignals(editor) + self.__editors.remove(editor) + if editor.autoCompletionHook() == self.codeAssist: + self.__unsetAutoCompletionHook(editor) + if editor.callTipHook() == self.codeAssistCallTip: + self.__unsetCalltipsHook(editor) + + def __editorLanguageChanged(self, language): + """ + Private slot to handle the language change of an editor. + + @param language programming language of the editor (string) + """ + editor = self.sender() + lang = self.__determineLanguage() + + if language in lang and self.__projectIsOpen: + self.__connectEditorSignals(editor) + if self.getPreferences("CodeAssistEnabled"): + self.__setAutoCompletionHook(editor) + if self.getPreferences("CodeAssistCalltipsEnabled"): + self.__setCalltipsHook(editor) + else: + self.__disconnectEditorSignals(editor) + if editor.autoCompletionHook() == self.codeAssist: + self.__unsetAutoCompletionHook(editor) + if editor.callTipHook() == self.codeAssistCallTip: + self.__unsetCalltipsHook(editor) + + def __connectEditorSignals(self, editor): + """ + Private method to connect to some signals of an editor. + + @param editor reference to the editor (QScintilla.Editor) + """ + editor.editorAboutToBeSaved.connect(self.__editorAboutToBeSaved) + editor.editorSaved.connect(self.__editorSaved) + + def __disconnectEditorSignals(self, editor): + """ + Private method to disconnect to some signals of an editor. + + @param editor reference to the editor (QScintilla.Editor) + """ + editor.editorAboutToBeSaved.disconnect(self.__editorAboutToBeSaved) + editor.editorSaved.disconnect(self.__editorSaved) + + 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 (QString) + """ + from QScintilla.Editor import EditorAutoCompletionListID + + editor = self.sender() + if id == EditorAutoCompletionListID: + lst = txt.split() + if len(lst) > 1: + txt = lst[0] + + 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) + """ + editor.userListActivated.connect(self.__completionListSelected) + editor.setAutoCompletionHook(self.codeAssist) + + def __unsetAutoCompletionHook(self, editor): + """ + Private method to unset the autocompletion hook. + + @param editor reference to the editor (QScintilla.Editor) + """ + editor.unsetAutoCompletionHook() + editor.userListActivated.disconnect(self.__completionListSelected) + + def codeAssist(self, editor, context = False): + """ + 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) + """ + self.__currentEditor = editor + if self.getPreferences("CodeAssistTimeout"): + self.__acTimer.stop() + self.__acTimer.start() + else: + self.__codeAssist() + + def __codeAssist(self): + """ + Private slot to show a list with completion proposals. + """ + from RefactoringRope.CodeAssist import CodeAssist + from QScintilla.Editor import EditorAutoCompletionListID + + if self.__currentEditor is not None: + if self.__currentEditor.isListActive(): + self.__currentEditor.cancelList() + ca = CodeAssist(self.__object, self.__currentEditor, self, self) + completions = ca.getCompletions() + if len(completions) == 0 and \ + self.getPreferences("ShowQScintillaCompletions"): + # try QScintilla autocompletion + self.__currentEditor.autoCompleteQScintilla() + else: + completions.sort() + self.__currentEditor.showUserList(EditorAutoCompletionListID, + completions) + + def __editorAboutToBeSaved(self, filename): + """ + Private slot to get the old contents of the named file. + + @param filename name of the file about to be saved (string) + """ + if filename and os.path.exists(filename): + try: + self.__oldEditorText = Utilities.readEncodedFile(filename)[0] + except IOError: + self.__oldEditorText = "" + self.__savedEditorName = filename + else: + self.__savedEditorName = "" + self.__oldEditorText = "" + + def __editorSaved(self, filename): + """ + Private slot to activate SOA. + + @param filename name of the file that was saved (string) + """ + import rope.base.libutils + + if filename == self.__savedEditorName and self.__oldEditorText: + rope.base.libutils.report_change(self.__object.getProject(), + self.__savedEditorName, self.__oldEditorText) + elif self.__savedEditorName == "": + rope.base.libutils.report_change(self.__object.getProject(), + filename, "") + + def __setCalltipsHook(self, editor): + """ + Private method to set the calltip hook. + + @param editor reference to the editor (QScintilla.Editor) + """ + editor.setCallTipHook(self.codeAssistCallTip) + + def __unsetCalltipsHook(self, editor): + """ + Private method to unset the calltip hook. + + @param editor reference to the editor (QScintilla.Editor) + """ + editor.unsetCallTipHook() + + def codeAssistCallTip(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) + """ + from RefactoringRope.CodeAssist import CodeAssist + ca = CodeAssist(self.__object, editor, self, self) + cts = ca.getCallTips(pos) + return cts