Skip to content

Commit a340e9a

Browse files
committed
Support change signature refactoring
Signed-off-by: Shi Chen <chenshi@microsoft.com>
1 parent 6572351 commit a340e9a

27 files changed

Lines changed: 14432 additions & 9 deletions

.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ test/resources/projects/maven/salut/testGradle
1919
test-temp
2020

2121
# specific to eslint
22-
vscode*.d.ts
22+
vscode*.d.ts
23+
webview-ui/build/static/js

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,6 @@
106106
"prefer-arrow-callback": "off"
107107
}
108108
}
109-
]
109+
],
110+
"ignorePatterns": ["webview-ui/**"]
110111
}

.vscode/launch.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@
77
"type": "extensionHost",
88
"request": "launch",
99
"runtimeExecutable": "${execPath}",
10+
"debugWebviews": true,
1011
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
1112
"env": {
1213
"DEBUG_VSCODE_JAVA":"true"
1314
},
1415
"stopOnEntry": false,
1516
"sourceMaps": true,
1617
"outFiles": [ "${workspaceRoot}/dist/**/*.js" ],
17-
"preLaunchTask": "npm: watch"
18+
"preLaunchTask": "npm: watch",
19+
"rendererDebugOptions": {
20+
"webRoot": "${workspaceFolder}/webview-ui/build/static/js",
21+
"urlFilter": "*redhat.java*",
22+
"sourceMaps": true,
23+
"pauseForSourceMap": true,
24+
}
1825
},
1926
{
2027
"name": "Launch Extension - Remote Server",
@@ -36,6 +43,7 @@
3643
"type": "extensionHost",
3744
"request": "launch",
3845
"runtimeExecutable": "${execPath}",
46+
"debugWebviews": true,
3947
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
4048
"stopOnEntry": false,
4149
"sourceMaps": true,
@@ -44,7 +52,13 @@
4452
"JDTLS_CLIENT_PORT": "5036",
4553
"DEBUG_VSCODE_JAVA":"true"
4654
},
47-
"preLaunchTask": "npm: watch"
55+
"preLaunchTask": "npm: watch",
56+
"rendererDebugOptions": {
57+
"webRoot": "${workspaceFolder}/webview-ui/build/static/js",
58+
"urlFilter": "*redhat.java*",
59+
"sourceMaps": true,
60+
"pauseForSourceMap": true,
61+
}
4862
},
4963
{
5064
"name": "Launch Extension - SyntaxLS Client",

.vscodeignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ Jenkinsfile
2323
.eslintrc.json
2424
.eslintignore
2525
webpack.config.js
26-
.DS_Store
26+
.DS_Store
27+
webview-ui/**
28+
!webview-ui/build/static/**

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,8 +1382,9 @@
13821382
}
13831383
},
13841384
"scripts": {
1385-
"vscode:prepublish": "webpack --mode production",
1386-
"compile": "tsc -p ./&webpack --mode development",
1385+
"postinstall": "cd webview-ui && npm install",
1386+
"vscode:prepublish": "webpack --mode production && cd webview-ui && npm run build",
1387+
"compile": "tsc -p ./&webpack --mode development && cd webview-ui && npm run build",
13871388
"watch": "webpack --mode development --watch",
13881389
"pretest": "npm run compile",
13891390
"test": "node ./out/test/runtest.js",

src/refactorAction.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FormattingOptions, WorkspaceEdit, RenameFile, DeleteFile, TextDocumentE
77
import { LanguageClient } from 'vscode-languageclient/node';
88
import { Commands as javaCommands } from './commands';
99
import { GetRefactorEditRequest, MoveRequest, RefactorWorkspaceEdit, RenamePosition, GetMoveDestinationsRequest, SearchSymbols, SelectionInfo, InferSelectionRequest } from './protocol';
10+
import { ChangeSignaturePanel } from './refactoring/changeSignaturePanel';
1011
import { getExtractInterfaceArguments, revealExtractedInterface } from './refactoring/extractInterface';
1112

1213
export function registerCommands(languageClient: LanguageClient, context: ExtensionContext) {
@@ -40,6 +41,7 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E
4041
|| command === 'extractMethod'
4142
|| command === 'extractField'
4243
|| command === 'extractInterface'
44+
|| command === 'changeSignature'
4345
|| command === 'assignField'
4446
|| command === 'convertVariableToField'
4547
|| command === 'invertVariable'
@@ -109,6 +111,9 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E
109111
return;
110112
}
111113
commandArguments.push(...args);
114+
} else if (command === 'changeSignature') {
115+
ChangeSignaturePanel.render(context.extensionUri, languageClient, command, params, formattingOptions, commandInfo);
116+
return;
112117
}
113118

114119
const result: RefactorWorkspaceEdit = await languageClient.sendRequest(GetRefactorEditRequest.type, {
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn, workspace, WorkspaceEdit, Position } from "vscode";
2+
import { LanguageClient } from "vscode-languageclient/node";
3+
import { GetRefactorEditRequest, RefactorWorkspaceEdit } from "../protocol";
4+
import { getNonce, getUri } from "../webview/utils";
5+
6+
interface MethodParameter {
7+
type: string;
8+
name: string;
9+
defaultValue: string;
10+
originalIndex: number;
11+
}
12+
13+
interface MethodException {
14+
type: string;
15+
typeHandleIdentifier: string;
16+
}
17+
18+
export class ChangeSignaturePanel {
19+
public static type = "java.refactor.changeSignature";
20+
public static title = "Refactor: Change Method Signature";
21+
public static currentPanel: ChangeSignaturePanel | undefined;
22+
private readonly panel: WebviewPanel;
23+
private disposables: Disposable[] = [];
24+
25+
// method matadata
26+
private methodIdentifier: string | undefined;
27+
private methodName: string | undefined;
28+
private accessType: string | undefined;
29+
private returnType: string | undefined;
30+
private parameters: MethodParameter[] | undefined;
31+
private exceptions: MethodException[] | undefined;
32+
33+
// refactor metadata
34+
private languageClient: LanguageClient;
35+
private params: any;
36+
private formattingOptions: any;
37+
private command: any;
38+
39+
private constructor(panel: WebviewPanel, extensionUri: Uri) {
40+
this.panel = panel;
41+
this.panel.onDidDispose(() => this.dispose(), null, this.disposables);
42+
this.panel.webview.html = this.getWebviewContent(this.panel.webview, extensionUri);
43+
this.setWebviewMessageListener(this.panel.webview);
44+
}
45+
46+
public static render(extensionUri: Uri, languageClient: LanguageClient, command: any, params: any, formattingOptions: any, commandInfo: any) {
47+
if (ChangeSignaturePanel.currentPanel) {
48+
ChangeSignaturePanel.currentPanel.panel.reveal(ViewColumn.Beside);
49+
} else {
50+
const panel = window.createWebviewPanel(
51+
ChangeSignaturePanel.type,
52+
ChangeSignaturePanel.title,
53+
ViewColumn.Beside,
54+
{
55+
enableCommandUris: true,
56+
enableScripts: true,
57+
localResourceRoots: [Uri.joinPath(extensionUri, "dist"), Uri.joinPath(extensionUri, "webview-ui/build")],
58+
}
59+
);
60+
ChangeSignaturePanel.currentPanel = new ChangeSignaturePanel(panel, extensionUri);
61+
ChangeSignaturePanel.currentPanel.setMetadata(languageClient, command, params, formattingOptions, commandInfo);
62+
}
63+
}
64+
65+
public setMetadata(languageClient: LanguageClient, command: any, params: any, formattingOptions: any, commandInfo: any) {
66+
this.languageClient = languageClient;
67+
this.command = command;
68+
this.params = params;
69+
this.formattingOptions = formattingOptions;
70+
this.methodIdentifier = commandInfo.methodIdentifier;
71+
this.methodName = commandInfo.methodName as string;
72+
this.accessType = commandInfo.accessType as string;
73+
this.returnType = commandInfo.returnType as string;
74+
this.parameters = commandInfo.parameters as MethodParameter[];
75+
this.exceptions = commandInfo.exceptions as MethodException[];
76+
}
77+
78+
public dispose() {
79+
ChangeSignaturePanel.currentPanel = undefined;
80+
this.panel.dispose();
81+
while (this.disposables.length) {
82+
const disposable = this.disposables.pop();
83+
if (disposable) {
84+
disposable.dispose();
85+
}
86+
}
87+
}
88+
89+
private getWebviewContent(webview: Webview, extensionUri: Uri) {
90+
const stylesUri = getUri(webview, extensionUri, [
91+
"webview-ui",
92+
"build",
93+
"static",
94+
"css",
95+
"main.css",
96+
]);
97+
const scriptUri = getUri(webview, extensionUri, [
98+
"webview-ui",
99+
"build",
100+
"static",
101+
"js",
102+
"main.js",
103+
]);
104+
105+
const nonce = getNonce();
106+
107+
return /* html*/ `
108+
<!DOCTYPE html>
109+
<html lang="en">
110+
<head>
111+
<meta charset="utf-8">
112+
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
113+
<meta name="theme-color" content="#000000">
114+
<link rel="stylesheet" type="text/css" href="${stylesUri}">
115+
<title>Change Signature</title>
116+
</head>
117+
<body>
118+
<div id="root"></div>
119+
<script nonce="${nonce}" src="${scriptUri}"></script>
120+
</body>
121+
</html>
122+
`;
123+
}
124+
125+
private setWebviewMessageListener(webview: Webview) {
126+
webview.onDidReceiveMessage(
127+
async (message: any) => {
128+
const command = message.command;
129+
switch (command) {
130+
case "webviewReady":
131+
await this.panel.webview.postMessage({
132+
command: "setInitialState",
133+
methodIdentifier: this.methodIdentifier,
134+
methodName: this.methodName,
135+
accessType: this.accessType,
136+
returnType: this.returnType,
137+
parameters: this.parameters,
138+
exceptions: this.exceptions
139+
});
140+
break;
141+
case "doRefactor":
142+
await this.doRefactor(message.methodIdentifier, message.isDelegate, message.methodName, message.accessType, message.returnType, message.parameters, message.exceptions);
143+
this.dispose();
144+
break;
145+
}
146+
},
147+
undefined,
148+
this.disposables
149+
);
150+
}
151+
152+
private async doRefactor(methodIdentifier: string, isDelegate: boolean, methodName: string, accessType: string, returnType: string, parameters: MethodParameter[], exceptions: MethodException[]) {
153+
const clientWorkspaceEdit: RefactorWorkspaceEdit = await this.languageClient.sendRequest(GetRefactorEditRequest.type, {
154+
command: this.command,
155+
context: this.params,
156+
options: this.formattingOptions,
157+
commandArguments: [methodIdentifier, isDelegate, methodName, accessType, returnType, parameters, exceptions]
158+
});
159+
if (!clientWorkspaceEdit) {
160+
return;
161+
}
162+
if (clientWorkspaceEdit.edit) {
163+
const codeEdit: WorkspaceEdit = await this.languageClient.protocol2CodeConverter.asWorkspaceEdit(clientWorkspaceEdit.edit);
164+
165+
/**
166+
* See the issue https://github.com/microsoft/vscode/issues/94650.
167+
* The current vscode doesn't provide a way for the extension to pre-select all changes.
168+
*
169+
* As a workaround, this extension would append a dummy text edit that needs a confirm,
170+
* and then make all others text edits not need a confirm. This will ensure that
171+
* the REFACTOR PREVIEW panel can be triggered and all valid changes pre-selected.
172+
*/
173+
const textEditEntries = codeEdit.entries();
174+
if (textEditEntries && textEditEntries.length) {
175+
const dummyNodeUri: Uri = textEditEntries[textEditEntries.length - 1][0];
176+
codeEdit.insert(dummyNodeUri, new Position(0, 0), "", {
177+
needsConfirmation: true,
178+
label: "Dummy node used to enable preview"
179+
});
180+
}
181+
182+
if (codeEdit) {
183+
await workspace.applyEdit(codeEdit);
184+
}
185+
}
186+
}
187+
}

src/webview/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Uri, Webview } from "vscode";
2+
3+
export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) {
4+
return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList));
5+
}
6+
7+
export function getNonce() {
8+
let text = "";
9+
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
10+
for (let i = 0; i < 32; i++) {
11+
text += possible.charAt(Math.floor(Math.random() * possible.length));
12+
}
13+
return text;
14+
}

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"exclude": [
1414
"node_modules",
1515
"server",
16-
".vscode-test"
16+
".vscode-test",
17+
"webview-ui"
1718
]
1819
}

webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const path = require('path');
88
/**@type {import('webpack').Configuration}*/
99
const config = {
1010
watchOptions: {
11-
ignored: /node_modules/
11+
ignored: ["/node_modules/", "/webview-ui/"]
1212
},
1313
target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
1414
node: {

0 commit comments

Comments
 (0)