Skip to content

Commit 9637799

Browse files
authored
Support "extract interface" refactoring (#2836)
* Support extract interface refactoring Signed-off-by: Shi Chen <[email protected]>
1 parent 6765a6e commit 9637799

File tree

6 files changed

+211
-1
lines changed

6 files changed

+211
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ The following settings are supported:
221221
- Windows: First use `"$APPDATA\\.jdt\\index"`, or `"~\\.jdt\\index"` if it does not exist
222222
- macOS: `"~/Library/Caches/.jdt/index"`
223223
- Linux: First use `"$XDG_CACHE_HOME/.jdt/index"`, or `"~/.cache/.jdt/index"` if it does not exist
224+
* `java.refactoring.extract.interface.replace`: Specify whether to replace all the occurrences of the subtype with the new extracted interface. Defaults to `true`.
224225

225226
New in 1.15.0
226227
* `java.import.maven.disableTestClasspathFlag` : Enable/disable test classpath segregation. When enabled, this permits the usage of test resources within a Maven project as dependencies within the compile scope of other projects. Defaults to `false`.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,11 @@
10631063
"markdownDescription": "Specifies a common index location for all workspaces. See default values as follows:\n \nWindows: First use `\"$APPDATA\\\\.jdt\\\\index\"`, or `\"~\\\\.jdt\\\\index\"` if it does not exist\n \nmacOS: `\"~/Library/Caches/.jdt/index\"`\n \nLinux: First use `\"$XDG_CACHE_HOME/.jdt/index\"`, or `\"~/.cache/.jdt/index\"` if it does not exist",
10641064
"default": "",
10651065
"scope": "window"
1066+
},
1067+
"java.refactoring.extract.interface.replace": {
1068+
"type": "boolean",
1069+
"markdownDescription": "Specify whether to replace all the occurrences of the subtype with the new extracted interface.",
1070+
"default": true
10661071
}
10671072
}
10681073
},

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
169169
actionableRuntimeNotificationSupport: true,
170170
shouldLanguageServerExitOnShutdown: true,
171171
onCompletionItemSelectedCommand: "editor.action.triggerParameterHints",
172+
extractInterfaceSupport: true,
172173
},
173174
triggerFiles,
174175
},

src/protocol.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,20 @@ export interface UpgradeGradleWrapperInfo {
447447
message: string;
448448
recommendedGradleVersion: string;
449449
}
450+
451+
export interface Member {
452+
name: string;
453+
typeName: string;
454+
parameters: string[];
455+
handleIdentifier: string;
456+
}
457+
458+
export interface CheckExtractInterfaceStatusResponse {
459+
members: Member[];
460+
subTypeName: string;
461+
destinationResponse: MoveDestinationsResponse;
462+
}
463+
464+
export namespace CheckExtractInterfaceStatusRequest {
465+
export const type = new RequestType<CodeActionParams, CheckExtractInterfaceStatusResponse, void>('java/checkExtractInterfaceStatus');
466+
}

src/refactorAction.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import { existsSync } from 'fs';
44
import * as path from 'path';
55
import { commands, ExtensionContext, Position, QuickPickItem, TextDocument, Uri, window, workspace } from 'vscode';
6-
import { FormattingOptions, WorkspaceEdit, CreateFile, RenameFile, DeleteFile, TextDocumentEdit, CodeActionParams, SymbolInformation } from 'vscode-languageclient';
6+
import { FormattingOptions, WorkspaceEdit, RenameFile, DeleteFile, TextDocumentEdit, CodeActionParams, SymbolInformation } from 'vscode-languageclient';
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 { getExtractInterfaceArguments, revealExtractedInterface } from './refactoring/extractInterface';
1011

1112
export function registerCommands(languageClient: LanguageClient, context: ExtensionContext) {
1213
registerApplyRefactorCommand(languageClient, context);
@@ -38,6 +39,7 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E
3839
|| command === 'extractConstant'
3940
|| command === 'extractMethod'
4041
|| command === 'extractField'
42+
|| command === 'extractInterface'
4143
|| command === 'assignField'
4244
|| command === 'convertVariableToField'
4345
|| command === 'invertVariable'
@@ -101,6 +103,12 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E
101103
}
102104
commandArguments.push(expression);
103105
}
106+
} else if (command === 'extractInterface') {
107+
const args = await getExtractInterfaceArguments(languageClient, params);
108+
if (args.length === 0) {
109+
return;
110+
}
111+
commandArguments.push(...args);
104112
}
105113

106114
const result: RefactorWorkspaceEdit = await languageClient.sendRequest(GetRefactorEditRequest.type, {
@@ -111,6 +119,10 @@ function registerApplyRefactorCommand(languageClient: LanguageClient, context: E
111119
});
112120

113121
await applyRefactorEdit(languageClient, result);
122+
123+
if (command === 'extractInterface') {
124+
await revealExtractedInterface(result);
125+
}
114126
} else if (command === 'moveFile') {
115127
if (!commandInfo || !commandInfo.uri) {
116128
return;

src/refactoring/extractInterface.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
'use strict';
2+
3+
import * as vscode from "vscode";
4+
import { LanguageClient } from "vscode-languageclient/node";
5+
import { CheckExtractInterfaceStatusRequest, CheckExtractInterfaceStatusResponse, RefactorWorkspaceEdit } from "../protocol";
6+
7+
enum Step {
8+
selectMember,
9+
specifyInterfaceName,
10+
selectPackage,
11+
}
12+
13+
export async function getExtractInterfaceArguments(languageClient: LanguageClient, params: any): Promise<any[]> {
14+
if (!params || !params.range) {
15+
return [];
16+
}
17+
const extractInterfaceResponse: CheckExtractInterfaceStatusResponse = await languageClient.sendRequest(CheckExtractInterfaceStatusRequest.type, params);
18+
if (!extractInterfaceResponse) {
19+
return [];
20+
}
21+
let step: Step = Step.selectMember;
22+
// step results, initialized as undefined
23+
let resultHandleIdentifiers: any[] | undefined;
24+
let interfaceName: string | undefined;
25+
let selectPackageNodeItem: SelectPackageQuickPickItem | undefined;
26+
while (step !== undefined) {
27+
switch (step) {
28+
case Step.selectMember:
29+
const items = extractInterfaceResponse.members.map((item) => {
30+
return {
31+
label: item.parameters ? `${item.name}(${item.parameters.join(", ")})` : item.name,
32+
description: item.typeName,
33+
handleIdentifier: item.handleIdentifier,
34+
picked: resultHandleIdentifiers === undefined ? false : resultHandleIdentifiers.includes(item.handleIdentifier),
35+
};
36+
});
37+
const members = await vscode.window.showQuickPick(items, {
38+
title: "Extract Interface (1/3): Select members",
39+
placeHolder: "Please select members to declare in the interface: ",
40+
matchOnDescription: true,
41+
ignoreFocusOut: true,
42+
canPickMany: true,
43+
});
44+
if (!members) {
45+
return [];
46+
}
47+
resultHandleIdentifiers = members.map((item) => item.handleIdentifier);
48+
if (!resultHandleIdentifiers) {
49+
return [];
50+
}
51+
step = Step.specifyInterfaceName;
52+
break;
53+
case Step.specifyInterfaceName:
54+
const specifyInterfaceNameDisposables = [];
55+
const specifyInterfaceNamePromise = new Promise<string | boolean | undefined>((resolve, _reject) => {
56+
const inputBox = vscode.window.createInputBox();
57+
inputBox.title = "Extract Interface (2/3): Specify interface name";
58+
inputBox.placeholder = "Please specify the new interface name: ";
59+
inputBox.ignoreFocusOut = true;
60+
inputBox.value = interfaceName === undefined ? extractInterfaceResponse.subTypeName : interfaceName;
61+
inputBox.buttons = [(vscode.QuickInputButtons.Back)];
62+
specifyInterfaceNameDisposables.push(
63+
inputBox,
64+
inputBox.onDidTriggerButton((button) => {
65+
if (button === vscode.QuickInputButtons.Back) {
66+
step = Step.selectMember;
67+
resolve(false);
68+
}
69+
}),
70+
inputBox.onDidAccept(() => {
71+
resolve(inputBox.value);
72+
}),
73+
inputBox.onDidHide(() => {
74+
resolve(undefined);
75+
})
76+
);
77+
inputBox.show();
78+
});
79+
try {
80+
const result = await specifyInterfaceNamePromise;
81+
if (result === false) {
82+
// go back
83+
step = Step.selectMember;
84+
} else if (result === undefined) {
85+
// cancelled
86+
return [];
87+
} else {
88+
interfaceName = result as string;
89+
step = Step.selectPackage;
90+
}
91+
} finally {
92+
specifyInterfaceNameDisposables.forEach(d => d.dispose());
93+
}
94+
break;
95+
case Step.selectPackage:
96+
const selectPackageDisposables = [];
97+
const packageNodeItems = extractInterfaceResponse.destinationResponse.destinations.sort((node1, node2) => {
98+
return node1.isParentOfSelectedFile ? -1 : 0;
99+
}).map((packageNode) => {
100+
const packageUri: vscode.Uri = packageNode.uri ? vscode.Uri.parse(packageNode.uri) : null;
101+
const displayPath: string = packageUri ? vscode.workspace.asRelativePath(packageUri, true) : packageNode.path;
102+
return {
103+
label: (packageNode.isParentOfSelectedFile ? '* ' : '') + packageNode.displayName,
104+
description: displayPath,
105+
packageNode,
106+
};
107+
});
108+
const selectPackagePromise = new Promise<SelectPackageQuickPickItem | boolean | undefined>((resolve, _reject) => {
109+
const quickPick = vscode.window.createQuickPick<SelectPackageQuickPickItem>();
110+
quickPick.items = packageNodeItems;
111+
quickPick.title = "Extract Interface (3/3): Specify package";
112+
quickPick.placeholder = "Please select the target package for extracted interface.";
113+
quickPick.ignoreFocusOut = true;
114+
quickPick.buttons = [(vscode.QuickInputButtons.Back)];
115+
selectPackageDisposables.push(
116+
quickPick,
117+
quickPick.onDidTriggerButton((button) => {
118+
if (button === vscode.QuickInputButtons.Back) {
119+
resolve(false);
120+
step = Step.specifyInterfaceName;
121+
}
122+
}),
123+
quickPick.onDidAccept(() => {
124+
if (quickPick.selectedItems.length > 0) {
125+
resolve(quickPick.selectedItems[0] as SelectPackageQuickPickItem);
126+
}
127+
}),
128+
quickPick.onDidHide(() => {
129+
resolve(undefined);
130+
}),
131+
);
132+
quickPick.show();
133+
});
134+
try {
135+
const result = await selectPackagePromise;
136+
if (result === false) {
137+
// go back
138+
step = Step.specifyInterfaceName;
139+
} else if (result === undefined) {
140+
// cancelled
141+
return [];
142+
} else {
143+
selectPackageNodeItem = result as SelectPackageQuickPickItem;
144+
step = undefined;
145+
}
146+
} finally {
147+
selectPackageDisposables.forEach(d => d.dispose());
148+
}
149+
break;
150+
default:
151+
return [];
152+
}
153+
}
154+
return [resultHandleIdentifiers, interfaceName, selectPackageNodeItem.packageNode];
155+
}
156+
157+
export async function revealExtractedInterface(refactorEdit: RefactorWorkspaceEdit) {
158+
if (refactorEdit?.edit?.documentChanges) {
159+
for (const change of refactorEdit.edit.documentChanges) {
160+
if ("kind" in change && change.kind === "create") {
161+
for (const document of vscode.workspace.textDocuments) {
162+
if (document.uri.toString() === vscode.Uri.parse(change.uri).toString()) {
163+
await vscode.window.showTextDocument(document);
164+
return;
165+
}
166+
}
167+
}
168+
}
169+
}
170+
}
171+
172+
interface SelectPackageQuickPickItem extends vscode.QuickPickItem {
173+
packageNode: any;
174+
}

0 commit comments

Comments
 (0)