PluginRefactoringRope.py

Tue, 10 Dec 2024 15:49:01 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 10 Dec 2024 15:49:01 +0100
branch
eric7
changeset 426
7592a1c052e8
parent 422
f98253eed7f0
permissions
-rw-r--r--

Updated copyright for 2025.

# -*- coding: utf-8 -*-

# Copyright (c) 2010 - 2025 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the Rope refactoring plugin.
"""

import contextlib
import os

from PyQt6.QtCore import QCoreApplication, QObject, Qt, QTranslator
from PyQt6.QtGui import QPalette

from eric7 import Preferences, Utilities
from eric7.EricWidgets.EricApplication import ericApp

try:
    from eric7.SystemUtilities.FileSystemUtilities import isRemoteFileName
except ImportError:
    from .RefactoringRope.RopeUtilities import isRemoteFileName

# Start-Of-Header
__header__ = {
    "name": "Refactoring Rope Plugin",
    "author": "Detlev Offenbach <detlev@die-offenbachs.de>",
    "autoactivate": True,
    "deactivateable": True,
    "version": "10.7.4",
    "className": "RefactoringRopePlugin",
    "packageName": "RefactoringRope",
    "shortDescription": "Refactoring and Code Assist using the Rope library.",
    "longDescription": (
        """This plug-in implements refactoring functionality using the Rope"""
        """ refactoring library. Additionally it implements an auto-completion,"""
        """ call-tips and code documentation provider as well as a mouse"""
        """ click handler."""
    ),
    "hasCompiledForms": True,
    "pyqtApi": 2,
    "doNotCompile": True,
}
# End-Of-Header

error = ""

refactoringRopePluginObject = None


def createAutoCompletionPage(configDlg):  # noqa: U100
    """
    Module function to create the autocompletion configuration page.

    @param configDlg reference to the configuration dialog
    @type ConfigurationWidget
    @return reference to the configuration page
    @rtype AutoCompletionRopePage
    """
    from RefactoringRope.ConfigurationPage.AutoCompletionRopePage import (
        AutoCompletionRopePage,
    )

    global refactoringRopePluginObject

    page = AutoCompletionRopePage(refactoringRopePluginObject)
    return page


def createCallTipsPage(configDlg):  # noqa: U100
    """
    Module function to create the calltips configuration page.

    @param configDlg reference to the configuration dialog
    @type ConfigurationWidget
    @return reference to the configuration page
    @rtype CallTipsRopePage
    """
    from RefactoringRope.ConfigurationPage.CallTipsRopePage import CallTipsRopePage

    global refactoringRopePluginObject

    page = CallTipsRopePage(refactoringRopePluginObject)
    return page


def createMouseClickHandlerPage(configDlg):  # noqa: U100
    """
    Module function to create the mouse click handler configuration page.

    @param configDlg reference to the configuration dialog
    @type ConfigurationWidget
    @return reference to the configuration page
    @rtype MouseClickHandlerRopePage
    """
    from RefactoringRope.ConfigurationPage.MouseClickHandlerRopePage import (
        MouseClickHandlerRopePage,
    )

    global refactoringRopePluginObject

    page = MouseClickHandlerRopePage(refactoringRopePluginObject)
    return page


def getConfigData():
    """
    Module function returning data as required by the configuration dialog.

    @return dictionary containing the relevant data
    @rtype dict
    """
    try:
        usesDarkPalette = ericApp().usesDarkPalette()
    except AttributeError:
        palette = ericApp().palette()
        lightness = palette.color(QPalette.Window).lightness()
        usesDarkPalette = lightness <= 128
    iconSuffix = "dark" if usesDarkPalette else "light"

    data = {
        "ropeAutoCompletionPage": [
            QCoreApplication.translate("RefactoringRopePlugin", "Rope"),
            os.path.join(
                "RefactoringRope",
                "ConfigurationPage",
                "preferences-refactoring-{0}".format(iconSuffix),
            ),
            createAutoCompletionPage,
            "1editorAutocompletionPage",
            None,
        ],
        "ropeCallTipsPage": [
            QCoreApplication.translate("RefactoringRopePlugin", "Rope"),
            os.path.join(
                "RefactoringRope",
                "ConfigurationPage",
                "preferences-refactoring-{0}".format(iconSuffix),
            ),
            createCallTipsPage,
            "1editorCalltipsPage",
            None,
        ],
        "ropeMouseClickHandlerPage": [
            QCoreApplication.translate("RefactoringRopePlugin", "Rope"),
            os.path.join(
                "RefactoringRope",
                "ConfigurationPage",
                "preferences-refactoring-{0}".format(iconSuffix),
            ),
            createMouseClickHandlerPage,
            "1editorMouseClickHandlers",
            None,
        ],
    }

    return data


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

        @param ui reference to the user interface object
        @type UserInterface
        """
        QObject.__init__(self, ui)
        self.__ui = ui
        self.__initialize()

        self.__defaults = {
            "CodeAssistEnabled": False,
            "MaxFixes": 10,
            "CodeAssistCalltipsEnabled": False,
            "CalltipsMaxFixes": 10,
            "MouseClickEnabled": True,
            "MouseClickGotoModifiers": (
                Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier
            ),
            "MouseClickGotoButton": Qt.MouseButton.LeftButton,
        }

        self.__translator = None
        self.__loadTranslator()

    def __initialize(self):
        """
        Private slot to (re)initialize the plugin.
        """
        self.__refactoringServer = None
        self.__codeAssistServer = None

        self.__editors = []

        self.__currentEditor = None
        self.__savedEditorName = None
        self.__oldEditorText = ""

    def activate(self):
        """
        Public method to activate this plugin.

        @return tuple of None and activation status
        @rtype tuple of (None, bool)
        """
        from RefactoringRope.CodeAssistServer import CodeAssistServer
        from RefactoringRope.RefactoringServer import RefactoringServer

        global refactoringRopePluginObject
        refactoringRopePluginObject = self

        ericApp().getObject("PluginManager").shutdown.connect(self.__shutdown)

        self.__codeAssistServer = CodeAssistServer(self, parent=self.__ui)
        self.__codeAssistServer.activate()

        self.__refactoringServer = RefactoringServer(self, parent=self.__ui)
        self.__refactoringServer.activate()

        ericApp().getObject("PluginManager").shutdown.connect(self.__shutdown)
        ericApp().getObject("ViewManager").editorOpenedEd.connect(self.__editorOpened)
        ericApp().getObject("ViewManager").editorClosedEd.connect(self.__editorClosed)

        for editor in ericApp().getObject("ViewManager").getOpenEditors():
            self.__editorOpened(editor)

        return None, True

    def deactivate(self):
        """
        Public method to deactivate this plugin.
        """
        if self.__refactoringServer:
            self.__refactoringServer.deactivate()

        if self.__codeAssistServer:
            self.__codeAssistServer.deactivate()

        ericApp().getObject("PluginManager").shutdown.disconnect(self.__shutdown)
        ericApp().getObject("ViewManager").editorOpenedEd.disconnect(
            self.__editorOpened
        )
        ericApp().getObject("ViewManager").editorClosedEd.disconnect(
            self.__editorClosed
        )

        for editor in self.__editors[:]:
            self.__editorClosed(editor)

        self.__initialize()

    def __shutdown(self):
        """
        Private slot handling the shutdown signal of the plug-in manager.
        """
        if self.__codeAssistServer:
            self.__codeAssistServer.deactivate()

    def __loadTranslator(self):
        """
        Private method to load the translation file.
        """
        if self.__ui is not None:
            loc = self.__ui.getLocale()
            if loc and loc != "C":
                locale_dir = os.path.join(
                    os.path.dirname(__file__), "RefactoringRope", "i18n"
                )
                translation = "rope_{0}".format(loc)
                translator = QTranslator(None)
                loaded = translator.load(translation, locale_dir)
                if loaded:
                    self.__translator = translator
                    ericApp().installTranslator(self.__translator)
                else:
                    print(
                        "Warning: translation file '{0}' could not"
                        " be loaded.".format(translation)
                    )
                    print("Using default.")

    def getPreferences(self, key):
        """
        Public method to retrieve the various refactoring settings.

        @param key the key of the value to get
        @type str
        @return the requested refactoring setting
        @rtype Any
        """
        if key in [
            "CodeAssistEnabled",
            "CodeAssistCalltipsEnabled",
            "MouseClickEnabled",
        ]:
            return Preferences.toBool(
                Preferences.Prefs.settings.value(
                    self.PreferencesKey + "/" + key, self.__defaults[key]
                )
            )
        elif key in ["CalltipsMaxFixes", "MaxFixes"]:
            return int(
                Preferences.Prefs.settings.value(
                    self.PreferencesKey + "/" + key, self.__defaults[key]
                )
            )
        else:
            return Preferences.Prefs.settings.value(
                self.PreferencesKey + "/" + key, self.__defaults[key]
            )

    def setPreferences(self, key, value):
        """
        Public method to store the various refactoring settings.

        @param key the key of the setting to be set
        @type str
        @param value the value to be set
        @type Any
        """
        Preferences.Prefs.settings.setValue(self.PreferencesKey + "/" + key, value)

        if key in ["MouseClickGotoModifiers", "MouseClickGotoButton"]:
            for editor in self.__editors:
                self.__disconnectMouseClickHandler(editor)
                if not isRemoteFileName(editor.getFileName()):
                    self.__connectMouseClickHandler(editor)

    def __determineLanguage(self):
        """
        Private method to determine the valid language strings.

        @return list of valid language strings
        @rtype list of str
        """
        return [
            "Python3",
            "MicroPython",
            "Pygments|Python",
            "Pygments|Python 2.x",
            "Cython",
        ]

    def __editorOpened(self, editor):
        """
        Private slot called, when a new editor was opened.

        @param editor reference to the new editor
        @type Editor
        """
        if not isRemoteFileName(editor.getFileName()):
            languages = self.__determineLanguage()

            if editor.getLanguage() in languages:
                self.__connectEditor(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
        @type Editor
        """
        if editor in self.__editors:
            editor.languageChanged.disconnect(self.__editorLanguageChanged)
            self.__disconnectEditor(editor)
            self.__editors.remove(editor)

    def __editorLanguageChanged(self, language):
        """
        Private slot to handle the language change of an editor.

        @param language programming language of the editor
        @type str
        """
        editor = self.sender()
        if not isRemoteFileName(editor.getFileName()):
            languages = self.__determineLanguage()

            self.__disconnectEditor(editor)
            if language in languages:
                self.__connectEditor(editor)

    def __connectEditor(self, editor):
        """
        Private method to connect an editor.

        @param editor reference to the editor
        @type Editor
        """
        editor.editorAboutToBeSaved.connect(self.__editorAboutToBeSaved)
        editor.editorSaved.connect(self.__editorSaved)

        self.__setAutoCompletionHook(editor)
        self.__setCalltipsHook(editor)

        self.__connectMouseClickHandler(editor)

    def __disconnectEditor(self, editor):
        """
        Private method to disconnect an editor.

        @param editor reference to the editor
        @type Editor
        """
        with contextlib.suppress(TypeError):
            editor.editorAboutToBeSaved.disconnect(self.__editorAboutToBeSaved)
            editor.editorSaved.disconnect(self.__editorSaved)

        self.__unsetAutoCompletionHook(editor)
        self.__unsetCalltipsHook(editor)

        self.__disconnectMouseClickHandler(editor)

    def __connectMouseClickHandler(self, editor):
        """
        Private method to connect the mouse click handler to an editor.

        @param editor reference to the editor
        @type Editor
        """
        if self.getPreferences("MouseClickGotoButton"):
            editor.setMouseClickHandler(
                "rope",
                self.getPreferences("MouseClickGotoModifiers"),
                self.getPreferences("MouseClickGotoButton"),
                self.__codeAssistServer.gotoDefinition,
            )

    def __disconnectMouseClickHandler(self, editor):
        """
        Private method to disconnect the mouse click handler from an editor.

        @param editor reference to the editor
        @type Editor
        """
        editor.removeMouseClickHandlers("rope")

    def __setAutoCompletionHook(self, editor):
        """
        Private method to set the autocompletion hook.

        @param editor reference to the editor
        @type Editor
        """
        editor.addCompletionListHook(
            "rope", self.__codeAssistServer.requestCompletions, True
        )

    def __unsetAutoCompletionHook(self, editor):
        """
        Private method to unset the autocompletion hook.

        @param editor reference to the editor
        @type Editor
        """
        editor.removeCompletionListHook("rope")

    def __setCalltipsHook(self, editor):
        """
        Private method to set the calltip hook.

        @param editor reference to the editor
        @type Editor
        """
        editor.addCallTipHook("rope", self.__codeAssistServer.getCallTips)

    def __unsetCalltipsHook(self, editor):
        """
        Private method to unset the calltip hook.

        @param editor reference to the editor
        @type Editor
        """
        editor.removeCallTipHook("rope")

    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
        @type str
        """
        if filename and os.path.exists(filename):
            try:
                self.__oldEditorText = Utilities.readEncodedFile(filename)[0]
            except OSError:
                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
        @type str
        """
        if filename == self.__savedEditorName and self.__oldEditorText:
            self.__refactoringServer.reportChanged(
                self.__savedEditorName, self.__oldEditorText
            )
            self.__codeAssistServer.reportChanged(
                self.__savedEditorName, self.__oldEditorText
            )
        else:
            self.__refactoringServer.reportChanged(filename, "")
            self.__codeAssistServer.reportChanged(filename, "")

    def getCodeAssistServer(self):
        """
        Public method to get a reference to the code assist server.

        @return reference to the code assist server
        @rtype CodeAssistServer
        """
        return self.__codeAssistServer


def installDependencies(pipInstall):
    """
    Function to install dependencies of this plug-in.

    @param pipInstall function to be called with a list of package names.
    @type function
    """
    try:
        import rope  # __IGNORE_WARNING__
    except ImportError:
        pipInstall(["rope"])


#
# eflag: noqa = M801, U200

eric ide

mercurial