Mon, 04 Oct 2021 19:59:06 +0200
Started implementing the Jedi refactoring interface.
--- 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>