PluginRefactoringRope.py

changeset 100
2bfe9e3fad8d
parent 99
e21a49043f31
child 101
5098ad8960ed
diff -r e21a49043f31 -r 2bfe9e3fad8d PluginRefactoringRope.py
--- 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

eric ide

mercurial