From af35f2efd69b8628927b50893a279dacddc8328b Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Thu, 9 Feb 2023 09:44:09 +0800 Subject: [PATCH 01/13] Support change signature refactoring Signed-off-by: Shi Chen --- .vscode/launch.json | 18 +- package-lock.json | 320 ++++++++++++++-- package.json | 11 +- src/log.ts | 2 +- src/refactorAction.ts | 5 + src/refactoring/changeSignaturePanel.ts | 178 +++++++++ src/webview/changeSignature/App.css | 107 ++++++ src/webview/changeSignature/App.tsx | 413 +++++++++++++++++++++ src/webview/changeSignature/index.tsx | 10 + src/webview/utils.ts | 14 + src/webview/vscodeapiWrapper.ts | 78 ++++ test/lightweight-mode-suite/index.ts | 4 +- test/standard-mode-suite/extension.test.ts | 2 +- test/standard-mode-suite/index.ts | 4 +- tsconfig.json | 16 +- webpack.config.js | 43 ++- 16 files changed, 1190 insertions(+), 35 deletions(-) create mode 100644 src/refactoring/changeSignaturePanel.ts create mode 100644 src/webview/changeSignature/App.css create mode 100644 src/webview/changeSignature/App.tsx create mode 100644 src/webview/changeSignature/index.tsx create mode 100644 src/webview/utils.ts create mode 100644 src/webview/vscodeapiWrapper.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index dda897afd2..9b3907883f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,6 +7,7 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", + "debugWebviews": true, "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], "env": { "DEBUG_VSCODE_JAVA":"true" @@ -14,7 +15,13 @@ "stopOnEntry": false, "sourceMaps": true, "outFiles": [ "${workspaceRoot}/dist/**/*.js" ], - "preLaunchTask": "npm: watch" + "preLaunchTask": "npm: watch", + "rendererDebugOptions": { + "webRoot": "${workspaceFolder}/dist/changeSignature", + "urlFilter": "*redhat.java*", + "sourceMaps": true, + "pauseForSourceMap": true, + } }, { "name": "Launch Extension - Remote Server", @@ -36,6 +43,7 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", + "debugWebviews": true, "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], "stopOnEntry": false, "sourceMaps": true, @@ -44,7 +52,13 @@ "JDTLS_CLIENT_PORT": "5036", "DEBUG_VSCODE_JAVA":"true" }, - "preLaunchTask": "npm: watch" + "preLaunchTask": "npm: watch", + "rendererDebugOptions": { + "webRoot": "${workspaceFolder}/dist/**", + "urlFilter": "*redhat.java*", + "sourceMaps": true, + "pauseForSourceMap": true, + } }, { "name": "Launch Extension - SyntaxLS Client", diff --git a/package-lock.json b/package-lock.json index 173ea5fbf4..1303968070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -142,6 +142,46 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@microsoft/fast-element": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.11.0.tgz", + "integrity": "sha512-VKJYMkS5zgzHHb66sY7AFpYv6IfFhXrjQcAyNgi2ivD65My1XOhtjfKez5ELcLFRJfgZNAxvI8kE69apXERTkw==" + }, + "@microsoft/fast-foundation": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-2.47.0.tgz", + "integrity": "sha512-EyFuioaZQ9ngjUNRQi8R3dIPPsaNQdUOS+tP0G7b1MJRhXmQWIitBM6IeveQA6ZvXG6H21dqgrfEWlsYrUZ2sw==", + "requires": { + "@microsoft/fast-element": "^1.11.0", + "@microsoft/fast-web-utilities": "^5.4.1", + "tabbable": "^5.2.0", + "tslib": "^1.13.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@microsoft/fast-react-wrapper": { + "version": "0.1.48", + "resolved": "https://registry.npmjs.org/@microsoft/fast-react-wrapper/-/fast-react-wrapper-0.1.48.tgz", + "integrity": "sha512-9NvEjru9Kn5ZKjomAMX6v+eF0DR+eDkxKDwDfi+Wb73kTbrNzcnmlwd4diN15ygH97kldgj2+lpvI4CKLQQWLg==", + "requires": { + "@microsoft/fast-element": "^1.9.0", + "@microsoft/fast-foundation": "^2.41.1" + } + }, + "@microsoft/fast-web-utilities": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-5.4.1.tgz", + "integrity": "sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==", + "requires": { + "exenv-es6": "^1.1.1" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -293,6 +333,38 @@ "integrity": "sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/react": { + "version": "17.0.53", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.53.tgz", + "integrity": "sha512-1yIpQR2zdYu1Z/dc1OxC+MA6GR240u3gcnP4l6mvj/PJiVaqHsQPmWttsvHsfnhfPbU2FuGmo0wSITPygjBmsw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.19", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.19.tgz", + "integrity": "sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ==", + "dev": true, + "requires": { + "@types/react": "^17" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, "@types/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", @@ -320,6 +392,12 @@ "integrity": "sha512-emg7wdsTFzdi+elvoyoA+Q8keEautdQHyY5LNmHVM4PTpY8JgOTVADrGVyXGepJ6dVW2OS5/xnLUWh+nZxvdiA==", "dev": true }, + "@types/vscode-webview": { + "version": "1.57.1", + "resolved": "https://registry.npmjs.org/@types/vscode-webview/-/vscode-webview-1.57.1.tgz", + "integrity": "sha512-ghW5SfuDmsGDS2A4xkvGsLwDRNc3Vj5rS6rPOyPm/IryZuf3wceZKxgYaUoW+k9f0f/CB7y2c1rRsdOWZWn0PQ==", + "dev": true + }, "@types/winreg": { "version": "1.2.30", "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", @@ -539,6 +617,11 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "@vscode/codicons": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.32.tgz", + "integrity": "sha512-3lgSTWhAzzWN/EPURoY4ZDBEA80OPmnaknNujA3qnI4Iu7AONWd9xF3iE4L+4prIe8E3TUnLQ4pxoaFTEEZNwg==" + }, "@vscode/test-electron": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.5.tgz", @@ -607,6 +690,16 @@ } } }, + "@vscode/webview-ui-toolkit": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.2.2.tgz", + "integrity": "sha512-xIQoF4FC3Xh6d7KNKIoIezSiFWYFuf6gQMdDyKueKBFGeKwaHWEn+dY2g3makvvEsNMEDji/woEwvg9QSbuUsw==", + "requires": { + "@microsoft/fast-element": "^1.6.2", + "@microsoft/fast-foundation": "^2.38.0", + "@microsoft/fast-react-wrapper": "^0.1.18" + } + }, "@webassemblyjs/ast": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", @@ -1799,6 +1892,45 @@ } } }, + "css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "dependencies": { + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -2208,9 +2340,9 @@ } }, "enhanced-resolve": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", - "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -2218,9 +2350,9 @@ }, "dependencies": { "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true } } @@ -2781,6 +2913,11 @@ } } }, + "exenv-es6": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==" + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -3953,6 +4090,12 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true + }, "ieee754": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", @@ -4313,6 +4456,11 @@ } } }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4721,6 +4869,14 @@ } } }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -5303,8 +5459,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -5627,6 +5782,12 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", @@ -5725,6 +5886,76 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "dependencies": { + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + } + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5797,6 +6028,25 @@ "safe-buffer": "^5.1.0" } }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -6073,6 +6323,15 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "schema-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", @@ -6381,6 +6640,12 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -6578,6 +6843,12 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "style-loader": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", + "dev": true + }, "supports-color": { "version": "2.0.0", "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -6594,6 +6865,11 @@ "es6-symbol": "^3.1.1" } }, + "tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==" + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -6813,9 +7089,9 @@ "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "ts-loader": { - "version": "9.2.6", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.6.tgz", - "integrity": "sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", + "integrity": "sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -6876,12 +7152,6 @@ "to-regex-range": "^5.0.1" } }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6889,15 +7159,21 @@ "dev": true }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index e70da57df3..b69590c8b0 100644 --- a/package.json +++ b/package.json @@ -1398,6 +1398,9 @@ "@types/lodash.findindex": "^4.6.6", "@types/mocha": "^5.2.5", "@types/node": "^16.11.7", + "@types/react": "^17.0.37", + "@types/react-dom": "^17.0.11", + "@types/vscode-webview": "^1.57.0", "@types/semver": "^7.3.8", "@types/sinon": "^10.0.12", "@types/vscode": "^1.74.0", @@ -1406,6 +1409,7 @@ "@vscode/test-electron": "^2.1.5", "@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/parser": "^5.18.0", + "css-loader": "^6.7.3", "eslint": "^8.13.0", "eslint-webpack-plugin": "^3.2.0", "gulp": "^4.0.2", @@ -1417,12 +1421,15 @@ "mocha": "^9.2.2", "request": "^2.88.2", "sinon": "^14.0.0", - "ts-loader": "^9.2.6", + "style-loader": "^3.3.1", + "ts-loader": "^9.4.2", "typescript": "^4.6.4", "webpack": "^5.28.0", "webpack-cli": "^4.6.0" }, "dependencies": { + "@vscode/codicons": "^0.0.32", + "@vscode/webview-ui-toolkit": "^1.2.1", "chokidar": "^3.5.3", "expand-home-dir": "^0.0.3", "fmtr": "^1.1.2", @@ -1430,6 +1437,8 @@ "glob": "^7.1.3", "htmlparser2": "6.0.1", "jdk-utils": "^0.4.4", + "react": "^17.0.2", + "react-dom": "^17.0.2", "semver": "^7.3.5", "vscode-languageclient": "8.1.0", "winreg-utf8": "^0.1.1", diff --git a/src/log.ts b/src/log.ts index ce340c3d96..30ffea336f 100644 --- a/src/log.ts +++ b/src/log.ts @@ -1,5 +1,5 @@ import { createLogger, format, transports } from 'winston'; -import * as DailyRotateFile from 'winston-daily-rotate-file'; +import DailyRotateFile from 'winston-daily-rotate-file'; export function initializeLogFile(filename: string) { logger.add(new DailyRotateFile({ diff --git a/src/refactorAction.ts b/src/refactorAction.ts index bc90e103dc..068998ccb7 100644 --- a/src/refactorAction.ts +++ b/src/refactorAction.ts @@ -7,6 +7,7 @@ import { FormattingOptions, WorkspaceEdit, RenameFile, DeleteFile, TextDocumentE import { LanguageClient } from 'vscode-languageclient/node'; import { Commands as javaCommands } from './commands'; import { GetRefactorEditRequest, MoveRequest, RefactorWorkspaceEdit, RenamePosition, GetMoveDestinationsRequest, SearchSymbols, SelectionInfo, InferSelectionRequest } from './protocol'; +import { ChangeSignaturePanel } from './refactoring/changeSignaturePanel'; import { getExtractInterfaceArguments, revealExtractedInterface } from './refactoring/extractInterface'; export function registerCommands(languageClient: LanguageClient, context: ExtensionContext) { @@ -40,6 +41,7 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E || command === 'extractMethod' || command === 'extractField' || command === 'extractInterface' + || command === 'changeSignature' || command === 'assignField' || command === 'convertVariableToField' || command === 'invertVariable' @@ -109,6 +111,9 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E return; } commandArguments.push(...args); + } else if (command === 'changeSignature') { + ChangeSignaturePanel.render(context.extensionUri, languageClient, command, params, formattingOptions, commandInfo); + return; } const result: RefactorWorkspaceEdit = await languageClient.sendRequest(GetRefactorEditRequest.type, { diff --git a/src/refactoring/changeSignaturePanel.ts b/src/refactoring/changeSignaturePanel.ts new file mode 100644 index 0000000000..11feac603b --- /dev/null +++ b/src/refactoring/changeSignaturePanel.ts @@ -0,0 +1,178 @@ +import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn, workspace, WorkspaceEdit, Position } from "vscode"; +import { LanguageClient } from "vscode-languageclient/node"; +import { GetRefactorEditRequest, RefactorWorkspaceEdit } from "../protocol"; +import { getNonce, getUri } from "../webview/utils"; + +interface MethodParameter { + type: string; + name: string; + defaultValue: string; + originalIndex: number; +} + +interface MethodException { + type: string; + typeHandleIdentifier: string; +} + +export class ChangeSignaturePanel { + public static type = "java.refactor.changeSignature"; + public static title = "Refactor: Change Method Signature"; + public static currentPanel: ChangeSignaturePanel | undefined; + private readonly panel: WebviewPanel; + private disposables: Disposable[] = []; + + // method matadata + private methodIdentifier: string | undefined; + private methodName: string | undefined; + private accessType: string | undefined; + private returnType: string | undefined; + private parameters: MethodParameter[] | undefined; + private exceptions: MethodException[] | undefined; + + // refactor metadata + private languageClient: LanguageClient; + private params: any; + private formattingOptions: any; + private command: any; + + private constructor(panel: WebviewPanel, extensionUri: Uri) { + this.panel = panel; + this.panel.onDidDispose(() => this.dispose(), null, this.disposables); + this.panel.webview.html = this.getWebviewContent(this.panel.webview, extensionUri); + this.setWebviewMessageListener(this.panel.webview); + } + + public static render(extensionUri: Uri, languageClient: LanguageClient, command: any, params: any, formattingOptions: any, commandInfo: any) { + if (ChangeSignaturePanel.currentPanel) { + ChangeSignaturePanel.currentPanel.panel.reveal(ViewColumn.Beside); + } else { + const panel = window.createWebviewPanel( + ChangeSignaturePanel.type, + ChangeSignaturePanel.title, + ViewColumn.Beside, + { + enableCommandUris: true, + enableScripts: true, + localResourceRoots: [Uri.joinPath(extensionUri, "dist")], + } + ); + ChangeSignaturePanel.currentPanel = new ChangeSignaturePanel(panel, extensionUri); + ChangeSignaturePanel.currentPanel.setMetadata(languageClient, command, params, formattingOptions, commandInfo); + } + } + + public setMetadata(languageClient: LanguageClient, command: any, params: any, formattingOptions: any, commandInfo: any) { + this.languageClient = languageClient; + this.command = command; + this.params = params; + this.formattingOptions = formattingOptions; + this.methodIdentifier = commandInfo.methodIdentifier; + this.methodName = commandInfo.methodName as string; + this.accessType = commandInfo.accessType as string; + this.returnType = commandInfo.returnType as string; + this.parameters = commandInfo.parameters as MethodParameter[]; + this.exceptions = commandInfo.exceptions as MethodException[]; + } + + public dispose() { + ChangeSignaturePanel.currentPanel = undefined; + this.panel.dispose(); + while (this.disposables.length) { + const disposable = this.disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } + + private getWebviewContent(webview: Webview, extensionUri: Uri) { + + const scriptUri = getUri(webview, extensionUri, [ + "dist", + "changeSignature", + "index.js", + ]); + + const nonce = getNonce(); + + return /* html*/ ` + + + + + + + Change Signature + + +
+ + + + `; + } + + private setWebviewMessageListener(webview: Webview) { + webview.onDidReceiveMessage( + async (message: any) => { + const command = message.command; + switch (command) { + case "webviewReady": + await this.panel.webview.postMessage({ + command: "setInitialState", + methodIdentifier: this.methodIdentifier, + methodName: this.methodName, + accessType: this.accessType, + returnType: this.returnType, + parameters: this.parameters, + exceptions: this.exceptions + }); + break; + case "doRefactor": + await this.doRefactor(message.methodIdentifier, message.isDelegate, message.methodName, message.accessType, message.returnType, message.parameters, message.exceptions); + this.dispose(); + break; + } + }, + undefined, + this.disposables + ); + } + + private async doRefactor(methodIdentifier: string, isDelegate: boolean, methodName: string, accessType: string, returnType: string, parameters: MethodParameter[], exceptions: MethodException[]) { + const clientWorkspaceEdit: RefactorWorkspaceEdit = await this.languageClient.sendRequest(GetRefactorEditRequest.type, { + command: this.command, + context: this.params, + options: this.formattingOptions, + commandArguments: [methodIdentifier, isDelegate, methodName, accessType, returnType, parameters, exceptions] + }); + if (!clientWorkspaceEdit) { + return; + } + if (clientWorkspaceEdit.edit) { + const codeEdit: WorkspaceEdit = await this.languageClient.protocol2CodeConverter.asWorkspaceEdit(clientWorkspaceEdit.edit); + + /** + * See the issue https://github.com/microsoft/vscode/issues/94650. + * The current vscode doesn't provide a way for the extension to pre-select all changes. + * + * As a workaround, this extension would append a dummy text edit that needs a confirm, + * and then make all others text edits not need a confirm. This will ensure that + * the REFACTOR PREVIEW panel can be triggered and all valid changes pre-selected. + */ + const textEditEntries = codeEdit.entries(); + if (textEditEntries && textEditEntries.length) { + const dummyNodeUri: Uri = textEditEntries[textEditEntries.length - 1][0]; + codeEdit.insert(dummyNodeUri, new Position(0, 0), "", { + needsConfirmation: true, + label: "Dummy node used to enable preview" + }); + } + + if (codeEdit) { + await workspace.applyEdit(codeEdit); + } + } + } +} diff --git a/src/webview/changeSignature/App.css b/src/webview/changeSignature/App.css new file mode 100644 index 0000000000..a079194fef --- /dev/null +++ b/src/webview/changeSignature/App.css @@ -0,0 +1,107 @@ +@import "../../../node_modules/@vscode/codicons/dist/codicon.css"; + +main { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + height: 100%; +} + +main>* { + margin: 1rem 0; +} + +.section { + padding: 0; + margin: 0; + width: 100%; +} + +.section-columns { + display: flex; + width: 100%; + flex-flow: row wrap; + min-width: none; +} + +.header-left { + flex-wrap: nowrap; + flex-direction: column; + display: flex; + padding: 1rem 1rem 1rem 0; + flex: 0 0 30%; + max-width: 30%; + box-sizing: border-box; +} + +.header { + flex-wrap: nowrap; + flex-direction: column; + display: flex; + padding: 1rem; + flex: 0 0 30%; + max-width: 30%; + box-sizing: border-box; +} + +.header-right { + flex-wrap: nowrap; + flex-direction: column; + display: flex; + padding: 1rem 0 1rem 1rem; + flex: 0 0 30%; + max-width: 30%; + box-sizing: border-box; +} + +.vsc-button-left { + float: left; + margin: 1rem 1rem 1rem 0; +} + +.bottom-buttons { + margin: 0; + display: block; + width: 90%; +} + +.preview { + width: 90%; +} + +.parameters-panel { + margin: 0; + width: 90%; +} + +.parameters-view { + padding: 0; + flex-direction: column; + width: 100%; +} + +.parameters-grid { + width: 100%; +} + +.parameter-cell { + padding-left: 0; +} + +.parameter-cell-button { + display: flex; + justify-content: right; +} + +.table-buttons { + display: inline-flex; +} + +.table-buttons-hide { + visibility: hidden; +} + +.delegate { + margin: 0; +} \ No newline at end of file diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx new file mode 100644 index 0000000000..1057238c35 --- /dev/null +++ b/src/webview/changeSignature/App.tsx @@ -0,0 +1,413 @@ +/* eslint-disable @typescript-eslint/prefer-for-of */ +import { VSCodeButton, VSCodeTextField, VSCodeDropdown, VSCodeOption, VSCodeCheckbox, VSCodePanels, VSCodePanelTab, VSCodePanelView, VSCodeDataGrid, VSCodeDataGridCell, VSCodeDataGridRow, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; +import "./App.css"; +import React from "react"; +import { vscode } from "../vscodeapiWrapper"; + +interface State { + focusRow: number; + methodIdentifier: string | undefined; + isDelegate: boolean; + methodName: string | undefined; + accessType: string | undefined; + returnType: string | undefined; + parameters: MethodParameter[]; + exceptions: MethodException[]; +} + +interface MethodParameter { + type: string; + name: string; + defaultValue: string; + originalIndex: number; +} + +interface MethodException { + type: string; + typeHandleIdentifier: string | undefined; +} + +export class App extends React.Component<{}, State> { + + constructor(props: any) { + super(props); + this.state = { + focusRow: -1, + methodIdentifier: undefined, + isDelegate: false, + methodName: undefined, + accessType: undefined, + returnType: undefined, + parameters: [], + exceptions: [] + }; + } + + doRefactor = () => { + vscode.postMessage({ + command: "doRefactor", + methodIdentifier: this.state.methodIdentifier, + isDelegate: this.state.isDelegate, + accessType: this.state.accessType, + methodName: this.state.methodName, + returnType: this.state.returnType, + parameters: this.state.parameters, + exceptions: this.state.exceptions, + }); + }; + + onChange = (event: any) => { + const id = event.target.id as string; + if (!id) { + return; + } + if (id === "access-modifier") { + this.setState({ + accessType: event.target.value + }); + this.forceUpdate(); + } else if (id === "returnType") { + this.setState({ + returnType: event.target.value + }); + this.forceUpdate(); + } else if (id === "methodName") { + this.setState({ + methodName: event.target.value + }); + this.forceUpdate(); + } else if (id.startsWith("parameterType")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + this.setState({ + parameters: this.state.parameters.map((e, i) => { + if (i === selectedRowNumber) { + e.type = event.target.outerText; + } + return e; + }) + }); + this.forceUpdate(); + } else if (id.startsWith("parameterName")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + this.setState({ + parameters: this.state.parameters.map((e, i) => { + if (i === selectedRowNumber) { + e.name = event.target.outerText; + } + return e; + }) + }); + this.forceUpdate(); + } else if (id.startsWith("parameterDefault")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + this.setState({ + parameters: this.state.parameters.map((e, i) => { + if (i === selectedRowNumber) { + e.defaultValue = event.target.outerText; + } + return e; + }) + }); + this.forceUpdate(); + } else if (id.startsWith("exceptionType")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + this.setState({ + exceptions: this.state.exceptions.map((e, i) => { + if (i === selectedRowNumber) { + e.type = event.target.outerText; + } + return e; + }) + }); + this.forceUpdate(); + } + return; + }; + + getPreview = () => { + let parameters = ""; + for (let i = 0; i < this.state.parameters.length; i++) { + parameters += `${this.state.parameters[i].type} ${this.state.parameters[i].name}, `; + } + parameters = parameters.substring(0, parameters.length - 2); + let exceptions = ""; + if (this.state.exceptions.length) { + exceptions = " throws "; + for (let i = 0; i < this.state.exceptions.length; i++) { + exceptions += `${this.state.exceptions[i].type}, `; + } + exceptions = exceptions.substring(0, exceptions.length - 2); + } + return `${this.state.accessType} ${this.state.returnType} ${this.state.methodName}(${parameters})${exceptions}`; + }; + + onClick = (event: any) => { + const id = event.target.id as string; + if (!id) { + return; + } + if (id === "refactor") { + this.doRefactor(); + } else if (id === "addParameter") { + const parameterNames = this.state.parameters.map(e => { + return e.name; + }); + let newParameterName: string = "newParam"; + let i = 1; + while (parameterNames.includes(newParameterName)) { + i++; + newParameterName = `newParam${i}`; + } + this.setState({ + parameters: [...this.state.parameters, { + type: "Object", + name: newParameterName, + defaultValue: "null", + originalIndex: -1 + }] + }); + this.forceUpdate(); + } else if (id.startsWith("removeParameter")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + this.setState({ + parameters: this.state.parameters.filter((e, i) => { + return i !== selectedRowNumber; + }) + }); + this.forceUpdate(); + } else if (id.startsWith("upParameter")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + const currentParameters = this.state.parameters; + const temp = currentParameters[selectedRowNumber - 1]; + currentParameters[selectedRowNumber - 1] = currentParameters[selectedRowNumber]; + currentParameters[selectedRowNumber] = temp; + this.setState({ + parameters: currentParameters + }); + this.forceUpdate(); + } else if (id.startsWith("downParameter")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + const currentParameters = this.state.parameters; + const temp = currentParameters[selectedRowNumber + 1]; + currentParameters[selectedRowNumber + 1] = currentParameters[selectedRowNumber]; + currentParameters[selectedRowNumber] = temp; + this.setState({ + parameters: currentParameters + }); + this.forceUpdate(); + } else if (id === "addException") { + const exceptionNames = this.state.exceptions.map(e => { + return e.type; + }); + let newExceptionName: string = "Exception"; + let i = 1; + while (exceptionNames.includes(newExceptionName)) { + i++; + newExceptionName = `Exception${i}`; + } + this.setState({ + exceptions: [...this.state.exceptions, { + type: newExceptionName, + typeHandleIdentifier: undefined, + }] + }); + this.forceUpdate(); + } else if (id.startsWith("removeException")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + this.setState({ + exceptions: this.state.exceptions.filter((e, i) => { + return i !== selectedRowNumber; + }) + }); + this.forceUpdate(); + } else if (id === "delegate") { + this.setState({ + isDelegate: event.target.checked + }); + this.forceUpdate(); + } + }; + + handleMessage = (event: any) => { + const { data } = event; + const command = data.command as string; + if (command === "setInitialState") { + this.setState({ + methodIdentifier: data.methodIdentifier, + accessType: data.accessType, + methodName: data.methodName, + returnType: data.returnType, + parameters: data.parameters, + exceptions: data.exceptions, + }); + this.forceUpdate(); + } + }; + + onMouseEnter = (event: any) => { + const id = event.target.id as string; + if (id.includes("Header")) { + this.setState({ + focusRow: -1 + }); + } else if (id) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber !== undefined) { + this.setState({ + focusRow: selectedRowNumber + }); + } + } + }; + + onMouseLeave = (event: any) => { + this.setState({ + focusRow: -1 + }); + }; + + componentDidMount(): void { + window.addEventListener("message", this.handleMessage); + vscode.postMessage({ + command: "webviewReady" + }); + } + + render = () => { + return ( +
+

Change Method Signature

+
+
+
+ + + public + protected + package-private + private + +
+
+ Return type: +
+
+ Method name: +
+
+
+ + Parameters + Exceptions + + + + Type + Name + Default value + + + { + (() => { + const options = []; + for (let i = 0; i < this.state.parameters.length; i++) { + options.push( + {this.state.parameters[i].type} + {this.state.parameters[i].name} + {this.state.parameters[i].originalIndex === -1 ? this.state.parameters[i].defaultValue : "-"} + +
+ + + + + + + + + +
+
+
+ ); + } + return options; + })() + } +
+
+ Add +
+
+ + + + Type + + + { + (() => { + const options = []; + for (let i = 0; i < this.state.exceptions.length; i++) { + options.push( + {this.state.exceptions[i].type} + +
+ + + +
+
+
+ ); + } + return options; + })() + } +
+
+ Add +
+
+
+ Method signature preview: + Keep original method as delegate to changed method +
+ Preview and Refactor +
+
+ ); + }; + + private getSelectedRowNumber(id: string): number | undefined { + const idSplit: string[] = id.split("-"); + if (idSplit.length !== 2) { + return undefined; + } + return Number(idSplit[1]); + } +} diff --git a/src/webview/changeSignature/index.tsx b/src/webview/changeSignature/index.tsx new file mode 100644 index 0000000000..16e0258412 --- /dev/null +++ b/src/webview/changeSignature/index.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { App } from "./App"; + +ReactDOM.render( + + + , + document.getElementById("root") +); diff --git a/src/webview/utils.ts b/src/webview/utils.ts new file mode 100644 index 0000000000..394fd9410e --- /dev/null +++ b/src/webview/utils.ts @@ -0,0 +1,14 @@ +import { Uri, Webview } from "vscode"; + +export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); +} + +export function getNonce() { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/src/webview/vscodeapiWrapper.ts b/src/webview/vscodeapiWrapper.ts new file mode 100644 index 0000000000..b00e0c4eee --- /dev/null +++ b/src/webview/vscodeapiWrapper.ts @@ -0,0 +1,78 @@ +import type { WebviewApi } from "vscode-webview"; + +/** + * A utility wrapper around the acquireVsCodeApi() function, which enables + * message passing and state management between the webview and extension + * contexts. + * + * This utility also enables webview code to be run in a web browser-based + * dev server by using native web browser features that mock the functionality + * enabled by acquireVsCodeApi. + */ +class VSCodeAPIWrapper { + private readonly vsCodeApi: WebviewApi | undefined; + + constructor() { + // Check if the acquireVsCodeApi function exists in the current development + // context (i.e. VS Code development window or web browser) + if (typeof acquireVsCodeApi === "function") { + this.vsCodeApi = acquireVsCodeApi(); + } + } + + /** + * Post a message (i.e. send arbitrary data) to the owner of the webview. + * + * @remarks When running webview code inside a web browser, postMessage will instead + * log the given message to the console. + * + * @param message Abitrary data (must be JSON serializable) to send to the extension context. + */ + public postMessage(message: unknown) { + if (this.vsCodeApi) { + this.vsCodeApi.postMessage(message); + } else { + console.log(message); + } + } + + /** + * Get the persistent state stored for this webview. + * + * @remarks When running webview source code inside a web browser, getState will retrieve state + * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). + * + * @return The current state or `undefined` if no state has been set. + */ + public getState(): unknown | undefined { + if (this.vsCodeApi) { + return this.vsCodeApi.getState(); + } else { + const state = localStorage.getItem("vscodeState"); + return state ? JSON.parse(state) : undefined; + } + } + + /** + * Set the persistent state stored for this webview. + * + * @remarks When running webview source code inside a web browser, setState will set the given + * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). + * + * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved + * using {@link getState}. + * + * @return The new state. + */ + public setState(newState: T): T { + if (this.vsCodeApi) { + return this.vsCodeApi.setState(newState); + } else { + localStorage.setItem("vscodeState", JSON.stringify(newState)); + return newState; + } + } +} + +// Exports class singleton to prevent multiple invocations of acquireVsCodeApi. +export const vscode = new VSCodeAPIWrapper(); diff --git a/test/lightweight-mode-suite/index.ts b/test/lightweight-mode-suite/index.ts index b04cc328fe..895e94daf5 100644 --- a/test/lightweight-mode-suite/index.ts +++ b/test/lightweight-mode-suite/index.ts @@ -11,8 +11,8 @@ // a possible error to the callback or null if none. import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; +import Mocha from 'mocha'; +import glob from 'glob'; export function run(testsRoot: string): Promise { const mocha = new Mocha({ diff --git a/test/standard-mode-suite/extension.test.ts b/test/standard-mode-suite/extension.test.ts index 0634c730ab..4dd115224e 100644 --- a/test/standard-mode-suite/extension.test.ts +++ b/test/standard-mode-suite/extension.test.ts @@ -1,4 +1,4 @@ -import * as assert from 'assert'; +import assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import { env } from 'process'; diff --git a/test/standard-mode-suite/index.ts b/test/standard-mode-suite/index.ts index b04cc328fe..895e94daf5 100644 --- a/test/standard-mode-suite/index.ts +++ b/test/standard-mode-suite/index.ts @@ -11,8 +11,8 @@ // a possible error to the callback or null if none. import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; +import Mocha from 'mocha'; +import glob from 'glob'; export function run(testsRoot: string): Promise { const mocha = new Mocha({ diff --git a/tsconfig.json b/tsconfig.json index 0a41f04d89..fe775f7f67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,13 +2,23 @@ "compilerOptions": { "target": "es6", "lib": [ - "es6" - ], + "es6", + "DOM" + ], "module": "commonjs", "moduleResolution": "node", "outDir": "out", "sourceMap": true, - "plugins": [{ "name": "eslint" }] + "plugins": [ + { + "name": "eslint" + } + ], + /* Enabled for React */ + "jsx": "react-jsx", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, }, "exclude": [ "node_modules", diff --git a/webpack.config.js b/webpack.config.js index eec53b45cb..8a660af69e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,6 +3,7 @@ //@ts-check const ESLintWebpackPlugin = require('eslint-webpack-plugin'); +const webpack = require('webpack'); const path = require('path'); /**@type {import('webpack').Configuration}*/ @@ -51,4 +52,44 @@ const config = { level: 'log', }, } -module.exports = config; + +const configAssets = { + name: 'assets', + mode: 'none', + entry: { + changeSignature: './src/webview/changeSignature/index.tsx', + }, + module: { + rules: [{ + test: /\.ts(x?)$/, + exclude: /node_modules/, + use: 'ts-loader' + }, { + test: /\.(css)$/, + use: [{ + loader: 'style-loader' + }, { + loader: 'css-loader' + }] + }, { + test: /\.(ttf)$/, + type: 'asset/inline', + }] + }, + output: { + filename: '[name]/index.js', + path: path.resolve(__dirname, 'dist'), + publicPath: '/', + devtoolModuleFilenameTemplate: "../[resource-path]" + }, + plugins: [ + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + devtool: 'source-map', + resolve: { + extensions: ['.js', '.ts', '.tsx'] + } +} +module.exports = [config, configAssets]; From 80b80adc247c507cf5083a9f8265cc56591c5be9 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Tue, 28 Feb 2023 11:41:17 +0800 Subject: [PATCH 02/13] use onInput() insteadof onChange() Signed-off-by: Shi Chen --- src/webview/changeSignature/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 1057238c35..61f58d8f92 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -313,10 +313,10 @@ export class App extends React.Component<{}, State> {
- Return type: + Return type:
- Method name: + Method name:
From 48037a8ae34a620109f2222ca1f0167f7e86f9d3 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Tue, 28 Feb 2023 15:50:37 +0800 Subject: [PATCH 03/13] address comments Signed-off-by: Shi Chen --- package.json | 2 +- src/refactoring/changeSignaturePanel.ts | 1 + src/webview/changeSignature/App.tsx | 7 +++- ...scodeapiWrapper.ts => vscodeApiWrapper.ts} | 39 +------------------ 4 files changed, 9 insertions(+), 40 deletions(-) rename src/webview/{vscodeapiWrapper.ts => vscodeApiWrapper.ts} (50%) diff --git a/package.json b/package.json index b69590c8b0..8fd8500645 100644 --- a/package.json +++ b/package.json @@ -1429,7 +1429,7 @@ }, "dependencies": { "@vscode/codicons": "^0.0.32", - "@vscode/webview-ui-toolkit": "^1.2.1", + "@vscode/webview-ui-toolkit": "1.2.2", "chokidar": "^3.5.3", "expand-home-dir": "^0.0.3", "fmtr": "^1.1.2", diff --git a/src/refactoring/changeSignaturePanel.ts b/src/refactoring/changeSignaturePanel.ts index 11feac603b..58ad54ef30 100644 --- a/src/refactoring/changeSignaturePanel.ts +++ b/src/refactoring/changeSignaturePanel.ts @@ -55,6 +55,7 @@ export class ChangeSignaturePanel { enableCommandUris: true, enableScripts: true, localResourceRoots: [Uri.joinPath(extensionUri, "dist")], + retainContextWhenHidden: true, } ); ChangeSignaturePanel.currentPanel = new ChangeSignaturePanel(panel, extensionUri); diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 61f58d8f92..0e5e053efc 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -2,7 +2,7 @@ import { VSCodeButton, VSCodeTextField, VSCodeDropdown, VSCodeOption, VSCodeCheckbox, VSCodePanels, VSCodePanelTab, VSCodePanelView, VSCodeDataGrid, VSCodeDataGridCell, VSCodeDataGridRow, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; import "./App.css"; import React from "react"; -import { vscode } from "../vscodeapiWrapper"; +import { vscode } from "../vscodeApiWrapper"; interface State { focusRow: number; @@ -403,6 +403,11 @@ export class App extends React.Component<{}, State> { ); }; + /** + * get the row number of the item id. The format is `${description}-${rowNumber}`. + * @param id the item id + * @returns the row number, or undefined if the id is not in the correct format + */ private getSelectedRowNumber(id: string): number | undefined { const idSplit: string[] = id.split("-"); if (idSplit.length !== 2) { diff --git a/src/webview/vscodeapiWrapper.ts b/src/webview/vscodeApiWrapper.ts similarity index 50% rename from src/webview/vscodeapiWrapper.ts rename to src/webview/vscodeApiWrapper.ts index b00e0c4eee..0b0eaa0113 100644 --- a/src/webview/vscodeapiWrapper.ts +++ b/src/webview/vscodeApiWrapper.ts @@ -32,44 +32,7 @@ class VSCodeAPIWrapper { if (this.vsCodeApi) { this.vsCodeApi.postMessage(message); } else { - console.log(message); - } - } - - /** - * Get the persistent state stored for this webview. - * - * @remarks When running webview source code inside a web browser, getState will retrieve state - * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). - * - * @return The current state or `undefined` if no state has been set. - */ - public getState(): unknown | undefined { - if (this.vsCodeApi) { - return this.vsCodeApi.getState(); - } else { - const state = localStorage.getItem("vscodeState"); - return state ? JSON.parse(state) : undefined; - } - } - - /** - * Set the persistent state stored for this webview. - * - * @remarks When running webview source code inside a web browser, setState will set the given - * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). - * - * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved - * using {@link getState}. - * - * @return The new state. - */ - public setState(newState: T): T { - if (this.vsCodeApi) { - return this.vsCodeApi.setState(newState); - } else { - localStorage.setItem("vscodeState", JSON.stringify(newState)); - return newState; + console.log(`VS Code API not available, raw message: ${message}`); } } } From f8a698272f84577005cba133295b4632011e7bf3 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Wed, 1 Mar 2023 09:54:45 +0800 Subject: [PATCH 04/13] 1. improve parameter/exception table 2. split refactor/preview buttons Signed-off-by: Shi Chen --- src/refactoring/changeSignaturePanel.ts | 41 ++-- src/webview/changeSignature/App.css | 33 ++- src/webview/changeSignature/App.tsx | 286 ++++++++++++++++-------- 3 files changed, 248 insertions(+), 112 deletions(-) diff --git a/src/refactoring/changeSignaturePanel.ts b/src/refactoring/changeSignaturePanel.ts index 58ad54ef30..7b312f3ca4 100644 --- a/src/refactoring/changeSignaturePanel.ts +++ b/src/refactoring/changeSignaturePanel.ts @@ -131,8 +131,9 @@ export class ChangeSignaturePanel { }); break; case "doRefactor": - await this.doRefactor(message.methodIdentifier, message.isDelegate, message.methodName, message.accessType, message.returnType, message.parameters, message.exceptions); - this.dispose(); + if (await this.doRefactor(message.methodIdentifier, message.isDelegate, message.methodName, message.accessType, message.returnType, message.parameters, message.exceptions, message.preview)) { + this.dispose(); + } break; } }, @@ -141,7 +142,20 @@ export class ChangeSignaturePanel { ); } - private async doRefactor(methodIdentifier: string, isDelegate: boolean, methodName: string, accessType: string, returnType: string, parameters: MethodParameter[], exceptions: MethodException[]) { + /** + * refactor the method with the given parameters. + * + * @param methodIdentifier the handle identifier of the method to refactor. + * @param isDelegate if true, the method will be refactored to a delegate method. + * @param methodName the new name of the method. + * @param accessType the new access type of the method. + * @param returnType the new return type of the method. + * @param parameters the new parameters of the method. + * @param exceptions the new exceptions of the method. + * @param preview if true, the refactoring will be previewed before applied. + * @returns true if the refactoring is successful, false otherwise. + */ + private async doRefactor(methodIdentifier: string, isDelegate: boolean, methodName: string, accessType: string, returnType: string, parameters: MethodParameter[], exceptions: MethodException[], preview: boolean): Promise { const clientWorkspaceEdit: RefactorWorkspaceEdit = await this.languageClient.sendRequest(GetRefactorEditRequest.type, { command: this.command, context: this.params, @@ -153,7 +167,6 @@ export class ChangeSignaturePanel { } if (clientWorkspaceEdit.edit) { const codeEdit: WorkspaceEdit = await this.languageClient.protocol2CodeConverter.asWorkspaceEdit(clientWorkspaceEdit.edit); - /** * See the issue https://github.com/microsoft/vscode/issues/94650. * The current vscode doesn't provide a way for the extension to pre-select all changes. @@ -162,18 +175,20 @@ export class ChangeSignaturePanel { * and then make all others text edits not need a confirm. This will ensure that * the REFACTOR PREVIEW panel can be triggered and all valid changes pre-selected. */ - const textEditEntries = codeEdit.entries(); - if (textEditEntries && textEditEntries.length) { - const dummyNodeUri: Uri = textEditEntries[textEditEntries.length - 1][0]; - codeEdit.insert(dummyNodeUri, new Position(0, 0), "", { - needsConfirmation: true, - label: "Dummy node used to enable preview" - }); + if (preview) { + const textEditEntries = codeEdit.entries(); + if (textEditEntries && textEditEntries.length) { + const dummyNodeUri: Uri = textEditEntries[textEditEntries.length - 1][0]; + codeEdit.insert(dummyNodeUri, new Position(0, 0), "", { + needsConfirmation: true, + label: "Dummy node used to enable preview" + }); + } } - if (codeEdit) { - await workspace.applyEdit(codeEdit); + return workspace.applyEdit(codeEdit); } } + return false; } } diff --git a/src/webview/changeSignature/App.css b/src/webview/changeSignature/App.css index a079194fef..f968257af8 100644 --- a/src/webview/changeSignature/App.css +++ b/src/webview/changeSignature/App.css @@ -29,7 +29,7 @@ main>* { flex-wrap: nowrap; flex-direction: column; display: flex; - padding: 1rem 1rem 1rem 0; + padding: 0.5rem 0.5rem 0.5rem 0; flex: 0 0 30%; max-width: 30%; box-sizing: border-box; @@ -39,7 +39,7 @@ main>* { flex-wrap: nowrap; flex-direction: column; display: flex; - padding: 1rem; + padding: 0.5rem; flex: 0 0 30%; max-width: 30%; box-sizing: border-box; @@ -49,7 +49,7 @@ main>* { flex-wrap: nowrap; flex-direction: column; display: flex; - padding: 1rem 0 1rem 1rem; + padding: 0.5rem 0 0.5rem 0.5rem; flex: 0 0 30%; max-width: 30%; box-sizing: border-box; @@ -57,7 +57,12 @@ main>* { .vsc-button-left { float: left; - margin: 1rem 1rem 1rem 0; + margin: 1rem 0.5rem 1rem 0; +} + +.vsc-button { + float: left; + margin: 1rem 0.5rem 1rem 0.5rem; } .bottom-buttons { @@ -89,7 +94,13 @@ main>* { padding-left: 0; } +.parameter-cell-edit { + background-color: var(--vscode-input-background); +} + .parameter-cell-button { + padding: 0; + border: 0; display: flex; justify-content: right; } @@ -98,8 +109,18 @@ main>* { display: inline-flex; } -.table-buttons-hide { - visibility: hidden; +.table-buttons-edit { + display: inline-flex; + background-color: var(--vscode-editor-background); +} + +.table-buttons-edit-ok { + margin-left: 0.2rem; + margin-right: 0.2rem; +} + +.table-buttons-edit-cancel { + margin: 0; } .delegate { diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 0e5e053efc..66a7dde3c4 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -6,6 +6,8 @@ import { vscode } from "../vscodeApiWrapper"; interface State { focusRow: number; + editParameterRow: number; + editExceptionRow: number; methodIdentifier: string | undefined; isDelegate: boolean; methodName: string | undefined; @@ -33,6 +35,8 @@ export class App extends React.Component<{}, State> { super(props); this.state = { focusRow: -1, + editParameterRow: -1, + editExceptionRow: -1, methodIdentifier: undefined, isDelegate: false, methodName: undefined, @@ -43,8 +47,9 @@ export class App extends React.Component<{}, State> { }; } - doRefactor = () => { + doRefactor = (preview: boolean) => { vscode.postMessage({ + preview: preview, command: "doRefactor", methodIdentifier: this.state.methodIdentifier, isDelegate: this.state.isDelegate, @@ -76,62 +81,6 @@ export class App extends React.Component<{}, State> { methodName: event.target.value }); this.forceUpdate(); - } else if (id.startsWith("parameterType")) { - const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); - if (selectedRowNumber === undefined) { - return; - } - this.setState({ - parameters: this.state.parameters.map((e, i) => { - if (i === selectedRowNumber) { - e.type = event.target.outerText; - } - return e; - }) - }); - this.forceUpdate(); - } else if (id.startsWith("parameterName")) { - const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); - if (selectedRowNumber === undefined) { - return; - } - this.setState({ - parameters: this.state.parameters.map((e, i) => { - if (i === selectedRowNumber) { - e.name = event.target.outerText; - } - return e; - }) - }); - this.forceUpdate(); - } else if (id.startsWith("parameterDefault")) { - const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); - if (selectedRowNumber === undefined) { - return; - } - this.setState({ - parameters: this.state.parameters.map((e, i) => { - if (i === selectedRowNumber) { - e.defaultValue = event.target.outerText; - } - return e; - }) - }); - this.forceUpdate(); - } else if (id.startsWith("exceptionType")) { - const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); - if (selectedRowNumber === undefined) { - return; - } - this.setState({ - exceptions: this.state.exceptions.map((e, i) => { - if (i === selectedRowNumber) { - e.type = event.target.outerText; - } - return e; - }) - }); - this.forceUpdate(); } return; }; @@ -159,7 +108,9 @@ export class App extends React.Component<{}, State> { return; } if (id === "refactor") { - this.doRefactor(); + this.doRefactor(false); + } else if (id === "preview") { + this.doRefactor(true); } else if (id === "addParameter") { const parameterNames = this.state.parameters.map(e => { return e.name; @@ -190,6 +141,36 @@ export class App extends React.Component<{}, State> { }) }); this.forceUpdate(); + } else if (id.startsWith("editParameter")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + this.setState({ + editParameterRow: selectedRowNumber, + editExceptionRow: -1, + focusRow: -1, + }); + this.forceUpdate(); + const elementToSelect = document.getElementById(`parameterType-${selectedRowNumber}`); + if (elementToSelect) { + elementToSelect.focus(); + } + } else if (id.startsWith("editException")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + this.setState({ + editParameterRow: -1, + editExceptionRow: selectedRowNumber, + focusRow: -1, + }); + this.forceUpdate(); + const elementToSelect = document.getElementById(`exceptionType-${selectedRowNumber}`); + if (elementToSelect) { + elementToSelect.focus(); + } } else if (id.startsWith("upParameter")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -249,6 +230,94 @@ export class App extends React.Component<{}, State> { isDelegate: event.target.checked }); this.forceUpdate(); + } else if (id.startsWith("confirmParameter")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + const parameterType = document.getElementById(`parameterType-${selectedRowNumber}`); + const parameterName = document.getElementById(`parameterName-${selectedRowNumber}`); + const parameterDefault = this.isDefaultValueEditable(selectedRowNumber) ? document.getElementById(`parameterDefault-${selectedRowNumber}`) : undefined; + this.setState({ + parameters: this.state.parameters.map((e, i) => { + if (i === selectedRowNumber) { + if (parameterType?.outerText) { + e.type = parameterType.outerText; + } + if (parameterName?.outerText) { + e.name = parameterName.outerText; + } + if (parameterDefault?.outerText) { + e.defaultValue = parameterDefault.outerText; + } + } + return e; + }), + editParameterRow: -1, + editExceptionRow: -1, + focusRow: -1 + }); + this.forceUpdate(); + } else if (id.startsWith("cancelParameter")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + const parameterType = document.getElementById(`parameterType-${selectedRowNumber}`); + if (parameterType) { + parameterType.textContent = this.state.parameters[selectedRowNumber].type; + } + const parameterName = document.getElementById(`parameterName-${selectedRowNumber}`); + if (parameterName) { + parameterName.textContent = this.state.parameters[selectedRowNumber].name; + } + if (this.isDefaultValueEditable(selectedRowNumber)) { + const parameterDefault = document.getElementById(`parameterDefault-${selectedRowNumber}`); + if (parameterDefault) { + parameterDefault.textContent = this.state.parameters[selectedRowNumber].defaultValue; + } + } + this.setState({ + editParameterRow: -1, + editExceptionRow: -1, + focusRow: -1 + }); + this.forceUpdate(); + } else if (id.startsWith("confirmException")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + const exceptionType = document.getElementById(`exceptionType-${selectedRowNumber}`); + this.setState({ + exceptions: this.state.exceptions.map((e, i) => { + if (i === selectedRowNumber) { + if (exceptionType?.outerText) { + e.type = exceptionType.outerText; + } + } + return e; + }), + editParameterRow: -1, + editExceptionRow: -1, + focusRow: -1 + }); + this.forceUpdate(); + } else if (id.startsWith("cancelException")) { + const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); + if (selectedRowNumber === undefined) { + return; + } + const exceptionType = document.getElementById(`exceptionType-${selectedRowNumber}`); + if (exceptionType) { + exceptionType.textContent = this.state.exceptions[selectedRowNumber].type; + } + this.setState({ + editParameterRow: -1, + editExceptionRow: -1, + focusRow: -1 + }); + this.forceUpdate(); } }; @@ -297,6 +366,64 @@ export class App extends React.Component<{}, State> { }); } + isDefaultValueEditable = (row: number) => { + return this.state.parameters[row].originalIndex === -1; + }; + + getDefaultValue = (row: number) => { + return this.isDefaultValueEditable(row) ? this.state.parameters[row].defaultValue : "-"; + }; + + generateParameterDataGridRow = (row: number) => { + return + {this.state.parameters[row].type} + {this.state.parameters[row].name} + {this.getDefaultValue(row)} + + {row === this.state.editParameterRow ? +
+ OK + Cancel +
: row === this.state.focusRow ?
+ + + + + + + + + + + + +
:
+ } +
+
; + }; + + generateExceptionDataGridRow = (row: number) => { + return + {this.state.exceptions[row].type} + + {row === this.state.editExceptionRow ? +
+ OK + Cancel +
: row === this.state.focusRow ?
+ + + + + + +
:
+ } +
+
; + }; + render = () => { return (
@@ -334,26 +461,8 @@ export class App extends React.Component<{}, State> { { (() => { const options = []; - for (let i = 0; i < this.state.parameters.length; i++) { - options.push( - {this.state.parameters[i].type} - {this.state.parameters[i].name} - {this.state.parameters[i].originalIndex === -1 ? this.state.parameters[i].defaultValue : "-"} - -
- - - - - - - - - -
-
-
- ); + for (let row = 0; row < this.state.parameters.length; row++) { + options.push(this.generateParameterDataGridRow(row)); } return options; })() @@ -366,24 +475,14 @@ export class App extends React.Component<{}, State> { - Type + Type { (() => { const options = []; - for (let i = 0; i < this.state.exceptions.length; i++) { - options.push( - {this.state.exceptions[i].type} - -
- - - -
-
-
- ); + for (let row = 0; row < this.state.exceptions.length; row++) { + options.push(this.generateExceptionDataGridRow(row)); } return options; })() @@ -394,10 +493,11 @@ export class App extends React.Component<{}, State> {
- Method signature preview: + Method signature: Keep original method as delegate to changed method
- Preview and Refactor + Refactor + Preview
); From 595734457ce9367746a4b1f4e5a2a49bd65c2dd6 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Wed, 1 Mar 2023 14:47:57 +0800 Subject: [PATCH 05/13] refine margin and enable debugging Signed-off-by: Shi Chen --- .vscode/launch.json | 4 --- src/refactoring/changeSignaturePanel.ts | 3 +-- src/webview/changeSignature/App.css | 36 +++++++++++++++++++++---- src/webview/changeSignature/App.tsx | 29 +++++++++++--------- webpack.config.js | 2 +- 5 files changed, 49 insertions(+), 25 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9b3907883f..0ad69b7cdf 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,10 +17,8 @@ "outFiles": [ "${workspaceRoot}/dist/**/*.js" ], "preLaunchTask": "npm: watch", "rendererDebugOptions": { - "webRoot": "${workspaceFolder}/dist/changeSignature", "urlFilter": "*redhat.java*", "sourceMaps": true, - "pauseForSourceMap": true, } }, { @@ -54,10 +52,8 @@ }, "preLaunchTask": "npm: watch", "rendererDebugOptions": { - "webRoot": "${workspaceFolder}/dist/**", "urlFilter": "*redhat.java*", "sourceMaps": true, - "pauseForSourceMap": true, } }, { diff --git a/src/refactoring/changeSignaturePanel.ts b/src/refactoring/changeSignaturePanel.ts index 7b312f3ca4..9e9286d7d4 100644 --- a/src/refactoring/changeSignaturePanel.ts +++ b/src/refactoring/changeSignaturePanel.ts @@ -91,8 +91,7 @@ export class ChangeSignaturePanel { const scriptUri = getUri(webview, extensionUri, [ "dist", - "changeSignature", - "index.js", + "changeSignature.js", ]); const nonce = getNonce(); diff --git a/src/webview/changeSignature/App.css b/src/webview/changeSignature/App.css index f968257af8..dcebaf1880 100644 --- a/src/webview/changeSignature/App.css +++ b/src/webview/changeSignature/App.css @@ -25,6 +25,16 @@ main>* { min-width: none; } +.text-title { + margin: 0; + height: calc(var(--input-height) * 0.8px); +} + +.text-title-content { + margin: 0.5rem 0 0 0; + height: calc(var(--input-height) * 0.8px); +} + .header-left { flex-wrap: nowrap; flex-direction: column; @@ -57,24 +67,35 @@ main>* { .vsc-button-left { float: left; - margin: 1rem 0.5rem 1rem 0; + margin: 0 0.5rem 0.5rem 0; } .vsc-button { float: left; - margin: 1rem 0.5rem 1rem 0.5rem; + margin: 0 0.5rem 0.5rem 0.5rem; +} + +.add-button { + margin: 0.5rem 0 0 0; + display: block; + width: 90%; } .bottom-buttons { - margin: 0; + margin: 0.5rem 0 0 0; display: block; width: 90%; } .preview { + margin: 0 0 0.5rem 0; width: 90%; } +.div.tablist { + padding-left: 0 !important; +} + .parameters-panel { margin: 0; width: 90%; @@ -94,6 +115,11 @@ main>* { padding-left: 0; } +.parameter-cell-header { + padding-left: 0; + background-color: var(--vscode-keybindingTable-headerBackground); +} + .parameter-cell-edit { background-color: var(--vscode-input-background); } @@ -124,5 +150,5 @@ main>* { } .delegate { - margin: 0; -} \ No newline at end of file + margin: 0.5rem 0 0.5rem 0; +} diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 66a7dde3c4..588dc2453a 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -385,12 +385,12 @@ export class App extends React.Component<{}, State> { OK Cancel : row === this.state.focusRow ?
- + {row === 0 ? <> : - - + } + {row === this.state.parameters.length - 1 ? <> : - + } @@ -406,7 +406,7 @@ export class App extends React.Component<{}, State> { generateExceptionDataGridRow = (row: number) => { return {this.state.exceptions[row].type} - + {row === this.state.editExceptionRow ?
OK @@ -431,7 +431,7 @@ export class App extends React.Component<{}, State> {
- +
Access modifier:
public protected @@ -440,10 +440,12 @@ export class App extends React.Component<{}, State> {
- Return type: +
Return type:
+
- Method name: +
Method name:
+
@@ -452,7 +454,7 @@ export class App extends React.Component<{}, State> { Exceptions - + Type Name Default value @@ -468,13 +470,13 @@ export class App extends React.Component<{}, State> { })() } -
+
Add
- + Type @@ -488,12 +490,13 @@ export class App extends React.Component<{}, State> { })() } -
+
Add
- Method signature: +
Method signature:
+ Keep original method as delegate to changed method
Refactor diff --git a/webpack.config.js b/webpack.config.js index 8a660af69e..a189186c02 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -77,7 +77,7 @@ const configAssets = { }] }, output: { - filename: '[name]/index.js', + filename: '[name].js', path: path.resolve(__dirname, 'dist'), publicPath: '/', devtoolModuleFilenameTemplate: "../[resource-path]" From 4705403b3c484ef1cdc0ef18c6812f9c5aa74796 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Wed, 1 Mar 2023 16:10:19 +0800 Subject: [PATCH 06/13] 1. address comments 2. align left margins Signed-off-by: Shi Chen --- src/markdownPreviewProvider.ts | 12 ++---------- src/refactoring/changeSignaturePanel.ts | 5 +---- src/webview/changeSignature/App.css | 11 ++++++----- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/markdownPreviewProvider.ts b/src/markdownPreviewProvider.ts index 9dbe94ea16..2eeeb0818e 100644 --- a/src/markdownPreviewProvider.ts +++ b/src/markdownPreviewProvider.ts @@ -2,6 +2,7 @@ import { Disposable, WebviewPanel, window, ViewColumn, commands, Uri, Webview, E import * as fse from 'fs-extra'; import * as path from 'path'; import { Commands } from "./commands"; +import { getNonce } from "./webview/utils"; class MarkdownPreviewProvider implements Disposable { private panel: WebviewPanel | undefined; @@ -42,7 +43,7 @@ class MarkdownPreviewProvider implements Disposable { } protected async getHtmlContent(webview: Webview, markdownFilePath: string, section: string, context: ExtensionContext): Promise { - const nonce: string = this.getNonce(); + const nonce: string = getNonce(); const styles: string = this.getStyles(webview, context); let body: string | undefined = this.documentCache.get(markdownFilePath); if (!body) { @@ -93,15 +94,6 @@ class MarkdownPreviewProvider implements Disposable { ]; return styles.map((styleUri: Uri) => ``).join('\n'); } - - private getNonce(): string { - let text = ""; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; - } } export const markdownPreviewProvider: MarkdownPreviewProvider = new MarkdownPreviewProvider(); diff --git a/src/refactoring/changeSignaturePanel.ts b/src/refactoring/changeSignaturePanel.ts index 9e9286d7d4..fb19ab649f 100644 --- a/src/refactoring/changeSignaturePanel.ts +++ b/src/refactoring/changeSignaturePanel.ts @@ -161,10 +161,7 @@ export class ChangeSignaturePanel { options: this.formattingOptions, commandArguments: [methodIdentifier, isDelegate, methodName, accessType, returnType, parameters, exceptions] }); - if (!clientWorkspaceEdit) { - return; - } - if (clientWorkspaceEdit.edit) { + if (clientWorkspaceEdit?.edit) { const codeEdit: WorkspaceEdit = await this.languageClient.protocol2CodeConverter.asWorkspaceEdit(clientWorkspaceEdit.edit); /** * See the issue https://github.com/microsoft/vscode/issues/94650. diff --git a/src/webview/changeSignature/App.css b/src/webview/changeSignature/App.css index dcebaf1880..8b0c0694f5 100644 --- a/src/webview/changeSignature/App.css +++ b/src/webview/changeSignature/App.css @@ -23,6 +23,7 @@ main>* { width: 100%; flex-flow: row wrap; min-width: none; + padding-left: calc(var(--design-unit) * 1px); } .text-title { @@ -31,6 +32,7 @@ main>* { } .text-title-content { + padding: 0 0 0 calc(var(--design-unit) * 1px); margin: 0.5rem 0 0 0; height: calc(var(--input-height) * 0.8px); } @@ -82,27 +84,25 @@ main>* { } .bottom-buttons { + padding: 0 0 0 calc(var(--design-unit) * 1px); margin: 0.5rem 0 0 0; display: block; width: 90%; } .preview { + padding: 0 0 0 calc(var(--design-unit) * 1px); margin: 0 0 0.5rem 0; width: 90%; } -.div.tablist { - padding-left: 0 !important; -} - .parameters-panel { margin: 0; width: 90%; } .parameters-view { - padding: 0; + padding: 0 0 0 calc(var(--design-unit) * 1px); flex-direction: column; width: 100%; } @@ -150,5 +150,6 @@ main>* { } .delegate { + padding: 0 0 0 calc(var(--design-unit) * 1px); margin: 0.5rem 0 0.5rem 0; } From 4e83f551cb2b82b9df016104d6e5809a10a36c5b Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Wed, 1 Mar 2023 17:11:16 +0800 Subject: [PATCH 07/13] 1. add panel icon 2. set text as preview cursor Signed-off-by: Shi Chen --- src/refactoring/changeSignaturePanel.ts | 2 ++ src/webview/changeSignature/App.tsx | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/refactoring/changeSignaturePanel.ts b/src/refactoring/changeSignaturePanel.ts index fb19ab649f..4222d73dbf 100644 --- a/src/refactoring/changeSignaturePanel.ts +++ b/src/refactoring/changeSignaturePanel.ts @@ -1,3 +1,4 @@ +import * as path from "path"; import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn, workspace, WorkspaceEdit, Position } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import { GetRefactorEditRequest, RefactorWorkspaceEdit } from "../protocol"; @@ -58,6 +59,7 @@ export class ChangeSignaturePanel { retainContextWhenHidden: true, } ); + panel.iconPath = Uri.file(path.join(extensionUri.fsPath, "icons", "icon128.png")); ChangeSignaturePanel.currentPanel = new ChangeSignaturePanel(panel, extensionUri); ChangeSignaturePanel.currentPanel.setMetadata(languageClient, command, params, formattingOptions, commandInfo); } diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 588dc2453a..096071b76b 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -360,12 +360,26 @@ export class App extends React.Component<{}, State> { }; componentDidMount(): void { + this.setTextAreaCursorStyle(); window.addEventListener("message", this.handleMessage); vscode.postMessage({ command: "webviewReady" }); } + /** + * Set the cursor style of the text area to text. Since the text area is + * inside a shadow DOM, we need to add a style element to the shadow DOM. + */ + setTextAreaCursorStyle(): void { + const host = document.getElementById("textArea"); + if (host?.shadowRoot) { + const style = document.createElement('style'); + style.innerHTML = '.control { cursor: text !important; }'; + host.shadowRoot.appendChild(style); + } + } + isDefaultValueEditable = (row: number) => { return this.state.parameters[row].originalIndex === -1; }; @@ -496,7 +510,7 @@ export class App extends React.Component<{}, State> {
Method signature:
- + Keep original method as delegate to changed method
Refactor From d419eab47d5bb70c3b4168897660d5f04cc0e40f Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Thu, 2 Mar 2023 11:28:30 +0800 Subject: [PATCH 08/13] add refactoring description Signed-off-by: Shi Chen --- document/_java.learnMoreAboutRefactorings.md | 39 ++++++++++++++++++- document/refactoring_change_signature.png | Bin 0 -> 33537 bytes 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 document/refactoring_change_signature.png diff --git a/document/_java.learnMoreAboutRefactorings.md b/document/_java.learnMoreAboutRefactorings.md index eec0591ee5..1af09cf3dd 100644 --- a/document/_java.learnMoreAboutRefactorings.md +++ b/document/_java.learnMoreAboutRefactorings.md @@ -18,7 +18,9 @@ - [Inline constant](#inline-constant) - [Inline local variable](#inline-local-variable) - [Inline method](#inline-method) -- [Introduce Parameter](#introduce-parameter) +- Method signature + - [Introduce Parameter](#introduce-parameter) + - [Change method signature](#change-method-signature) - Invert boolean - [Invert conditions](#invert-conditions) - [Invert local variable](#invert-local-variable) @@ -469,6 +471,41 @@ public void addUser(String name) { } ``` +--- + +## Change Method Signature +Changes the method visibility, return type, name, and updates the parameters and exceptions. The above changes can be applied through the call hierarchy of the method. + +### Example +Let's change signature for the method `public void setAddress(String address)`. + +#### Before + +```java +public void setAddress(String address) { + this.address = address; +} + +public void setAddr() { + this.setAddress("Addr"); +} +``` + +#### Refactor configuration + +![change_signature](./refactoring_change_signature.png) +#### After +```java +public void setAddress1(Object newParam, String address) { + this.address = address; +} + +public void setAddr() { + this.setAddress1(null, "Addr"); +} +``` +--- + ## Invert conditions Inverts the boolean expression in the conditions. diff --git a/document/refactoring_change_signature.png b/document/refactoring_change_signature.png new file mode 100644 index 0000000000000000000000000000000000000000..6690f7471fd26d4b258e1ae6211fa9cace6c6636 GIT binary patch literal 33537 zcmd?QWmMGR*ETAMgp`DIJAg=cgNO=4r_u;W4Im)h5`r`gNDL|6A*qyrfJk>U$-NNmWC4XL%N6e?%g8>E5Fpacke#! zy?fZk__)ANzTg=003TRxI!Z6@RSh$20Sg>k1$Bjc_iEz_Z!8`FYXTQ#L$`bPNV+j! zSbfeV@9*6UNddo9(DOFiTf$3Yv~IgN5`T`(k$}Vc4HOo~^6rx|Ug5)49X^BS54b|U zs3hgxD9ranQ-^#i`NEdV@~ZmjCVA&QwsGQdzk;SPC=Pzuy~~61B_B;2&f>lOT^ldd zt;~qeu%w#}LQTE{Zj3mg7llmjvVa2yy9oKAZBP(+$tif(5cq_uSU_39P*ABJ2kn2B z*8ja!_qf@gIX2)Me7F;F-W9&ORAbp;xN21=|M1|WeKMs0tN15K$ANg2_rVJSqy$>Nd8(`W{Os2T33Bu&8zUKtm_@@2%}i@HQ^yY(vr> z8LGs@%%6p?)Jg8M zxpo{_?;PmDnyZnsx*O;U9g(BqTafpwp4aH8-IQn+%uH$HoxxiDj+aABYPNjzgomj( z;D}}~VY3U%77b#!DcyAv&5@G0*%-WU)SCW)Sx-^Krr@l9hcnQ6 z&RLe%+&-KW07EC&_0@%X!_MbVI{%5USXq1~IFFdhhP^oz-0pnaRbzh3MKtf-Qu8Y> zy3aoCsMw~1-dtQvb`fpP+mA(n9S@g({N_^UnmkrNf|SJKFN&R=I*Y&Z13$FDHiwa( z1KFfFupR~O?*151aAtXRvSsmAut9O}exo6G{cg|M*0@o0oSm$vC+S%K ztr#i!s&~w3JzcVG;N$kz^JW*nSr7*u-UdbFW1~iE)sR~7K=Jz7hdHy6_ucGZo%pw+ z{j<4g41?K>RR7@eJk5XD++>&U{^bkQZK4!*DyUN~J4-D#(mM$1x?i|{Keg$g&RN(i zskG7(^Z#*Kpa53Q^V7;>&DicAD8}P<^2UvHr&5`vs9L7+byD?27ZZKN;bcLh zs9oDI+W2z=uKeSw+3kOh&&%_h=B4Tx^P=#EOF?vfw_K_s$^vM26ZTFCWbm{$aB&A( zqWvpD0>?H%Cl(iWz0z|nlFM0wPg<|d9N3PoHdDijJ?EeDhxb%~`&i%|1;12$7qxeu7+RYf25aB#cL#875%%F^&%KZZ|`FV?^O9V#xz zM=~;FS3fPWQDi?+&pGuelPc#j^PmEb+hGmcdrL5J(A?V3Qokoe?O+@=FoQtpZJW=g zxH+YEA?*X^2g?!Y605%#0)gWzHPd4HjlZ|7x}HriKcwKaJ#d@f?~}aMSv%yf96yG9 zt?|35(8?dPlU+YcL-|Yvh|Dw^h+gMr>vyF|HMqF`AY3TllWMF``WDWiKH+&cZ4Q^* znea7XY7)frP-QW{tnl!5ZK?!`5q@?P0MMr}xHT|T$VUb(c#?$tPy%8G_Tn$1$BIX=k#52WANwITE` z8*w%w!Vi-iGj;dVmcN^R?1U^%XN2{4j3CC2Y2^LS)7=&h{#^uZlpMk{$#eQ%TIB7)TKKo*FQrwX+A=u9AxjEVdMK&jrE>0 zvhk_Z-M}j6QOiTHA)_m!ZV(&Oov36?Y`?C8DtUnN1-OxiA)j|AZ2?gnVC_wlgXl!! z>>i=aKl4{F{Uq)>i90oh?WFDh7Sz&pl=67x4LN*f@fZ^7$Ogo6wZ3S-x2@+BS!{+z{3;{2^FOyRQ4)UkzVrbRj=20s?8j*GmETWw6 zWcIi-!d&_^KMsW(g(P;=&x)z}TrQ=}7fsdI1era;vl{Nb91fvBnL-Gt%pV7d-n`7qmy0q9CtnFAt4Xt^3N+Ti)rraFfVvOlTK)2>-8VS z)gkU`{3N*!>$J<+-;Q?p{3rAYZIJgGea}{`-8{ZV!6PSJP zw9nR5AAC?9>u>2zBXj%BMOTpaivlWh(?%_ovg<3A=I(UgR=DC@(NofsRdM6$|U zXSjf}lMj@g{r&UWU48s$#~Kfs(~hPH?Nm6;74bi2DifE^nt!#9e*V(+BOAK(gw8JD(CE_toLM5HgYfhZ8rhu@lTqOJ(VP)}6nq z#G)8@t7Q7@swF~e?oF)fl5|!wom00B!1E|`XOqXmS3`bqC(9*cT`+bC_cshG)!FSJX;Vyv{GoWbjVo z`#4kmLQebfzM*-D`DbKt*v}@pEc#$>1+7YHVUKs&@tWB!hLqps9>+S+KkF%Qw{kFseOn%cUb1F{42#&vv|0x@3yuV1pf2Ts{!)b^~c&??#{Dynm>~* z>P~I{b&HiiY`*9~ug0?yid+RyE~5{#1gxfwEqbdpNhi+WKlau?Wp!USnJhu|UCy~a z1T4U^7AKu+4=o4$r3%J<8XG9~$0+P~vY>H;=+w{j4vii;(k0S+MgDH3l96idZ8un{ z@HEj02yq@R>Bthe#5%oMc$`E24}7ec{kc@K=e5&9IdzZGSEC_oqu-ur5n`@o-jW`B zanWmjxEsHl1K!oHx~@*kRpWwhmD9}rt@eX}_ivc!NetaS?is~YaLsc>G%k4-FC{N% zfY{W<#Z}`-Qv24+_Znk&=EzH$&WPmWeime&9ngk`_WOpa@#s`zr&$;^UWnZc`Z#lo6YFBsi^jj!Ld+J_9 zXZ>OaZ;O_3&=wF-hqJCG*0^|I=ci1v-X@*zl6LQ&!+B-?<&2&2=_Fn8GVmF`v%@>! zoqFam8S6D73EBC}e4;-ebob6X&7iJdP4$!aS!P_5Os^V`NNOIfYLy#b{JIhL@}!s? zJ1dc9BtBzvbz1Vl6BiSMomJhgW5%blAIhzL)|Z09sjrwvZwdpklc=TnYzKfFO0i#9 zGAaAp$Ru1lx?8q%oll2-o08`B_qWuL!z>>cRtCz{iW3Qud)CjKGn7>@^`fp-*Qi}S zs5^Le@+n-QBi%{6=kUkXv#_$e;PJA*QXk(|Ko~oABJX&Uq{WO8g6*>=&p1TpBn-!| z%Vh(FY-!>JDKZ>v^#2NeLUvb^$QPiUAjageiX_h9yt*c!VL*ws0g*D=4}3oEh&GA^##;&H^|D zgT8|hbc>hyZU(!%`{IGocr5M|xqqNyM^9Hj?|R0&6T!VJe$ub2^@8Q~?{`ljK0v?X zTlEu+E4MqtZ}Rik=;7_>6oL-pcSnaV9j`3Oj{^TuFGG9~vfc6amp5whmsikjw zyY8eisXp8&tOs&#KZQiji=o!|ELu=-+Ekb&kiwTsKe%xXMVvX`rtXNDhF@}?YMe`J z^BysGEVwr!f~+8?t6FuAeC*)yRx2oLsishW0?2;Ze%3?<(=p9=gA{>Jol-R+UQ z-LFr=)LmA$lc9I&1yJ(cwvgsGoIGN zE{os?M%-F*H;3HeO5F*qrqDCp^eZHw^=y&98M{PO`Tr z6M{8aasj?#1wV(x;W7-KvlgpX%-05D@8b*s1WU-*%wpm(r9YS8(+#rv;80Qfde$O> zu}f;Ff`ZD_eaxGL^PuA{Fip_vwKpQ&wkwQ)qKmg_k$l9#f6u|3(EMhN+Uk2^>&dw4 z>a10KWhN5X70%n=5jr5sK1MH-i#_KcdQW)3c}>?t?%yjy>@+MFG!icFXe2+rpLL&5 z@BTve zO#&}BS9j}1NTm0htqg3^t-D?j@+JqALGVLLZw>tR8Wt#t>icvxW%Wy^l#A3esLcG2 zdUj3WP1|MV;W>9V$W^E+AA$MZ)lBzxc{LN2;rqm0UEthR<4Hv1(fRRmQYj%2^k}tt z$816P%RDM*f<W-6ssk8lBz33k@0j46jG+C;lf^%92&uz`6_Bc-} zzonaqGEkIPHX5B(HPpGtDQJ(pa1%PSWxC!SSLG)Ty4^8f*lj`P1j%K3OdGCB!!I?O zulHM)s-YmRXM6iP?l(8f=k4Ned3y`MNtUs%{4$g$e-4Zs1fI7Xey#Rr_dl!4A!fe2 z=(F-W0+6k^%;iSXYAb3u+Dod8V`(#L6SIe`9=)Q#g}DmI4uDZ4(40J9uie#auA#y` zQTEfx-c}&wYSZ!671O+Ufwy5f>oy|WT`YH6+ANdIK+$@K%DIb~5Q3jAZhhG-aO1)X zfSH$xvV41@T+mzcB+4t5t+`;rx!sVVAEdq+<&}Q5h?Juyk@}abQf$%4sNm3^99|!9vEwibtYRaBYHXjVg?L#+HjQKcL zbI>Qn>&JC_31=ffSkWC+P20&a)SU><1zzv1{}g0KbTZEO#xpa=Tb(J`?(_BBoG&fVS;2d0U_zl@NxI^01U?^JileYg*XLbxh9o-8d3#WU~AqmSYX8#;^X z_DO>-x@jtSe{4VnUG6E|50(mM3wRM_SJruZN$?Jz>cUmsseph6|BpS>3$4HLE_Hjw zeKE8Y2kqTn#YN9~CNQLwJuM)1t2oK}aJGZ(LMd?gXrfdr5T&nF#RyyqpiF1rxja<=<(r^?lPPRe(Yl~*1-@44Vaeg5jDIDW&8pj9rvfnandC3b)7N~t$8IzHapuF_?u%|j6RF|DSB~pO z3x$bs*~OW<>%FnjUelI-!cOQ`OFDA9Gy0J>tcG3_LLFvyrY&z=#tF$clD*B+255s# z2AkQYj@!Lv`CTtga9MFZD0CcU`RI8+7yD;x)<;jk%JL_#Zd?LS%gwXO?+Yndq9f|5 zQi29}n_>#RV-tw9zMHC5kf$W-Oh8a;O4*-kmQ8Igu*@AQ8@_ur)Krw zNdBAL(SvG7ota)Gi6W%NGKp(CG$2JlPLy;g&ppK{jnuP@wRYa2y3LZsyfKc+YnL=c z3NlK^E04d?7sp6n%(btI$l5>CbC8tioiTA6@^)5}`qSeAkuIv2i#^EP(@9uQ(Pb9gPTacrTc7E6PpxF~knwI^08@B#D ziFfoKN(2pcr~oPclidrQ-^}h`4GV|8M#DmaNCHt{i@0&;W)wm!`3g^Sp2QqI*!6;&wkhb*m zBrXxueRLsz0GAj=Ki&gJ=BxIaI1$l)PHdc)?(=&*?eX$j8*vIgIL|IQR6vZ$Xin$$ zOPh%RXXReI=|nh*i0hB5?Q*MM0l-FT*m6-D?`^b(Ye&DXQDZy#%U%86YES{iCb!<{ zJ=l~2JV2Z(k@#mE&1xh5-johRvsJ+6L7QBEdHMNW^Ovo3(uU82sRw1$EMlQnmjEir zbeQ|q{*D!OX+WE( z*MQ^-GztpEJFi6Enxl_X4Ius_vghxu8uPaLRk$mY+sb3lF6vQegm?$7s?gSj(MzqI zj>k{$d`%r~5~g2(dbjslx?I?>4;KpyXN8uLv3 zgd0Gy36x~s8)S}S?tH5ra^Lbw^@IcND>CbyZ zmw%amj}h#gPH03p~LioU%UexzvN&G!MeVGJK&819`sv9( zK867ii%lcNJ6@P?qQKZ9w>d@k(I3$-^q!0UsG%c+TtnIbA@aBho9irPC_=svM7#7^ zM>HfSY{7ym)Ff~>TK!4$6B2cqW5!U0(bp)b<-6Al>qd8E35QndN(+)8v$7zbF_s$!I8>2(qG34YbKNsHYrO@$%>k6N3BfgULjkAofd=P3wPuT>Fl7TN0IY z-rVHin@zYjd#l3zhm`A3R||0T8*xE9eyF5M4MC%WMH^U5d2)V%KX@bLeX0+LM|$Vi zPmn$RRJl`GVLp;85UA@tVR;gKNQMX8Q{m!2KMbl?NYA_=XO3e|$+1dz>K07t;5#xr zGO(0^gdWe@Sm!uH3);V_CdF6^EE(c>_S`GG$j6&!q8>B<6xXu&Eu6ex{_b*Q73NSC zsg5NzZZEt<_B7tQlp3E*?{UUgvfQ^V`Hu^4V^war8u&k0!3f7 zF;*HqiM-SNlOIGf#22MrNOJ%nz_({ra?`wULk5v2&#LIz|KfZhz;X~Y*sX|T@LD7; zp9RH{uS3*(m;x_W#n^wpGit;-_PPK=hRW02J*X76g*NemB>>w2+P|4k z+b=^&0Y@~i2UTg{)+uSU=gSS&;R56C?Up|fW85m4GwV4Uo^ouMdb66^abxXR^rD|j zKB_0TQd-a9P4Dr{!|bSFCvBymMX=CcqaYZ&?8QpxT+5r%e!bdK%1+<*jkfF?#$WT? zOA002qEhc9C5H*lzR{c6F+G6uETHAjEisf-uito?<1PY!1m1i_$nxU}T)7qS(0`+azff8Xe zNsUt-b!R%~$n$Gm4F#-57cNY)JUS#7(7#^QN0ns#5oi47o(sG<$8YlL5jr?KJy5UtrxTNT zv(CBFt`37Y=M*$!DOoLfkCYz?jG0)QeY@{e+pny%`)GkkCG!$h^E%!r=-&+4BDB#v zf?;1qk-&aDyGW;D)T?m*CnYmyj{iroo>s-@M!U`C)%DG3I1>>?W;vNgs649DU7M(> zQTH#m+LMXW&o{MowVamh0%yj?&(88_^^Q_TDV1FY-t1q!LUEzJlLLvZpPdaTRJJI1 zh(fvc3-tGH{>slXO~L$Bpn677g@{FnJT%#n$#1vT&0}pSzc{Nnu>ZBT955nR>fngT0hU(^Z?Ne**RJsP&i(%<{w-$Qc8 zLhuaLA~_sIKrvJJy}A!taH(n!gy^30kIj@H5MjF)3i9|L=m#q0FRgg6anY=jOByNC zvyTjtO|gwDD+RF(+J%J%w0GdJf!@Y%rLUM4B6M`oq-?Cs`6-3k#E+T~Jj<)$94d<+ z>*%189~9>vtIy(h9V_Ud@96N;7EpNpEVO%6MaOoWrh|}}O z7{`t!sH4{1h*!*;FOgeeGdIRTUJbXl3slvwdeE9CeRPkpC%an9JQ6s$c+KR&Ak+-5 znODzglogM;@-NBPcQB(Sjv}hixK$`G|w_Eihe;q0JU4^Dazb==8)2Xe_k-1@edd~n7dJY z1^`RD(4HcxEG36JjMAKQF#2wIg>+3BSHkmb+3!H-zU#0dI^wf5m+RZYyIc5O>|O11 zU9PxVOYD)4Nsf(pNX1$T*+^=yfOX%l&>7S8M^vuVm1{@ew~8{tUDtbitm16Rzu8n7 zPeB}Jl?FNYnyP`G(N&@$xQE`(WrlAUUeD{j^mv9F?O!`v9mnC%neALG=xZpURS9getl1{ zJ2d&t3@3AMqP}~qb3-9^#w@Cb^PguqpW!>CR6jepl!um)n;W?|}Z{7g<>QOMF-e6oX-Tf1) zMRgPaqLT4e=L)L*_35C(v(@8GLs0WJpbdb_s zqypWyKCr4v3c8xeK|t2}KI$aB9D168bEp-86*1u`T|Q5AwR`=X3*IneMxTM@-ovXwpr-1^8WCZD{AIO_T_a$Ou>29!ca;MdijIm>egJuxL6bG=bM4wm^^bhsUZe zSl{o+)HciGjRZj9bTfJ_k~zm|-2|Qy4<*{gy>KxmyyhR`WE@zL$)n6f1WnR;P+cdY z5)amI-lH$AZLZza3f)qz;%#g%UFSnV2`#PK0-f#3sMYk5*+75Iqnw)-a&3+df zs8~wGW_~0c(smvlTjShZ`v7U$PWl+NZj^A4`a-C`d3SwsFzSGH)q>t7qTCWyE|mCa z3@0%NjBNeOS;52K$G8yIGU2+H3@S|t1o}5f`yZX9aOppzwL(ENtWegFj4I{cg6x>P z7S_(c@cne0WI1e59KashZb4y09!yzl(RW$TBat0F7pqk?hTk0vXm6`uc!b?)=9{&kTY3V>=b?@~q}bV^?GAeKWnilp%MK;vb7Z zXTRmB%ndtQ1UmFNX+Qy-m^FbgX;~n1+S@XILI0tY zf0gR@B5TY`*aBm*Ri?DFYx(LLWAnfKUqd)j<6|aJkO!%Q5Q6o?1C^ww=#~(?;5Q98 zp^Zp)Be6~`^ViTJC!>kWU%Y0~1h~z{3|@A^oe6;QAk_b_vt-MABT>n6NU7nDJORN8 zdc1hrPbUlL1MLqYh-g1ro*6BL$rl;hefw-l@2=wPljWlN2K(s;i~c)26W^`Evu>@N z*GUQxo(s2^kaHb#5bV_voOSW=)7|$~vEIer2I)H)UEu0s0{)xTN9bUPR40Q6J$D{50cpPWN9z}!UVFaof9wXD4AVax7!)w5E-vQ>P04wt`K zB@M#gscbS6E0Ea|zXS;Xk#Oa*LStx*yUy`SgH%NHf@L%>6^6m98IijV189Hl>zCV? z7pt82Sir;bH)APRReA!iv9$;vSW~7wCY%DDYAiwDI|2l$#b~a=QSGqQ&U?kF7>qE$ zL5|1l?;-{Y132KG-ytI|=u6~VzF{3_*UhhQ!x=qiS>=E(tX05fFs+CpHd_mp1}*ee zNGgyl(#WEFjp5ES=yOg1HNkHtv#O#YVNwz}lgq02VJBWUcXcaybuI-frW8h^gudp&&rbn zH;V61RRLG<5ukNXM84i2iw8#Po|=1PAUu#+Ug7W?g!pPflE%aC2-0dIX@HbnchH)L z-7#!kQ#6hU_AyX6`20HNtWbjWVKM=RH*BP(_D2GjP6?c{#bvv+_)Vij+g{h%`y0Rg zADoW0JyfnscV|rl=78=yXI$k?KIiJULjAeZIX@9eS^Rv)eqx*O!*l6LIsV3K9W#gjG+keNf7@#f) zwgQ*5+J3zsO=>306Q2~qJhUDs%_o+%;scoAj)XN{@{x-auLxhUx{$q)B9n|9w>{BU zrWP(z51_t~c|eirsOF`v`rI_rmN&B=2iIQCgkf0re353fz(VBwUHUJt1huh>$Pu3! zk_y6suf1T-0J?~1XLMeA-{*~J^BYSd32%YGhUi!Md)U}b7@9gz!>t0yvBJO>3A~w& zrxSgqmL|a3u8G7|cp)V&k$Q22kox$CHR3n3Q(&4y7T#$^!?e*VQ2TT<#U3zbQs77q zW~>foJpXLXWl2}1d-KO_Us!JD{xWq&=wY_eA|<{{pm2=6w^pd)qogXvq09~)=N?Io z*jMOQHUZ?~`nfw|Fnw)&a?`x~Fj=G~k@Ho$^O2ln1jn|ZJPmC<$6t_tn{gmu4*Q~t8Q`NrhF zL7%RMBzqOb!9P3XJ-vNdHQg0s$MX?3^_44q`@rb!wh+6$Wjs{X*6DG0|(j zgpz6`>;3JMxrnianUUop<=5GIZqgc!;18H^W>|8S0<#+)O%}h~0OG-c$R(;9e^U38 z0$b06PlRu}K9Xw{oNBIXVP`zz5rhxh)tf#iP8SyhF&=d$Dlg$?LVFL1acg9@AqpeJPP?qiG4*djQ?Qe3YVbLna%N=ZF z5PE*>-_0gnB56^~)?w4()z`;ISjcBc_n%?~G@KS0H5vnaEskQK-+PHE@}~EXcl28MMCRZ4bIuSu z2u^1G*OL;<*7%SxhrIuQ91Gh?<2xG^OQQ@TUzAyrysvGPfc=>1vP?5@mWxpx%e~7F zJMBKHObztX>!pfLnMIq<$FF@?b?VJDlXS(xusOaJ?6Vuxi0whaek$aicab}=(NbZw z8}SyyZ-=vl^rXE8BsiPj)W&RG^~TGihRor|513a(yA6;wBj%+|DxHGt$6U0WX~q1n zkD3zPEdPyutHqJeBAlcV(@oMk@UMDoe3(7^>79I5^o@doP`?_B;gZOiRenx zPj9&s1i23kLPlMP$gMCJZg>)V(&JRPTsZ(G;$zTT*T1d>c`NJtzrSvPJx{2eSdBc2 z(%CZ4f2RBBx69h&a)WfdTQY-Qf89Kx8na6q3Ig~@&)4TTJ+3-iV5J7z7G>E7Sg+fM zK31ZNX)ihOa>J^nwL_&oZv3eMHgvDs4{ghYs;|juzGz6`FW12HC9l4&*Ra<*`R02v zrPR#suPbNSvZ5E&*txQ!>tDP}mx|hv)dW|NPdG?kd)noukTGKcS*|z+e2kv{t zfD5T{PFp!^RMvCr7(3{ls7pCjvM4VTH}LAbKe${gsmLcyBBMseNjbAhF2AWGWeXFM zX*@quh83VGxE@(Z><#<~)o*Z%LuK51Qb|4Y<>s^x^%iwM?!1r5uL8oB?ZQ`Rtcjw} z9>%l$B%=PT1oYZY^Vl?o*Zb2nu|I!VR#ujD_W=Dnk8kgS7mT8Z*n$QAw!38ly;meS9Q4(0!Ko?MsnMZ9 zI2B|c_ztk~vnw-lZ>hoV+_b?^gwol6yGb1W8_@obHkGo0tM`YThEU%QjOgsAPf|^+ zGx8SUb0EooO1#&`XOQ&2oD%ro!o7qfv_c!dcRfmt5E*UTayhrVw%rPNjFdJ*u*cZ} zXLF8r#u!L-vG3JE|Hjl~%H^Q#yw@J1t{&FL2k^+CFV^P_piujXBJ1kEg8Ws)7{Dp9 z^67rpV7jn_89nSrpfW02Xyn?*bvU&&-63>_bcl4`&$NaEFz5_XKefXgVLfyvUyY48f_z#l1%a>`=`V?=px0B3ap z6b_Tv?7L7$yMxzmqk#E>4X}T7;Pba%Sc>Vv&i~s3^iz))HDo6G+PJsYsxLglxm_|v zsDR~5(&ov8W_#cp!$DPw|`$o^}f{xp~%%I-hLE&2vFRgQ6uslLPHlv z>)fc#ma{qAoSHvu(ZA1`{SOtI5ikdy2m0|1fQz7yc@U#}uf~cm{|O)N{PCRaBcSSq z13*=pem?WRf*|h9g#Iv}`L<-}k|0n|eA#>5W$YF8XCSb7+TkAucS*RccLWsfRj-a~(@j`{h?|q0h{jKZ3}Do;7)%p%ssjJ!1t4XKT1GtocBNOK z)T13rV~Y(dwo*qRHc!%EM0^hcH0vZYMk2jila`p2UQAJBn3Y9J&Be!*>@FZn%EzRm zc?OX3>>Q+1kY`d!?@n-pZ3e%HLzJIBrIGb@mr)`hkuV1U7U~dabW1-D1JJMKC_Oii zuE@~(tNz+)@DctC{71)~kp(`Xa0~1ccMZ4fcHlXF9eGLv5^QpFkEa*s z!#x2}Z!KzwEjZTA3T9-q9s-pv5&v<%VW)+#9!Mtu{x09;B=ZWcc;)}pylNxo%} z$k)!A0?#YgzYp*kZxw*0A@jQOKo@Y7iRhCH8VDwn z{%`bR&{6k-7cjWpLz?kRjTH@rclmJdDrOU<=4=7t__c<@bO%MhD(2P^b1yI( zE-91A54qKnl#hURCxK5}2j(Vu$KXdYkmvq+fm-_x>N_y`iq>&{h>NoSO%^Xr% zxaKA-CCtnxl1wRc^qXStQK?ryC*94XvooCgfRjZ2#U1*Vo8Z2DZ+$VxpOs7v_9UMh z3E|-yi|+5NZj(SI9C^N?P7H{cmqP|2IR$V#7}r?8r0nrfG-UOv(<5vL{6gVX`+aZW znea0c`7cU3UH710dZ>(fs@@u%0S1iguP?qC?GRV3`iqN-g1B{@-}O0{#sinoCfj+B zK=Ij@YGEQ3j&cBX;i(9J-j7G0*>Tn{b2Exwl>X56eN<5l`wS_;;=;XX+5hR_I6j%h z_{;w9SD4HubA2|TmQ$9wM94s#3_m0hpW@0&^|O?)lxwo%Cch}yep#Rs`#K6$`k5kg zTX|oO_0$GLcjIz;hVz8Al;uY*KkiJhJD;vfTJ@%~-S~4) zAUm`mcq177IP>n`A7}TUOnRysXT4j?mVeQqz!LiF^8ldH|A4xT;;y0r zHp5Tv525qw4?!2ioE$4|D(LF=tL+OPrU@5bj7Kk1yyG;sig?IGK5B-+MRra4i$XJ@ zjZxrq^Z{UVB+`WUG1dZf{v?N-$`{jLLMqk{2Oi&)ZZ%({nI@sW;Xb$Ww9udMRy2SF zWHqaAfe(O$3J3+tJs4V`ll~qv|MpBj{$CPnDe}8!xX4|tW_SVH^?!a2EB;>_Qp*OU zfH^&2Zcg6t|Am_VzXbsPU;N|$#u5#awwO?|m@)Mt2E3T>yn%vO_YJta?T5Fo0Z*;8 z-x7Nuz{ZE4&X@z-4&-k#*XnNyO(MWvC|}3AtuBX39|^;7F}S-vBQhwp=H2fD-@RlB z!M#Qfm5=uUgWKVG$DJ(wGvIkwjq-wfA97b{fuG6UZfW+-{Bw%z#dM+=a>!-gzWDRe zdY`Q#QXWJLiaTF)(~KvY1wWlOu!?m7x{-(oCf}$3InMt0UVxDDGoU=e1nfq0F`N`Y zEdeKJ&s%U167bxebpgc>JD__?7 zyfsQ$6G9YkUbmk$RQDVJG`T!u2Xl|L&)x0$x;yVe+mJ|GDS=@dEM_u!`x2Q`Es~|u zU83K-|8u7cxFTjx;D&23*2xi9;6M6C?8fVtJQqF#Uee#I&TXe})Ks>V5{vwRJ72eD zc>6A+$S4|PtK_l~ zscQM4rc5peV5O<|v?qfs!N#d&*MyYpH<6l_o8T z#(!HkCkm{j!q$HHnLiAZlK@Ans2u)$A~{jW)d}r-qJcmTh@IL#b8DiwZz~P`w&O}a zlezyvCz{qaQ@f#m5&$Ke5@@HUrD;Tqfmk@_Zp+s{+fo~dcFeWOD8TUJDCHpK+`XtdI24SrEM|GzdK}ws|WH7+{FY?_SejH$6 z-MeU~gN8>i2TH_6ulZAi7hWQgcmy)ZFHU@%pZDJaRu{TBNkrO3u1%CqK#eT^?A(V8 z$M0YU&+EOWKHKo4IgH~K9=Z{Vmy;+^V1$hU;1-ztne@J9>6meFu2FfRfed;d$^s4b zS66Ge^uJh*SOq*)C6j70fH~qQ-Bk{-H1^FnciejV+hhXT>#Pxw1V!LGDWFhT1g3?+ zK$572dpO@fbOa4p$xn+%N`L%$V&T320|^*z@K?YxY2iL7##p@!%@?D}#J>SkCp9vi z>5@a|mAj=aok|^Qq5xq)0M8(Qn2?b0{>b!_E`B*|McSP3IYkX+4aIwkH2$W=k4K55 z<8x*rUS(8YP_+HlDkk0ue<%+uzlL0e0sb^T#c-nj@JXeQ&$|p|sFbCYKOUnjWZ}Rb z1dO2-{OffwphzGqL8f@YxVOYQ6d^WQ#%&;VZy!D{%zffn`$Ny{;Vvpbta(CD zc&Kpu+jKC8+G77R=3M|HSB(?Miu(M;E3O|a2J5#^NUjT;^2W*??ETYIuNi7wRlrWT zvMD&vxn4xB`HlX_#sgzt%f%96pS7LMbNkIifs&3CLKMgq-AX5MqSx=2j=Urr%2WQ7 z=C!SaP5uO-=+^|vcBmA;qHq}mHe3ekFD-Fr-;>#H~)v; zc7jGysfhDF^ymnoj6xKO* z5)1ky{gP}Dwc?>d zY18q;->!Xm5?5kW_q@*)INQ!W2&n9ox|E$4kM!=I79im1r`~CX>rp{?$M4>68~{@% z(1L91?CJm1^liWYsiyD#cM1QmUqAc$KO%$wX{n#OWV#LPmcM4DZDNZq-Me3bsxx`* zO=ygP6@}a5AS+;R4x3?C+gmonndxoY4F3ft{x6RFKb`3R{?a$*0hIxZKVYaOrZ#a= zz?9!Ez;uxvFfIwG5w@9dOjaY_V)AE(51gxx-xR=0wDWCjt5us0y9oOTcY<#Eg5E14 z&VT~(`R+33j)1@Q@P*jR?WJ)+d57r=jQA3EcinL}AZPz-R1heI5;uVM&>ybSHZfak z)s4x$qa8taj>`DVffp9us3GzHh{9E^$A1rj8eyFyl7i^FL;)IfIA&x3e!K5ASIn3? zJP=92iLp&sE4E}KTMCwKFzk5w@YXSmD3Aqxdl1yu2jnUmIY4C=uYA^jPs9u20*UqBDE_Cqn`NABpq2xObuu;|}Uh^5zKge{%I zCubhH^8%#wDxvws3rB3WQ)}RCtPu?4allB1!Iyf#5f_08O!b`mfP54`VrqI(4{!xx z)+vS@a;nh3Gn+$&$2y`H7_q1wP>&)2*-r%hD3(D|=;q)K?R)_^_8I!}CwEHn#di7D%SNY`2IQ)vu`a|&Hx8&7|=)aX9l4!Edbrb^(SxRESonx|C(;|l^XT}!XdM&wa-R|`r{1Eitm4)9ks6b{Q86?NEUS5&$(W!zGqH%hsbay z!2lvVN!xV^Oe5K~j{oO#rOn-H{L+v0dA+N05e%^M_Ll+*#p?ibtA~z|m6bQ_=^jr$=13b!lJunD+ zc}^;SP``k8P*Z(GP5a z%Au-@be4i=7`~aD*T_7FPJZ=OcPsU6e&;4w6y`7_Ix`p^UIPShL}w^oAH+)zkNP0X zYgzJaiKS+(4>^&8cE6aPX{VBhW9EtgR)QWg0YwwD1T;gnI8FfeiU-F`E-8l*@Zd{r zi)I)B(@0iZyjZ=MQKOCir2h=hVD^V&%p1(Wm5eQu&qkv6up@w4R{<}Qrwnx(6}@AP z0@0fl$!|WfR;K_|CS|^!?XwISJh~im4ix5S14b$Qn)ezPEV0OET%kz7BJV6XcQ5iD z@_q-_Q1+7{%WM!{rY2_6sTmmZuo?k5m-rt|=Rhkxc7kba&8 z%xO7GR|35gz~)WX4WYysObUJ&=#sZ!dP9r)_(D($q5lglj|p}09*woqEWXYuz|~C2 zf01)JXlsc$1;S1yh2<7_@g#sX*1O`Ag*FlKAkHv^G~@wok2E>U(`-~^2 zau`zLYDxbR8r$HGx6Y*s1x0r8AIiM{laQ8>@bYV$V@Xq1$!bEu^1EzKT2k2)Eu!p_ zA1iC>c?w+$Q!edbJbBz?NA?@#QMIToWtHYMH>@IAfvHiMlComT)K99gw{~lXfVutm zdNN%sTigPWNchE)RvNS5m&0C`T8iX}&iop`l3)sjWopCsq3(x_?~EYoGMpmSu9>c_ zu8ZU%6DJW!0EXv&RQmwLr|TkS>iqese6$o956sb}ZKtw=V00jh5nxTghl^S{xL}>~_M4Hke zASD#30U{;T*$?mc%~^BKpKoT)TC>JKE?V;BDf_80N2c@0qF=BZGEphUxNQ;jjgQnMq6w%VyF{gIZ(2wUjhT2ezK&#gI_=9`}!o{j5i zKWcAR@XFuzSjth+`>P*bJ~yDJDCoRrtS>oB+rZb$tw*1s$FE287hT>RJ}DtIwhySgBByF zMq7&$9(f-J+8=^x01`;XO8>ZsC4zXhb93CALIi&V+Mhj>8$AXLScnY zZQm7d$q5}kecf1$_H?SOyJi=pyyNW}VU4p*QW(urKAS!v_jdVqhlIe6qLb=v!~rnq zon?#mRLZT8?uf{<&x~n;EI`M+f3f{B?rE26^u42fUYZtw=Yfm#H;x)A{jr~XsHf;* zSvpeLsqm0-54ZzcLPH&YW%AFuNwy8YhHhEy5Bs{RN^^npFiuDeX)zab1IM_jGkc=Y zhuH(_Z>Dj^dfmKAIf5bj=U5})ae_vyhu}(G7r5E$xBE(3?z=qpSC;Rx0O-(=5cmtU zU#4${-J|>0A6m3oysO6hY=1C@Ua8I?X6~>HL;RZe_**Ts&iF4O9evp!N}(1Ez%~#= z@!zx%Ye|5Y`akw#KRdL03y0%ZH6>Iam=B*r8Un5h@cE*(fq>!#^RXCcZnFgKpQke# zQqqKKUi}C&2e#LQ@QC1ExxUWJz&co=3mIzlH|MKhA*?uJjEXWOI)KC~33%Xv<7tWF zI{ReA9q-aU!|nd{oCl|5H1{73UyT;hQ4@&q>Ar!E zki%JyHoAL@S$WG^B&xa4w4xDcPPmJ~Xs}g3tcbf}8nm}w0n8RPl&moQQ7ILz6bXSq zJXp>X7!gKSIMA48_bMPH`AYO%94vbqbg=L4vs#_+Rhu)1o8Uisf(St~u3_#>wV-a= z>~5{Pc49*j5PJFOyKrvk&rG#&@Ky!oBHTL+xq`7%`(T8o2TRxELzgSmx|OdewR?|$ ze&&wh04&4$UMqwaVo4uoGfXaj+fCNxd;&nomVtTyi?@e?4QO%|@17c+v>P~UV*a>Q zfM8zI;|Z_^-`e8YlDWBL6mSkJK7rwq=Argd1o9QLGxV*2#_w-TLzeCaeRe61a^e}| z4gDeqBjGplp!^ZHppKehMc?D3@~l!5C2f^9zxdO9{v*&x;g7uh{IW|+tk;yf&O1e`Uw|5D_QGsizHpu=CMcal zXbUtjhMmseymhF;Pxg!&zZ+yYf2z+>(|k@ zI%`CZlF%^E``AdoFDy4$zUc7^6hU0t#?`OgKXC@R_gweC#0((|iq0I%>QxItmjUI- z`RK0Mcq)U*ROC%|5%+TsQosqoR%V$ZAW+XKC=eP>=6?qFXHZTG(_90z;MK``XaJspUSexD4wFblD!&A5OwcE;655J$oLeP39EWm-Ot)sxVmL(~mNt2!;;uE_0u z&##1BKlXYiTNUq|FAds^(BMtjsa~fa46p1$YqYsX#ydVK)u#ycZ=svr=RE z;lpQ;%w+9P$qnF&^u8&fH}vydEL$8qAT4QN?PG z;!=pKfXs`T{R#wroX!+=b{{=|@dsaD$m-Oe$>c?!!W2)X^2wkRmqt9gTn#cWdKs4a z@*dZnQ>6xYX6}771I7hn@ydMF=U=4%2n~5EIHt@a#q$=YdNbF}kiL*zm1_fqH{%8x zLh-T*k9w?~pn&}D;7_5DStL(ac5(M_pX0MsdxA26nKNNaj`qi7i7{}0=y($*HkK4j z%1atQCmrw>ah4rBogg3F_!cxYVE|f}uix|m2T(%Tw)>&1IC?{!*ln&Hk?uY!$lM8xua0+o{`( z+4rw{h-d=4{6h*hvN(31|FeG{1idS1e(1A{j}X*pt^w3M7`8e5?9n%BEQgbG4|t)> zhhC3A`7`>S)VX8SOFn=fp8UA&4q^S6O+VMaNdP5-5-5=Z-~~Vp`#%~Jk#Ct`Cj!PC z$PG^KnC7ku#&53fvVeARyxpyQ;^5|168OXx9|Sm{Ab{%0mzNts;)AKL)L#(j4O>?R zN=pzEplnl8xA7MMn%oD$6gf$Ra9qIuTTYXq!c3w-yGL>Z7JcN*?ys&;f*dUFiNihlb2ZX(`5C!+GCFRBV0 zi38~-6wLrBGYk6$f!(+fT)-wUU-IT?rLaPw6s1507!HPa!8E^lTp3+I)HDfdC9~jp z0Q#JU2ag$>iY|K$CVqc!2fxXzxN;pT0+-0Izu!|VhR7g@F}EjS6a?i?KnjEJ?jTY^ z7w@Q-niL(>`5K}g+JWpbLjm?j>W(>7x5@~CDtju>h%V+bSh|%lmuC-K&AsOiZ7 zU1?%AZt(?z(P7xt@hu2vCRWdloc{vK;ORyPRL{*sTo}`(de+3GAW{o4;S{LKyUVsU zfBd?|FpEk9u^GEs;0{&#B+z zmiI!synB9FNl+C1DR9Lv_eZK*yd69hYa3_l+bWP0NlKLyg#QJ-jZZNQHb)tboPz2~ zFUw=gqN-seWIDt#*!pfMchVun^Ne6NaFcs2@HTw4^+#R#xvVXauL6z+#2vip4`%I- zcgaHcnbEfEe>R{eP94jsJ*9W($Ya6z>s^jr&S@#jz$i-lYq75Mc;*k*mgf2F8j$hc zOJEvyU7kurUQgzO(`n;gwkp%fM=-W6q>uHA6cZ`7QTN3?(lpUCZlf4G7>w&r>L5U7 z_QdtEw}K*fM|+)~-1hjqjK=Wh%;?R%-oB?7(&&$QFztK>e%&eY+k2p!aA089mg!ZO z&*%=Z&AaoH9!)vdheaq<*+6{9@*aKOAAz=;lYqgj2eCpGN!<%>7>KlpPmf?QAMw0 zOY|FZrLNGS@=eblo#eKYB|ajC_R=XeX!SlYtXX;wsv7_3nhJ*!V|Mkf?$d+AraZwd zW4$#w2~0jbga&^oxD19v1t_<^iSpfNl9N)IutFnr^h<1u!CI0PJxA`y?rym3flm*8 zvV>E40e$mvN7CP2y;nK~aIDa`ia`tz^g{o4X6nDx_5YU~1pgp2Ej}@aN772(8#|AZ z^+|D2A1fdKlhpJXUrbN0<1Y@{dvH*i&Jx>?gOBz{d4ZHtw3HeU2?of0QJ)Dv{Cc=a z`9J2o{-=&P;9Be8CfR0~f@Pkyb1N8r;^MQsTh}N}a3Ddo$jgkRF7cpoW&P1+{g6NS z23+Z$sJcWy(?Fqulve6$;MUjgsg6zHuwd(kN-C{`Pwd#09wDpeF$0b7vyDp;ao|V2 zP3v1Q;`mR^u=6EaOeYS!P}TDZM{WN6uB^}87Xv64?bh||bMbrf?MsJJcx`s!OeH0lOxn>le zx+%ykmjBtAAPq!f<50U^D0VMG?;>Nl-E@^oSV+bB^CO%x)sxv`SNw&bi@^G7omi+|YFC9^SY|Sn~tbEjDRk7LUQsZPIk1d2gz`TUgwbmEXr_M8&27~~~ zKuLH>B?JAFBhVrF74oLqePtSxe7q-eDMqM~`1Z$9)Hr&=<9QZwT!6-*1btKTvK~FU z4!Y=u(YzSDLLTL7V2Roop*LD)^>VJ>D%)&|y~}?;^{&O%?X8{2qZ5(}QRzK}NJ7~} zUf&5jH?yw_2G3U_w8`G-!-^vozqzkMpMdP=fG#5;CnS%JF>j6?Sj)UA@>mYKqQzD1 zGI+UPOOgGF{M7QwW~qy;fONsIaA*&~bC+pioEXRxxbywTN~K0m!NkN>C>j`042O|n zApfabdvs%K(P&KOPl=FY2{>Zd`tKS|@E?3WKuZGp`d^IE0@7!bN84>f1@+E>CTigC z!16BD%Dy$|6(CRRIC}UkxQFMQ%HNWQgF&JH?}PS#>UjSvzntF%N!U7P9piqp_$46vE`jNf;;y;!gLAol#UjHl_PPiFK#fxAPac8V z89)$OehWLiTbKSRSoEX_>H&iW9pum6C_bINlI1}KP2*oeng9oo7SJuf1aM1k ztAAQN5*&;WvQQm|3@9z3_Q2Ca4+KB~gFqd4sgmE=($~IFTYak&ND8h1n+$|w(+Q=q z9I8aso!N>(yHO$$1Em_-e0_0p0&)?WFT|djRX}uSRVy<%el%%;#6p3pW6@BHRqrtR z5WGbxhzbjb+R0*DOwO-QrS*7j4=r{8=b^Aw$HwkDb1i{M-e$1W#? zd;*X;T)9MWj1Wk4A_q{G5~~KJ2h4jM?t^<8vEO@zr$GpGgw7&s+TOplw1a^uc9*|B zO-_<)_>zDS&?DYLvDAXVmv+{3Y%XlFd_a>m0a-i*e1wiTWQ21+R!>_7@Mh#uk8R6f z|KJUv(kPK2L3XG}z>`YoC|m0gV~;TMDJuqAGWM(}A?ig2{>=4M=n@`q^aGAwmWh}F zvW>*QKx40rYJuuPIRtKeJ$Ho&S(otkHT?wDj`5l#Aa9Tz1G9K8+B)`67Rk1zY!L;l zo9OrfAZ&q@P`^N0k_@~B3NH)aTQ3Rf2z!6QS||_d4Z+G5O$Lo|{}SOS@;DHVsu&>! z4iT63&9!sYLcZTNW7Nmun15jbZzm(L20I@=GH`S0NCsejwzRGXEtcV127D15wEYG@ zx+Ghv2p39zeHsUeLCzJtn`1#iT1^WS4N-mJz z&^(inJ`>-bi*^h4-F`5YD*Wte05BFD_VeC3lkR};?=OO@1Cz%atJ|%LUp&fgV>hXP zK+XyCDo7Pbv$%AF^>)$GHgl59hswma7{3MdiUopeIu+@4Z?$GfNUA|&teFTfth9oS zJK$Nl`2ogpjz!2Pdn&ctCzxU=Z*{xq3lJ2|GGNAjM0_%fQ0Dn+Aekm{ zS3>v1eDBD`2_^3aU`EFJ!XB1+&i7D>&}rU?4(G1^5n72%pm)Qm#5Ijl}u7i^nVD&+WBSvY%q*n57 zr5OXuZx3g~A7}a3kB&1LA~>ncw(tHBeWMg4tL9MvIMkN8ZqyBfTtoirv-TL?pZN1p zWB}~+E^S5aZo%$06D9>mC=vF$0Xp3lsV_lXrOh5Zx!X^S`Jr5@u`8E&Ne3-^9#a)$4mjCa@2&!|oOC$m>28n_v{==de(1N$livo2$g7+O&^qYmi2m8p zDNBr$3Ap(i|G6Qk=WJG8di5dQ{%uBoH1a+HZ8-Jw#8;Ja(3R+0{WB(CCJFrY{}alX ze{~`5r^MJHqn`B(8p7HvrSbnmIga`NaKQepB=;)GPO_9o0c$>ELGdRf!C28 zm)QvEcOC+t;dd#B07Q+AD(?(+g@rIH%c2%-`RE~XO~&&f#eR9tBo}~shm)!zK!t~X zRAEMq>x?9KoPiFC?gaOv>&alVfsQ2r`3R9Vqs;*~9WeGD(Nk{R0ibQ|^KZ6(#sr?a z2Zl({v);J*$Le6+%4h&0FR{*bz8$4t)+<<(1>zvvM>!wM0`O5>0}lSH854CUXKw~t zY|s1tWXraVfN2A|Fvae@5Abw0LVN}kWay&4`3Yd#jSwsh!JFXg3$lXhf^Uk$_%J`V7d>)_?7u~*EU12Eus;m|kItd3&fdRF$s35^EEAlv$kdboEb;euc0=Hi1GlpjXAJ-~n!q|wlv%!_UJ6$&0id*tdI4}Sen5xjFF^TmHZ74H zHb6Ju1`4v@yMGu@uNt+XDvUeMi|w>rt3PQ87L={PqRRf4T@ad5{rlak$rvBbSpe#} zu}$553@I+cAmVN{Ad~V`kolZ!d(bA3`|*Y6+CUUN(ch5%Dwzy_Np_YJrRo8egcerv zsUNKE%MfJq`p%gM5|A!5Z$+9(UyZkhGDe1K`VrmL&6GNjdf@JVQugk<+8;ORnP-;jRHWIuG6K)_l>irA>hF|gDt&?J8vefgJw3(x*I3>C znpmq1GyI*^!4A^qdo2J-nJKn(f5fBYqRE7k{wJ3-@Eihj&8x5Q=VW8lb6&Q6**xDc zrckcCmFC*$dyiYRPsnrlIgLk{AYk=Ssdo=!e1+*2J z^R-SgZQ(J_;IxDj-y}Ls%cSxiT0&R9UhSXHq#uKXAhD5p`NiBFW{xlCByx`h|t@-Ls@E79O{~@F$wIKYlzC{Ig8z$0t%28vHqg~qwP!0D~lbICG0h7uQKw;OF=vt!p|Ii17Wm}my^~$ z=Y)vqY!65J1dc;stc$uc*F5sejlXV^$pU#W-1&1jnUYh5D@4t4!}y%Hr=)Bw*0RIz z%;9B@d$nU7xUB<4bOR|sf3Up8KkDQ^HE_K7NSq@p18cGrQy2C1cnv|Apve6|jzK|c zdta@lasg^CaK;aw&9LHd9T2t?esVnI>HMt9fiDUQNwq!F9RmdfK^Ef3*j+kIw=OPX69g7F*?YsP7n`rkql~aW=_1!V}&~V#bUT5s!9&8 zNDy@O2NyGtJ>uSeweW>%tq9q+j4{=AEb$!Q^)oT_pv5XYf7wo2idC?R5BvDq&3%!X-ZK(7;a1>-vv!-x*?W6%9nTjZ+* zEG%VN*Z9;0mMHtMN`oAO$KoqCf${P)q!+gwXHnX>q?^5Nh~q%W;e;7mRpVjJ+2ZQuVCY6e7CH@k)+s84Hw&yYVa_l>f8MuI5sxHkcRWT< zy1y>%>QN+XKc2@WT?EO2(K-a-A*Y$n2h9t2rK2|JX1(jkg^6Z_sv#myX&`ggPewi? zg%8iT6NO=_vezg1(L^7Qz5&8G?C~VlSTUAJGCzD}^y2U3zjm#AB10XH_lm68W*oTd zXv?m}sCs{ROC4t|$p_0im#v`i)F2;&xj)5+lfph3MrBY905H=!w%fA*tz;_=;lsVh zu`IOu@?$CI5=rVEoog|@XXmv$lU4YGOJ5fX7fSG5@bA_?bB45QIessobX z_y>q_48czVztT;`JwQCJ`z(r+!J1u+D(H*HaRv*s($|M@Mxk(4MZR|@+oYOjWZ~`@ z!}&p4fu?wQwny2%@4W*)S8Ewuk$Y=#4Q`a*Vg0dtA@pgDi2)kvk3JPY(6nKbHo5ys zU$3!zI@ROZ^|0~mdy?4>cO@HZodqu8OzE;qD@)DWGtK72vn!})EBh0?U{04G;>)Eg zIt;W&IGqbmCPfgh3Y>;iZUp&0{oc5$6A{*Rp&u?v|NMUKIOgoI!L4CaipN#lW?jca zf9O5$&7TX zUS)BfmUYvb(pM;y)xBN*Yx*k~-Ttk&jvosI>?s=*YEf!7N@s;4Ss6|U-;1|W)i8P0 z@!;j4)#9}(Q{`_qLloYyvkE(ch_vW)KCl<($2ClhHOz6Rg&zWXm2$PEm1NK_XGN7N zzjjZ7i7wA++i3Ac`*vA|y7uQavKBV!^oq>zj5RmfZh0e~Rj{6q!8ym5CI5qkr zQM7INXFz&su~b057q5{*f8Lkzkx50|6W#=YbPq~Ew^KToE5V@)I#WM>`#31ouNS#h z%C)d2=Jp3(A13u-et@Sv>_2p((vE-&iTctC9EyA-y_@IMTE$d`DTGsKyF(Sd_eCgh zw+X}oOkSyU>X!s22U#6S@(_^P5XqpXSyGmvp?Cxg9jnW?A))6WByEWi9;;}VXbXk9 zt;e(P-UaS$KZo2NG*pmXLPp9k{p=UHj4MeeM{dbWqLDN$(0bG69VTlFBB~@N?ozszm8%ht9aL z1&dNyg;ms%+cKtN_FxU%kk4h*HQJMb1(id-z{tO4caZCPSFxVlU6x?QNA+apOIh<{ zLxKuJmzxl)sQiUEpQ(~#bAwbL?oC(glCLlkD)A{?9rvjj52n8~W~vyY z{`qCwvq^a`J8Wquj85cMAOY6la-TPD-rT`%4xW|bnP1)) zP5&ZcV4z0|lGx@9x(uTDf3E|ufBjU{wsrhlh2gaVxy4ti-e&U%^_TObm#Rg-A27fE znf^r|$73EUV51%x}j15DgbJfHjfnSpzW|(+5dv z=w8VobB%ciY7Nig>~y)MF{NL94Cu&R?W@&S!~$yON9Dj)BIWNtd^C)KOF(1(y4K%D zYbo7-*t7n1F*Ja%|2O`!(LWeg2;tSw{av%f)nf3LNt%$FlQ zl6*6VG{EdaIV?m_KV4glb*3jp5CUS+pJI3#AkqXN-I-^H-SIq4r;W`|LWXS@bz7d4 zoY#__vi!<^!NulhszwEp%vrI2i@4H&j|tiz0oYBhl_Y(*Hm+#RRRjj;q1a#9b@MX3JHt0U#}5nOVSHVOv6L$*MFiQ!j?hlqNH+QZ{7 zXxkUE%}syu7lh+&6q6yY4N_1oT9DN*LY%jlO=Boz;d%~vHzN5|0*hG#hS|ub!2V0z zfh3fWc5GF;8%4v!%|50J#9C0={sfppV|tc>!EOQ!1WW+;_F|WDG2zaYjX|pWJpthH zq6L8x-1Pyt@BV3s!*>DeS$`=&hr@x~xE6i!bkYHE6VbMbRA?RUzW|aQ%}iUQv@xOt zd%$!j%$bDmR=qVH0xFs0I_DizO(4|yrCM-jk@878cLK-^|j#nxoflGR_O8YrIy zwli1+qjZuPh`VS(zWCv~j$+pM)-#(|6|+W!82u3dZXbHrtGw^J*HgUSc`7%I zv{zW72~Jk;Qi$RWwd*Zvs~%OF=qg?JkC-9R_J(TLmv|HXG?g_%SxSJh;ll2uqO0fb z*$?nl9}T>>fAmK)yg{aGZ@c*Gr09qEo734Gdn1(AIHR!2 z5Q=aa%O*P$r&-D~$cyT%I=0ZVQBr9&n33ksU@Kq6h}RpI@ueOSIYG9zjKl`_I&4_n zUEO`a8N8hJbF~2qY`Oik%cmA(j!(P^VM3AvqM|=KMCYm!@-k-n=Qt^Rn0hOh@uw#^I1t7zHPkx^o^-SDi*5SF8-nKq*ab~A26HF@83XP0E+iSGlrol$JJHeX#VLb*q zu?|&t)wJ0X79~Y;*p5%n8zRl=_XZg z=<3$n62U7fNA>*sQr&Jo#TU7m&H_@czB}@*W;^Qmr=X#`MX9o^W*UZkevC1iDk2U#<29qw;S+7dl z$n|$;V!swCL3he*D!5ai{I7e{2H#DJ=nV4ryHqxS=uYElEXHk$Z=^^#fp+7Lix38p z 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 Date: Fri, 3 Mar 2023 14:48:04 +0800 Subject: [PATCH 09/13] 1. add reset button 2. layout improvement 3. tips for re-open the refactoring 4. remain unchecked in the preview Signed-off-by: Shi Chen --- src/refactoring/changeSignaturePanel.ts | 42 ++++++++--------- src/webview/changeSignature/App.css | 63 ++++++++++++------------- src/webview/changeSignature/App.tsx | 59 ++++++++++++++++++----- 3 files changed, 96 insertions(+), 68 deletions(-) diff --git a/src/refactoring/changeSignaturePanel.ts b/src/refactoring/changeSignaturePanel.ts index 4222d73dbf..891b9265ff 100644 --- a/src/refactoring/changeSignaturePanel.ts +++ b/src/refactoring/changeSignaturePanel.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn, workspace, WorkspaceEdit, Position } from "vscode"; +import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn, workspace, WorkspaceEdit } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import { GetRefactorEditRequest, RefactorWorkspaceEdit } from "../protocol"; import { getNonce, getUri } from "../webview/utils"; @@ -46,7 +46,25 @@ export class ChangeSignaturePanel { public static render(extensionUri: Uri, languageClient: LanguageClient, command: any, params: any, formattingOptions: any, commandInfo: any) { if (ChangeSignaturePanel.currentPanel) { - ChangeSignaturePanel.currentPanel.panel.reveal(ViewColumn.Beside); + if (!commandInfo?.methodIdentifier) { + // the new method to do refactoring is not available, just reveal the current panel. + ChangeSignaturePanel.currentPanel.panel.reveal(ViewColumn.Beside); + return; + } + if (commandInfo.methodIdentifier === ChangeSignaturePanel.currentPanel.methodIdentifier) { + // the new method to do refactoring is the same as the old one, just reveal the current panel. + ChangeSignaturePanel.currentPanel.panel.reveal(ViewColumn.Beside); + return; + } else { + window.showInformationMessage(`Change Method Signature for '${ChangeSignaturePanel.currentPanel.methodName}' is in process. Would you like to reveal or abort it?`, "Reveal", "Abort").then(async (selection) => { + if (selection === "Reveal") { + ChangeSignaturePanel.currentPanel.panel.reveal(ViewColumn.Beside); + } else if (selection === "Abort") { + ChangeSignaturePanel.currentPanel.dispose(); + ChangeSignaturePanel.render(extensionUri, languageClient, command, params, formattingOptions, commandInfo); + } + }); + } } else { const panel = window.createWebviewPanel( ChangeSignaturePanel.type, @@ -161,28 +179,10 @@ export class ChangeSignaturePanel { command: this.command, context: this.params, options: this.formattingOptions, - commandArguments: [methodIdentifier, isDelegate, methodName, accessType, returnType, parameters, exceptions] + commandArguments: [methodIdentifier, isDelegate, methodName, accessType, returnType, parameters, exceptions, preview] }); if (clientWorkspaceEdit?.edit) { const codeEdit: WorkspaceEdit = await this.languageClient.protocol2CodeConverter.asWorkspaceEdit(clientWorkspaceEdit.edit); - /** - * See the issue https://github.com/microsoft/vscode/issues/94650. - * The current vscode doesn't provide a way for the extension to pre-select all changes. - * - * As a workaround, this extension would append a dummy text edit that needs a confirm, - * and then make all others text edits not need a confirm. This will ensure that - * the REFACTOR PREVIEW panel can be triggered and all valid changes pre-selected. - */ - if (preview) { - const textEditEntries = codeEdit.entries(); - if (textEditEntries && textEditEntries.length) { - const dummyNodeUri: Uri = textEditEntries[textEditEntries.length - 1][0]; - codeEdit.insert(dummyNodeUri, new Position(0, 0), "", { - needsConfirmation: true, - label: "Dummy node used to enable preview" - }); - } - } if (codeEdit) { return workspace.applyEdit(codeEdit); } diff --git a/src/webview/changeSignature/App.css b/src/webview/changeSignature/App.css index 8b0c0694f5..98fe56c7fb 100644 --- a/src/webview/changeSignature/App.css +++ b/src/webview/changeSignature/App.css @@ -1,28 +1,18 @@ @import "../../../node_modules/@vscode/codicons/dist/codicon.css"; main { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - height: 100%; -} - -main>* { - margin: 1rem 0; + max-width: 600px; + margin-left: auto; + margin-right: auto; } .section { - padding: 0; - margin: 0; - width: 100%; + margin-bottom: 0; } .section-columns { display: flex; width: 100%; - flex-flow: row wrap; - min-width: none; padding-left: calc(var(--design-unit) * 1px); } @@ -38,32 +28,26 @@ main>* { } .header-left { - flex-wrap: nowrap; flex-direction: column; display: flex; padding: 0.5rem 0.5rem 0.5rem 0; - flex: 0 0 30%; - max-width: 30%; + flex: 0 0 33%; box-sizing: border-box; } .header { - flex-wrap: nowrap; flex-direction: column; display: flex; padding: 0.5rem; - flex: 0 0 30%; - max-width: 30%; + flex: 0 0 33%; box-sizing: border-box; } .header-right { - flex-wrap: nowrap; flex-direction: column; display: flex; padding: 0.5rem 0 0.5rem 0.5rem; - flex: 0 0 30%; - max-width: 30%; + flex: 0 0 33%; box-sizing: border-box; } @@ -79,44 +63,43 @@ main>* { .add-button { margin: 0.5rem 0 0 0; - display: block; - width: 90%; } .bottom-buttons { padding: 0 0 0 calc(var(--design-unit) * 1px); margin: 0.5rem 0 0 0; - display: block; - width: 90%; } .preview { padding: 0 0 0 calc(var(--design-unit) * 1px); margin: 0 0 0.5rem 0; - width: 90%; + width: 99%; } .parameters-panel { margin: 0; - width: 90%; + width: calc(99% + 4px); } .parameters-view { padding: 0 0 0 calc(var(--design-unit) * 1px); flex-direction: column; - width: 100%; } -.parameters-grid { - width: 100%; +.parameter-cell { + padding-left: 0; } -.parameter-cell { +.parameter-cell-title { padding-left: 0; } +.parameter-cell-title:focus { + border: none; + background-color: inherit; +} + .parameter-cell-header { - padding-left: 0; background-color: var(--vscode-keybindingTable-headerBackground); } @@ -124,6 +107,14 @@ main>* { background-color: var(--vscode-input-background); } +.parameter-cell-edit-button { + padding: 0; + border: 0; + display: flex; + justify-content: right; + background-color: var(--vscode-input-background); +} + .parameter-cell-button { padding: 0; border: 0; @@ -131,6 +122,10 @@ main>* { justify-content: right; } +.parameter-cell-button:focus { + background-color: inherit; +} + .table-buttons { display: inline-flex; } diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 096071b76b..253336008f 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -4,10 +4,15 @@ import "./App.css"; import React from "react"; import { vscode } from "../vscodeApiWrapper"; -interface State { +type State = UIState & Metadata; + +interface UIState { focusRow: number; editParameterRow: number; editExceptionRow: number; +} + +interface Metadata { methodIdentifier: string | undefined; isDelegate: boolean; methodName: string | undefined; @@ -31,6 +36,8 @@ interface MethodException { export class App extends React.Component<{}, State> { + private initialMetadata: Metadata; + constructor(props: any) { super(props); this.state = { @@ -111,6 +118,14 @@ export class App extends React.Component<{}, State> { this.doRefactor(false); } else if (id === "preview") { this.doRefactor(true); + } else if (id === "reset") { + this.setState({ + ...this.initialMetadata, + focusRow: -1, + editParameterRow: -1, + editExceptionRow: -1, + }); + this.forceUpdate(); } else if (id === "addParameter") { const parameterNames = this.state.parameters.map(e => { return e.name; @@ -325,13 +340,17 @@ export class App extends React.Component<{}, State> { const { data } = event; const command = data.command as string; if (command === "setInitialState") { - this.setState({ + this.initialMetadata = { methodIdentifier: data.methodIdentifier, + isDelegate: false, accessType: data.accessType, methodName: data.methodName, returnType: data.returnType, parameters: data.parameters, exceptions: data.exceptions, + }; + this.setState({ + ...this.initialMetadata }); this.forceUpdate(); } @@ -367,6 +386,19 @@ export class App extends React.Component<{}, State> { }); } + isUnchanged = () => { + return this.state.isDelegate === this.initialMetadata?.isDelegate + && this.state.accessType === this.initialMetadata?.accessType + && this.state.methodName === this.initialMetadata?.methodName + && this.state.returnType === this.initialMetadata?.returnType + && this.isArrayEqual(this.state.parameters, this.initialMetadata?.parameters) + && this.isArrayEqual(this.state.exceptions, this.initialMetadata?.exceptions); + }; + + isArrayEqual = (a: any[], b: any[]) => { + return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((e, i) => e === b[i]); + }; + /** * Set the cursor style of the text area to text. Since the text area is * inside a shadow DOM, we need to add a style element to the shadow DOM. @@ -393,7 +425,7 @@ export class App extends React.Component<{}, State> { {this.state.parameters[row].type} {this.state.parameters[row].name} {this.getDefaultValue(row)} - + {row === this.state.editParameterRow ?
OK @@ -420,7 +452,7 @@ export class App extends React.Component<{}, State> { generateExceptionDataGridRow = (row: number) => { return {this.state.exceptions[row].type} - + {row === this.state.editExceptionRow ?
OK @@ -446,7 +478,7 @@ export class App extends React.Component<{}, State> {
Access modifier:
- + public protected package-private @@ -467,12 +499,12 @@ export class App extends React.Component<{}, State> { Parameters Exceptions - + - Type - Name - Default value - + Type + Name + Default value + { (() => { @@ -489,10 +521,10 @@ export class App extends React.Component<{}, State> {
- + - Type - + Type + { (() => { @@ -515,6 +547,7 @@ export class App extends React.Component<{}, State> {
Refactor Preview + Reset
); From 0722f743471981eea28cb733dbc45a557b2d0f81 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Mon, 13 Mar 2023 14:34:29 +0800 Subject: [PATCH 10/13] 1. address comments 2. fix package private modifier 3. move backend functions from /webview/utils.ts to /webviewUtils.ts Signed-off-by: Shi Chen --- src/markdownPreviewProvider.ts | 2 +- src/refactoring/changeSignaturePanel.ts | 8 +-- src/webview/changeSignature/App.css | 4 +- src/webview/changeSignature/App.tsx | 68 +++++++++++++++++++------ src/webview/utils.ts | 17 ++----- src/webviewUtils.ts | 14 +++++ 6 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 src/webviewUtils.ts diff --git a/src/markdownPreviewProvider.ts b/src/markdownPreviewProvider.ts index 2eeeb0818e..ad43e535cb 100644 --- a/src/markdownPreviewProvider.ts +++ b/src/markdownPreviewProvider.ts @@ -2,7 +2,7 @@ import { Disposable, WebviewPanel, window, ViewColumn, commands, Uri, Webview, E import * as fse from 'fs-extra'; import * as path from 'path'; import { Commands } from "./commands"; -import { getNonce } from "./webview/utils"; +import { getNonce } from "./webviewUtils"; class MarkdownPreviewProvider implements Disposable { private panel: WebviewPanel | undefined; diff --git a/src/refactoring/changeSignaturePanel.ts b/src/refactoring/changeSignaturePanel.ts index 891b9265ff..66c98f7d38 100644 --- a/src/refactoring/changeSignaturePanel.ts +++ b/src/refactoring/changeSignaturePanel.ts @@ -2,7 +2,7 @@ import * as path from "path"; import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn, workspace, WorkspaceEdit } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import { GetRefactorEditRequest, RefactorWorkspaceEdit } from "../protocol"; -import { getNonce, getUri } from "../webview/utils"; +import { getNonce, getUri } from "../webviewUtils"; interface MethodParameter { type: string; @@ -26,7 +26,7 @@ export class ChangeSignaturePanel { // method matadata private methodIdentifier: string | undefined; private methodName: string | undefined; - private accessType: string | undefined; + private modifier: string | undefined; private returnType: string | undefined; private parameters: MethodParameter[] | undefined; private exceptions: MethodException[] | undefined; @@ -90,7 +90,7 @@ export class ChangeSignaturePanel { this.formattingOptions = formattingOptions; this.methodIdentifier = commandInfo.methodIdentifier; this.methodName = commandInfo.methodName as string; - this.accessType = commandInfo.accessType as string; + this.modifier = commandInfo.modifier as string; this.returnType = commandInfo.returnType as string; this.parameters = commandInfo.parameters as MethodParameter[]; this.exceptions = commandInfo.exceptions as MethodException[]; @@ -143,7 +143,7 @@ export class ChangeSignaturePanel { command: "setInitialState", methodIdentifier: this.methodIdentifier, methodName: this.methodName, - accessType: this.accessType, + modifier: this.modifier, returnType: this.returnType, parameters: this.parameters, exceptions: this.exceptions diff --git a/src/webview/changeSignature/App.css b/src/webview/changeSignature/App.css index 98fe56c7fb..b903ead1fc 100644 --- a/src/webview/changeSignature/App.css +++ b/src/webview/changeSignature/App.css @@ -88,6 +88,7 @@ main { .parameter-cell { padding-left: 0; + pointer-events: none; } .parameter-cell-title { @@ -95,8 +96,9 @@ main { } .parameter-cell-title:focus { - border: none; + border-color: var(--vscode-keybindingTable-headerBackground); background-color: inherit; + color: inherit; } .parameter-cell-header { diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 253336008f..f57ccb95e5 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/prefer-for-of */ import { VSCodeButton, VSCodeTextField, VSCodeDropdown, VSCodeOption, VSCodeCheckbox, VSCodePanels, VSCodePanelTab, VSCodePanelView, VSCodeDataGrid, VSCodeDataGridCell, VSCodeDataGridRow, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; import "./App.css"; import React from "react"; import { vscode } from "../vscodeApiWrapper"; +import { cloneArray } from "../utils"; type State = UIState & Metadata; @@ -37,6 +39,10 @@ interface MethodException { export class App extends React.Component<{}, State> { private initialMetadata: Metadata; + private static TOOLTIP_RETURN_TYPE: string = "The method return type, can be either name or full qualified name of the type."; + private static TOOLTIP_PARAMETER_TYPE: string = "The parameter type, can be either name or full qualified name of the type."; + private static TOOLTIP_PARAMETER_DEFAULT: string = "The parameter default value, used when replacing the occurrences for an added parameter."; + private static TOOLTIP_EXCEPTION_TYPE: string = "The exception type, can be either name or full qualified name of the type."; constructor(props: any) { super(props); @@ -60,7 +66,7 @@ export class App extends React.Component<{}, State> { command: "doRefactor", methodIdentifier: this.state.methodIdentifier, isDelegate: this.state.isDelegate, - accessType: this.state.accessType, + accessType: this.getModifierString(this.state.accessType), methodName: this.state.methodName, returnType: this.state.returnType, parameters: this.state.parameters, @@ -106,7 +112,11 @@ export class App extends React.Component<{}, State> { } exceptions = exceptions.substring(0, exceptions.length - 2); } - return `${this.state.accessType} ${this.state.returnType} ${this.state.methodName}(${parameters})${exceptions}`; + let accessTypeString = this.getModifierString(this.state.accessType); + if (accessTypeString?.length) { + accessTypeString += " "; + } + return `${accessTypeString}${this.state.returnType} ${this.state.methodName}(${parameters})${exceptions}`; }; onClick = (event: any) => { @@ -343,14 +353,20 @@ export class App extends React.Component<{}, State> { this.initialMetadata = { methodIdentifier: data.methodIdentifier, isDelegate: false, - accessType: data.accessType, + accessType: this.getAccessTypeString(data.modifier), methodName: data.methodName, returnType: data.returnType, parameters: data.parameters, exceptions: data.exceptions, }; this.setState({ - ...this.initialMetadata + methodIdentifier: this.initialMetadata.methodIdentifier, + isDelegate: this.initialMetadata.isDelegate, + accessType: this.initialMetadata.accessType, + methodName: this.initialMetadata.methodName, + returnType: this.initialMetadata.returnType, + parameters: cloneArray(this.initialMetadata.parameters), + exceptions: cloneArray(this.initialMetadata.exceptions), }); this.forceUpdate(); } @@ -396,7 +412,14 @@ export class App extends React.Component<{}, State> { }; isArrayEqual = (a: any[], b: any[]) => { - return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((e, i) => e === b[i]); + return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((e, i) => this.objectsEqual(e, b[i])); + }; + + objectsEqual = (o1, o2) => { + return typeof o1 === 'object' && Object.keys(o1).length > 0 + ? Object.keys(o1).length === Object.keys(o2).length + && Object.keys(o1).every(p => this.objectsEqual(o1[p], o2[p])) + : o1 === o2; }; /** @@ -421,7 +444,7 @@ export class App extends React.Component<{}, State> { }; generateParameterDataGridRow = (row: number) => { - return + return {this.state.parameters[row].type} {this.state.parameters[row].name} {this.getDefaultValue(row)} @@ -450,7 +473,7 @@ export class App extends React.Component<{}, State> { }; generateExceptionDataGridRow = (row: number) => { - return + return {this.state.exceptions[row].type} {row === this.state.editExceptionRow ? @@ -479,15 +502,15 @@ export class App extends React.Component<{}, State> {
Access modifier:
- public - protected - package-private - private + public + protected + package-private + private
Return type:
- +
Method name:
@@ -501,9 +524,9 @@ export class App extends React.Component<{}, State> { - Type + Type Name - Default value + Default value { @@ -523,7 +546,7 @@ export class App extends React.Component<{}, State> { - Type + Type { @@ -565,4 +588,19 @@ export class App extends React.Component<{}, State> { } return Number(idSplit[1]); } + + private getModifierString(accessType: string): string { + if (accessType === "package-private") { + return ""; + } + return accessType; + } + + private getAccessTypeString(visibility: string): string { + if (visibility === "") { + return "package-private"; + } + return visibility; + } + } diff --git a/src/webview/utils.ts b/src/webview/utils.ts index 394fd9410e..335339f647 100644 --- a/src/webview/utils.ts +++ b/src/webview/utils.ts @@ -1,14 +1,5 @@ -import { Uri, Webview } from "vscode"; - -export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { - return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); -} - -export function getNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; +export function cloneArray(array: any[]): any[] { + const clonedArray = []; + array.forEach(val => clonedArray.push(Object.assign({}, val))); + return clonedArray; } diff --git a/src/webviewUtils.ts b/src/webviewUtils.ts new file mode 100644 index 0000000000..394fd9410e --- /dev/null +++ b/src/webviewUtils.ts @@ -0,0 +1,14 @@ +import { Uri, Webview } from "vscode"; + +export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { + return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); +} + +export function getNonce() { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} From 1cc199b255f61c085fab96832e2be98f9972d58f Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Tue, 14 Mar 2023 10:48:00 +0800 Subject: [PATCH 11/13] fix: parameters are not editable Signed-off-by: Shi Chen --- src/webview/changeSignature/App.css | 1 + src/webview/changeSignature/App.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/webview/changeSignature/App.css b/src/webview/changeSignature/App.css index b903ead1fc..a866110fca 100644 --- a/src/webview/changeSignature/App.css +++ b/src/webview/changeSignature/App.css @@ -106,6 +106,7 @@ main { } .parameter-cell-edit { + padding-left: 0; background-color: var(--vscode-input-background); } diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index f57ccb95e5..11e627cd50 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -445,9 +445,9 @@ export class App extends React.Component<{}, State> { generateParameterDataGridRow = (row: number) => { return - {this.state.parameters[row].type} - {this.state.parameters[row].name} - {this.getDefaultValue(row)} + {this.state.parameters[row].type} + {this.state.parameters[row].name} + {this.getDefaultValue(row)} {row === this.state.editParameterRow ?
@@ -474,7 +474,7 @@ export class App extends React.Component<{}, State> { generateExceptionDataGridRow = (row: number) => { return - {this.state.exceptions[row].type} + {this.state.exceptions[row].type} {row === this.state.editExceptionRow ?
From 2818564c147b30771d201e01966148a57f03157b Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Tue, 14 Mar 2023 18:04:34 +0800 Subject: [PATCH 12/13] 1. address comments 2. use loadsh.cloneDeep() to keep state clean Signed-off-by: Shi Chen --- src/webview/changeSignature/App.css | 4 ++++ src/webview/changeSignature/App.tsx | 16 +++++++++++----- src/webview/utils.ts | 5 ----- 3 files changed, 15 insertions(+), 10 deletions(-) delete mode 100644 src/webview/utils.ts diff --git a/src/webview/changeSignature/App.css b/src/webview/changeSignature/App.css index a866110fca..2feef25eaa 100644 --- a/src/webview/changeSignature/App.css +++ b/src/webview/changeSignature/App.css @@ -129,6 +129,10 @@ main { background-color: inherit; } +.parameter-cell-button:active { + background-color: inherit; +} + .table-buttons { display: inline-flex; } diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 11e627cd50..2ef92f456b 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -4,7 +4,7 @@ import { VSCodeButton, VSCodeTextField, VSCodeDropdown, VSCodeOption, VSCodeChec import "./App.css"; import React from "react"; import { vscode } from "../vscodeApiWrapper"; -import { cloneArray } from "../utils"; +import _ from "lodash"; type State = UIState & Metadata; @@ -130,7 +130,13 @@ export class App extends React.Component<{}, State> { this.doRefactor(true); } else if (id === "reset") { this.setState({ - ...this.initialMetadata, + methodIdentifier: this.initialMetadata.methodIdentifier, + isDelegate: this.initialMetadata.isDelegate, + accessType: this.initialMetadata.accessType, + methodName: this.initialMetadata.methodName, + returnType: this.initialMetadata.returnType, + parameters: _.cloneDeep(this.initialMetadata.parameters), + exceptions: _.cloneDeep(this.initialMetadata.exceptions), focusRow: -1, editParameterRow: -1, editExceptionRow: -1, @@ -365,8 +371,8 @@ export class App extends React.Component<{}, State> { accessType: this.initialMetadata.accessType, methodName: this.initialMetadata.methodName, returnType: this.initialMetadata.returnType, - parameters: cloneArray(this.initialMetadata.parameters), - exceptions: cloneArray(this.initialMetadata.exceptions), + parameters: _.cloneDeep(this.initialMetadata.parameters), + exceptions: _.cloneDeep(this.initialMetadata.exceptions), }); this.forceUpdate(); } @@ -566,7 +572,7 @@ export class App extends React.Component<{}, State> {
Method signature:
- Keep original method as delegate to changed method + Keep original method as delegate to changed method
Refactor Preview diff --git a/src/webview/utils.ts b/src/webview/utils.ts deleted file mode 100644 index 335339f647..0000000000 --- a/src/webview/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function cloneArray(array: any[]): any[] { - const clonedArray = []; - array.forEach(val => clonedArray.push(Object.assign({}, val))); - return clonedArray; -} From 35acd8e8e440f593c2c7502fa11afad4eb2e9002 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Wed, 15 Mar 2023 13:30:53 +0800 Subject: [PATCH 13/13] address comments Signed-off-by: Shi Chen --- src/webview/changeSignature/App.tsx | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/webview/changeSignature/App.tsx b/src/webview/changeSignature/App.tsx index 2ef92f456b..8535ab2de6 100644 --- a/src/webview/changeSignature/App.tsx +++ b/src/webview/changeSignature/App.tsx @@ -39,10 +39,10 @@ interface MethodException { export class App extends React.Component<{}, State> { private initialMetadata: Metadata; - private static TOOLTIP_RETURN_TYPE: string = "The method return type, can be either name or full qualified name of the type."; - private static TOOLTIP_PARAMETER_TYPE: string = "The parameter type, can be either name or full qualified name of the type."; + private static TOOLTIP_RETURN_TYPE: string = "The method return type, can be either name or fully qualified name of the type."; + private static TOOLTIP_PARAMETER_TYPE: string = "The parameter type, can be either name or fully qualified name of the type."; private static TOOLTIP_PARAMETER_DEFAULT: string = "The parameter default value, used when replacing the occurrences for an added parameter."; - private static TOOLTIP_EXCEPTION_TYPE: string = "The exception type, can be either name or full qualified name of the type."; + private static TOOLTIP_EXCEPTION_TYPE: string = "The exception type, can be either name or fully qualified name of the type."; constructor(props: any) { super(props); @@ -83,17 +83,14 @@ export class App extends React.Component<{}, State> { this.setState({ accessType: event.target.value }); - this.forceUpdate(); } else if (id === "returnType") { this.setState({ returnType: event.target.value }); - this.forceUpdate(); } else if (id === "methodName") { this.setState({ methodName: event.target.value }); - this.forceUpdate(); } return; }; @@ -141,7 +138,6 @@ export class App extends React.Component<{}, State> { editParameterRow: -1, editExceptionRow: -1, }); - this.forceUpdate(); } else if (id === "addParameter") { const parameterNames = this.state.parameters.map(e => { return e.name; @@ -160,7 +156,6 @@ export class App extends React.Component<{}, State> { originalIndex: -1 }] }); - this.forceUpdate(); } else if (id.startsWith("removeParameter")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -171,7 +166,6 @@ export class App extends React.Component<{}, State> { return i !== selectedRowNumber; }) }); - this.forceUpdate(); } else if (id.startsWith("editParameter")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -182,7 +176,6 @@ export class App extends React.Component<{}, State> { editExceptionRow: -1, focusRow: -1, }); - this.forceUpdate(); const elementToSelect = document.getElementById(`parameterType-${selectedRowNumber}`); if (elementToSelect) { elementToSelect.focus(); @@ -197,7 +190,6 @@ export class App extends React.Component<{}, State> { editExceptionRow: selectedRowNumber, focusRow: -1, }); - this.forceUpdate(); const elementToSelect = document.getElementById(`exceptionType-${selectedRowNumber}`); if (elementToSelect) { elementToSelect.focus(); @@ -214,7 +206,6 @@ export class App extends React.Component<{}, State> { this.setState({ parameters: currentParameters }); - this.forceUpdate(); } else if (id.startsWith("downParameter")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -227,7 +218,6 @@ export class App extends React.Component<{}, State> { this.setState({ parameters: currentParameters }); - this.forceUpdate(); } else if (id === "addException") { const exceptionNames = this.state.exceptions.map(e => { return e.type; @@ -244,7 +234,6 @@ export class App extends React.Component<{}, State> { typeHandleIdentifier: undefined, }] }); - this.forceUpdate(); } else if (id.startsWith("removeException")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -255,12 +244,10 @@ export class App extends React.Component<{}, State> { return i !== selectedRowNumber; }) }); - this.forceUpdate(); } else if (id === "delegate") { this.setState({ isDelegate: event.target.checked }); - this.forceUpdate(); } else if (id.startsWith("confirmParameter")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -288,7 +275,6 @@ export class App extends React.Component<{}, State> { editExceptionRow: -1, focusRow: -1 }); - this.forceUpdate(); } else if (id.startsWith("cancelParameter")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -313,7 +299,6 @@ export class App extends React.Component<{}, State> { editExceptionRow: -1, focusRow: -1 }); - this.forceUpdate(); } else if (id.startsWith("confirmException")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -333,7 +318,6 @@ export class App extends React.Component<{}, State> { editExceptionRow: -1, focusRow: -1 }); - this.forceUpdate(); } else if (id.startsWith("cancelException")) { const selectedRowNumber: number | undefined = this.getSelectedRowNumber(id); if (selectedRowNumber === undefined) { @@ -348,7 +332,6 @@ export class App extends React.Component<{}, State> { editExceptionRow: -1, focusRow: -1 }); - this.forceUpdate(); } }; @@ -374,7 +357,6 @@ export class App extends React.Component<{}, State> { parameters: _.cloneDeep(this.initialMetadata.parameters), exceptions: _.cloneDeep(this.initialMetadata.exceptions), }); - this.forceUpdate(); } };