Started implementing the Jedi refactoring interface. eric7

Mon, 04 Oct 2021 19:59:06 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 04 Oct 2021 19:59:06 +0200
branch
eric7
changeset 8666
3a62b4009df9
parent 8664
5aa1070daa48
child 8667
99dfcefcc4d8

Started implementing the Jedi refactoring interface.

eric7.epj file | annotate | diff | comparison | revisions
eric7/JediInterface/AssistantJedi.py file | annotate | diff | comparison | revisions
eric7/JediInterface/JediClient.py file | annotate | diff | comparison | revisions
eric7/JediInterface/JediServer.py file | annotate | diff | comparison | revisions
eric7/JediInterface/RefactoringPreviewDialog.py file | annotate | diff | comparison | revisions
eric7/JediInterface/RefactoringPreviewDialog.ui file | annotate | diff | comparison | revisions
--- a/eric7.epj	Sun Oct 03 18:36:41 2021 +0200
+++ b/eric7.epj	Mon Oct 04 19:59:06 2021 +0200
@@ -713,7 +713,8 @@
       "eric7/Preferences/ConfigurationPages/EditorMouseClickHandlerJediPage.ui",
       "eric7/VirtualEnv/VirtualenvManagerWidget.ui",
       "eric7/UI/FindFileWidget.ui",
-      "eric7/UI/FindLocationWidget.ui"
+      "eric7/UI/FindLocationWidget.ui",
+      "eric7/JediInterface/RefactoringPreviewDialog.ui"
     ],
     "HASH": "df7daa8781250f7664e6ecaeaf1361fa2efd39ee",
     "IDLPARAMS": {
--- a/eric7/JediInterface/AssistantJedi.py	Sun Oct 03 18:36:41 2021 +0200
+++ b/eric7/JediInterface/AssistantJedi.py	Mon Oct 04 19:59:06 2021 +0200
@@ -7,7 +7,10 @@
 Module implementing the Jedi assistant plug-in.
 """
 
+import contextlib
+
 from PyQt6.QtCore import pyqtSlot, QObject
+from PyQt6.QtWidgets import QMenu
 
 import Preferences
 
@@ -33,6 +36,7 @@
         
         self.__jediServer = None
         self.__editors = []
+        self.__menuActions = {}
         
         from .JediServer import JediServer
         self.__jediServer = JediServer(self.__vm, project, self.__ui)
@@ -40,6 +44,8 @@
         
         self.__ui.preferencesChanged.connect(self.__preferencesChanged)
         
+        self.__initRefactoringMenu()
+        
         self.__vm.editorOpenedEd.connect(self.__editorOpened)
         self.__vm.editorClosedEd.connect(self.__editorClosed)
     
@@ -118,6 +124,14 @@
         
         editor.registerMouseHoverHelpFunction(
             self.__jediServer.hoverHelp)
+        
+        menu = editor.getMenu("Main")
+        if menu is not None:
+            checkAction = editor.getMenu("Checks").menuAction()
+            act = menu.insertMenu(checkAction, self.__menu)
+            sep = menu.insertSeparator(checkAction)
+            self.__menuActions[editor] = [act, sep]
+        editor.showMenu.connect(self.__editorShowMenu)
     
     def __disconnectEditor(self, editor):
         """
@@ -133,6 +147,14 @@
         
         editor.unregisterMouseHoverHelpFunction(
             self.__jediServer.hoverHelp)
+        
+        with contextlib.suppress(TypeError):
+            editor.showMenu.disconnect(self.__editorShowMenu)
+        menu = editor.getMenu("Main")
+        if menu is not None and editor in self.__menuActions:
+            for act in self.__menuActions[editor]:
+                menu.removeAction(act)
+            del self.__menuActions[editor]
     
     def __connectMouseClickHandler(self, editor):
         """
@@ -194,3 +216,28 @@
         @type Editor
         """
         editor.removeCallTipHook("jedi")
+    
+    def __initRefactoringMenu(self):
+        """
+        Private method to initialize the Refactoring menu.
+        """
+        # TODO: populate the Refactoring menu
+        self.__menu = QMenu(self.tr("Refactoring"))
+        self.__menu.addAction(
+            self.tr("Rename Variable"),
+            self.__jediServer.refactoringRenameVariable)
+    
+    def __editorShowMenu(self, menuName, menu, editor):
+        """
+        Private slot called, when the the editor context menu or a submenu is
+        about to be shown.
+        
+        @param menuName name of the menu to be shown
+        @type str
+        @param menu reference to the menu
+        @type QMenu
+        @param editor reference to the editor
+        @type Editor
+        """
+        if menuName == "Main":
+            self.__menu.setEnabled(editor.hasSelectedText())
--- a/eric7/JediInterface/JediClient.py	Sun Oct 03 18:36:41 2021 +0200
+++ b/eric7/JediInterface/JediClient.py	Mon Oct 04 19:59:06 2021 +0200
@@ -7,6 +7,7 @@
 Module implementing the Jedi client of eric7.
 """
 
+import contextlib
 import sys
 
 SuppressedException = Exception
@@ -55,11 +56,17 @@
             "hoverHelp": self.__getHoverHelp,
             "gotoDefinition": self.__getAssignment,
             "gotoReferences": self.__getReferences,
+            
+            "renameVariable": self.__renameVariable,
+            "applyRefactoring": self.__applyRefactoring,
+            "cancelRefactoring": self.__cancelRefactoring,
         }
         
         self.__id = idString
         
         self.__project = None
+        
+        self.__refactorings = {}
     
     def handleCall(self, method, params):
         """
@@ -404,6 +411,81 @@
         result.update(errorDict)
         
         self.sendJson("GotoReferencesResult", result)
+    
+    def __renameVariable(self, params):
+        """
+        Private method to rename the variable under the cursor.
+        
+        @param params dictionary containing the method parameters
+        @type dict
+        """
+        filename = params["FileName"]
+        source = params["Source"]
+        line = params["Line"]
+        index = params["Index"]
+        uid = params["Uuid"]
+        newName = params["NewName"]
+        
+        errorDict = {}
+        diff = ""
+        
+        script = jedi.Script(source, path=filename, project=self.__project)
+        
+        try:
+            refactoring = script.rename(line, index, new_name=newName)
+            self.__refactorings[uid] = refactoring
+            diff = refactoring.get_diff()
+        except SuppressedException as err:
+            errorDict = self.__handleError(err)
+        
+        result = {
+            "Diff": diff,
+            "Uuid": uid,
+        }
+        result.update(errorDict)
+        
+        self.sendJson("RenameVariableDiff", result)
+    
+    def __applyRefactoring(self, params):
+        """
+        Private method to apply a refactoring.
+        
+        @param params dictionary containing the method parameters
+        @type dict
+        """
+        uid = params["Uuid"]
+        
+        errorDict = {}
+        
+        try:
+            refactoring = self.__refactorings[uid]
+            refactoring.apply()
+            ok = True
+        except KeyError:
+            ok = False
+        except SuppressedException as err:
+            errorDict = self.__handleError(err)
+        
+        result = {
+            "result": ok,
+        }
+        result.update(errorDict)
+        
+        self.sendJson("RefactoringApplyResult", result)
+        
+        with contextlib.suppress(KeyError):
+            del self.__refactorings[uid]
+    
+    def __cancelRefactoring(self, params):
+        """
+        Private method to cancel a refactoring.
+        
+        @param params dictionary containing the method parameters
+        @type dict
+        """
+        uid = params["Uuid"]
+        with contextlib.suppress(KeyError):
+            del self.__refactorings[uid]
 
 
 if __name__ == '__main__':
--- a/eric7/JediInterface/JediServer.py	Sun Oct 03 18:36:41 2021 +0200
+++ b/eric7/JediInterface/JediServer.py	Mon Oct 04 19:59:06 2021 +0200
@@ -12,8 +12,10 @@
 import uuid
 
 from PyQt6.QtCore import pyqtSlot, QCoreApplication, QTimer
+from PyQt6.QtWidgets import QInputDialog, QLineEdit, QDialog
 
 from EricWidgets.EricApplication import ericApp
+from EricWidgets import EricMessageBox
 
 from EricNetwork.EricJsonServer import EricJsonServer
 
@@ -22,6 +24,8 @@
 import Preferences
 import Globals
 
+from .RefactoringPreviewDialog import RefactoringPreviewDialog
+
 
 class JediServer(EricJsonServer):
     """
@@ -86,6 +90,9 @@
             "GotoDefinitionResult": self.__processGotoDefinitionResult,
             "GotoReferencesResult": self.__processGotoReferencesResult,
             
+            "RenameVariableDiff": self.__showRenameVariableDiff,
+            "RefactoringApplyResult": self.__checkRefactoringResult,
+            
             "ClientException": self.__processClientException,
         }
         
@@ -480,6 +487,114 @@
             del self.__editors[euuid]
     
     #######################################################################
+    ## Refactoring methods below
+    #######################################################################
+    
+    @pyqtSlot()
+    def refactoringRenameVariable(self):
+        """
+        Public slot to rename the selected variable.
+        """
+        editor = self.__vm.activeWindow()
+        if editor:
+            idString = self.__idString(editor)
+            if not idString:
+                return
+            
+            newName, ok = QInputDialog.getText(
+                None,
+                self.tr("Rename Variable"),
+                self.tr("Enter the new name for the variable:"),
+                QLineEdit.EchoMode.Normal
+            )
+            
+            if ok and newName:
+                filename = editor.getFileName()
+                line, index = editor.getCursorPosition()
+                line += 1       # jedi line numbers are 1 based
+                source = editor.text()
+                
+                self.__ensureActive(idString)
+                
+                euuid = str(uuid.uuid4())
+                self.__editors[euuid] = editor
+                
+                self.sendJson("renameVariable", {
+                    "FileName": filename,
+                    "Source": source,
+                    "Line": line,
+                    "Index": index,
+                    "Uuid": euuid,
+                    "NewName": newName,
+                }, idString=idString)
+    
+    def __showRenameVariableDiff(self, result):
+        """
+        Private method to show the diff of the Rename Variable refactoring.
+        
+        @param result dictionary containing the result data
+        @type dict
+        """
+        if "Error" not in result:
+            # ignore errors silently
+            euuid = result["Uuid"]
+            diff = result["Diff"]
+            dlg = RefactoringPreviewDialog(self.tr("Rename Variable"), diff)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                self.__applyRefactoring(euuid)
+            else:
+                self.__cancelRefactoring(euuid)
+    
+    def __applyRefactoring(self, uid):
+        """
+        Private method to apply a given refactoring.
+        
+        @param uid UID of the calculated refactoring
+        @type str
+        """
+        with contextlib.suppress(KeyError):
+            editor = self.__editors[uid]
+            idString = self.__idString(editor)
+            
+            self.sendJson("applyRefactoring", {
+                "Uuid": uid,
+            }, idString=idString)
+            
+            del self.__editors[uid]
+    
+    def __cancelRefactoring(self, uid):
+        """
+        Private method to cancel a given refactoring.
+        
+        @param uid UID of the calculated refactoring
+        @type str
+        """
+        with contextlib.suppress(KeyError):
+            editor = self.__editors[uid]
+            idString = self.__idString(editor)
+            
+            self.sendJson("cancelRefactoring", {
+                "Uuid": uid,
+            }, idString=idString)
+            
+            del self.__editors[uid]
+    
+    def __checkRefactoringResult(self, result):
+        """
+        Private method to check the refactoring result for errors.
+        
+        @param result dictionary containing the result data
+        @type dict
+        """
+        if "Error" in result:
+            EricMessageBox.critical(
+                self,
+                self.tr("Apply Refactoring"),
+                self.tr("<p>The refactoring could not be applied.</p>"
+                        "<p>Reason: {0}</p>").format(result["ErrorString"])
+            )
+    
+    #######################################################################
     ## Methods below handle the network connection
     #######################################################################
     
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/JediInterface/RefactoringPreviewDialog.py	Mon Oct 04 19:59:06 2021 +0200
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to preview refactoring changes.
+"""
+
+from PyQt6.QtWidgets import QDialog, QDialogButtonBox
+
+from UI.DiffHighlighter import DiffHighlighter
+
+from .Ui_RefactoringPreviewDialog import Ui_RefactoringPreviewDialog
+
+
+class RefactoringPreviewDialog(QDialog, Ui_RefactoringPreviewDialog):
+    """
+    Class implementing a dialog to preview refactoring changes.
+    """
+    def __init__(self, title, diff, parent=None):
+        """
+        Constructor
+        
+        @param title title string to be shown above the diff
+        @type str
+        @param diff changes to be shown (unified diff)
+        @type str
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.buttonBox.addButton(
+            self.tr("&Apply Changes"), QDialogButtonBox.ButtonRole.AcceptRole)
+        
+        self.highlighter = DiffHighlighter(self.previewEdit.document())
+        
+        self.titleLabel.setText(title)
+        self.previewEdit.setPlainText(diff)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/JediInterface/RefactoringPreviewDialog.ui	Mon Oct 04 19:59:06 2021 +0200
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>RefactoringPreviewDialog</class>
+ <widget class="QDialog" name="RefactoringPreviewDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Preview Changes</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="titleLabel"/>
+   </item>
+   <item>
+    <widget class="QPlainTextEdit" name="previewEdit">
+     <property name="tabChangesFocus">
+      <bool>true</bool>
+     </property>
+     <property name="lineWrapMode">
+      <enum>QPlainTextEdit::NoWrap</enum>
+     </property>
+     <property name="readOnly">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>RefactoringPreviewDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>RefactoringPreviewDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

eric ide

mercurial