Skip to content

Commit 86bf3ae

Browse files
authored
Add capability to select projects to be imported (#3356)
* Support to manually select Maven pom files to import - A new setting `java.import.configurationFileCollectionMode` is added to configure whether manually build file selection is required before import. - A new contribution point `javaBuildTypes` is introduced, and will be used when the build files need to be manually selected. Signed-off-by: Sheng Chen <[email protected]>
1 parent b1d0831 commit 86bf3ae

File tree

11 files changed

+419
-23
lines changed

11 files changed

+419
-23
lines changed

package.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@
7272
"^pom\\.xml$",
7373
".*\\.gradle(\\.kts)?$"
7474
],
75+
"javaBuildTools": [
76+
{
77+
"displayName": "Maven",
78+
"buildFileNames": ["pom.xml"]
79+
},
80+
{
81+
"displayName": "Gradle",
82+
"buildFileNames": ["build.gradle", "settings.gradle", "build.gradle.kts", "settings.gradle.kts"]
83+
}
84+
],
7585
"semanticTokenTypes": [
7686
{
7787
"id": "annotation",
@@ -346,6 +356,21 @@
346356
"title": "Project Import/Update",
347357
"order": 20,
348358
"properties": {
359+
"java.import.projectSelection": {
360+
"type": "string",
361+
"enum": [
362+
"manual",
363+
"automatic"
364+
],
365+
"enumDescriptions": [
366+
"Manually select the build configuration files.",
367+
"Let extension automatically scan and select the build configuration files."
368+
],
369+
"default": "automatic",
370+
"markdownDescription": "[Experimental] Specifies how to select build configuration files to import. \nNote: Currently, `Gradle` projects cannot be partially imported.",
371+
"scope": "window",
372+
"order": 10
373+
},
349374
"java.configuration.updateBuildConfiguration": {
350375
"type": [
351376
"string"
@@ -358,7 +383,7 @@
358383
"default": "interactive",
359384
"description": "Specifies how modifications on build files update the Java classpath/configuration",
360385
"scope": "window",
361-
"order": 10
386+
"order": 20
362387
},
363388
"java.import.exclusions": {
364389
"type": "array",

schemas/package.schema.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,26 @@
2121
"type": "string",
2222
"description": "Regular expressions for specifying build file"
2323
}
24+
},
25+
"javaBuildTools": {
26+
"type": "array",
27+
"description": "Information about the cared build files. Will be used when 'java.import.projectSelection' is 'manual'.",
28+
"items": {
29+
"type": "object",
30+
"properties": {
31+
"displayName": {
32+
"description": "The display name of the build file type.",
33+
"type": "string"
34+
},
35+
"buildFileNames": {
36+
"description": "The build file names that supported by the build tool.",
37+
"type": "array",
38+
"items": {
39+
"type": "string"
40+
}
41+
}
42+
}
43+
}
2444
}
2545
}
2646
}

src/apiManager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Emitter } from "vscode-languageclient";
1010
import { ServerMode } from "./settings";
1111
import { registerHoverCommand } from "./hoverAction";
1212
import { onDidRequestEnd, onWillRequestStart } from "./TracingLanguageClient";
13+
import { getJavaConfiguration } from "./utils";
1314

1415
class ApiManager {
1516

@@ -22,6 +23,11 @@ class ApiManager {
2223
private serverReadyPromiseResolve: (result: boolean) => void;
2324

2425
public initialize(requirements: RequirementsData, serverMode: ServerMode): void {
26+
// if it's manual import mode, set the server mode to lightwight, so that the
27+
// project explorer won't spinning until import project is triggered.
28+
if (getJavaConfiguration().get<string>("import.projectSelection") === "manual") {
29+
serverMode = ServerMode.lightWeight;
30+
}
2531
const getDocumentSymbols: GetDocumentSymbolsCommand = getDocumentSymbolsProvider();
2632
const goToDefinition: GoToDefinitionCommand = goToDefinitionProvider();
2733

src/buildFilesSelector.ts

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { ExtensionContext, MessageItem, QuickPickItem, QuickPickItemKind, Uri, WorkspaceFolder, extensions, window, workspace } from "vscode";
2+
import { convertToGlob, getExclusionGlob as getExclusionGlobPattern, getInclusionPatternsFromNegatedExclusion } from "./utils";
3+
import * as path from "path";
4+
5+
export const PICKED_BUILD_FILES = "java.pickedBuildFiles";
6+
export class BuildFileSelector {
7+
private buildTypes: IBuildTool[] = [];
8+
private context: ExtensionContext;
9+
private exclusionGlobPattern: string;
10+
// cached glob pattern for build files.
11+
private searchPattern: string;
12+
// cached glob pattern for build files that are explicitly
13+
// included from the setting: "java.import.exclusions" (negated exclusion).
14+
private negatedExclusionSearchPattern: string | undefined;
15+
16+
constructor(context: ExtensionContext) {
17+
this.context = context;
18+
// TODO: should we introduce the exclusion globs into the contribution point?
19+
this.exclusionGlobPattern = getExclusionGlobPattern(["**/target/**", "**/bin/**", "**/build/**"]);
20+
for (const extension of extensions.all) {
21+
const javaBuildTools: IBuildTool[] = extension.packageJSON.contributes?.javaBuildTools;
22+
if (!Array.isArray(javaBuildTools)) {
23+
continue;
24+
}
25+
26+
for (const buildType of javaBuildTools) {
27+
if (!this.isValidBuildTypeConfiguration(buildType)) {
28+
continue;
29+
}
30+
31+
this.buildTypes.push(buildType);
32+
}
33+
}
34+
this.searchPattern = `**/{${this.buildTypes.map(buildType => buildType.buildFileNames.join(","))}}`;
35+
const inclusionFolderPatterns: string[] = getInclusionPatternsFromNegatedExclusion();
36+
if (inclusionFolderPatterns.length > 0) {
37+
const buildFileNames: string[] = [];
38+
this.buildTypes.forEach(buildType => buildFileNames.push(...buildType.buildFileNames));
39+
this.negatedExclusionSearchPattern = convertToGlob(buildFileNames, inclusionFolderPatterns);
40+
}
41+
}
42+
43+
/**
44+
* @returns `true` if there are build files in the workspace, `false` otherwise.
45+
*/
46+
public async hasBuildFiles(): Promise<boolean> {
47+
if (this.buildTypes.length === 0) {
48+
return false;
49+
}
50+
51+
let uris: Uri[];
52+
if (this.negatedExclusionSearchPattern) {
53+
uris = await workspace.findFiles(this.negatedExclusionSearchPattern, null /* force not use default exclusion */, 1);
54+
if (uris.length > 0) {
55+
return true;
56+
}
57+
}
58+
uris = await workspace.findFiles(this.searchPattern, this.exclusionGlobPattern, 1);
59+
if (uris.length > 0) {
60+
return true;
61+
}
62+
return false;
63+
}
64+
65+
/**
66+
* Get the uri strings for the build files that the user selected.
67+
* @returns An array of uri string for the build files that the user selected.
68+
* An empty array means user canceled the selection.
69+
*/
70+
public async getBuildFiles(): Promise<string[] | undefined> {
71+
const cache = this.context.workspaceState.get<string[]>(PICKED_BUILD_FILES);
72+
if (cache !== undefined) {
73+
return cache;
74+
}
75+
76+
const choice = await this.chooseBuildFilePickers();
77+
const pickedUris = await this.eliminateBuildToolConflict(choice);
78+
if (pickedUris.length > 0) {
79+
this.context.workspaceState.update(PICKED_BUILD_FILES, pickedUris);
80+
}
81+
return pickedUris;
82+
}
83+
84+
private isValidBuildTypeConfiguration(buildType: IBuildTool): boolean {
85+
return !!buildType.displayName && !!buildType.buildFileNames;
86+
}
87+
88+
private async chooseBuildFilePickers(): Promise<IBuildFilePicker[]> {
89+
return window.showQuickPick(this.getBuildFilePickers(), {
90+
placeHolder: "Note: Currently only Maven projects can be partially imported.",
91+
title: "Select build files to import",
92+
ignoreFocusOut: true,
93+
canPickMany: true,
94+
matchOnDescription: true,
95+
matchOnDetail: true,
96+
});
97+
}
98+
99+
/**
100+
* Get pickers for all build files in the workspace.
101+
*/
102+
private async getBuildFilePickers(): Promise<IBuildFilePicker[]> {
103+
const uris: Uri[] = await workspace.findFiles(this.searchPattern, this.exclusionGlobPattern);
104+
if (this.negatedExclusionSearchPattern) {
105+
uris.push(...await workspace.findFiles(this.negatedExclusionSearchPattern, null /* force not use default exclusion */));
106+
}
107+
108+
// group build files by build tool and then sort them by build tool name.
109+
const groupByBuildTool = new Map<IBuildTool, Uri[]>();
110+
for (const uri of uris) {
111+
const buildType = this.buildTypes.find(buildType => buildType.buildFileNames.includes(path.basename(uri.fsPath)));
112+
if (!buildType) {
113+
continue;
114+
}
115+
if (!groupByBuildTool.has(buildType)) {
116+
groupByBuildTool.set(buildType, []);
117+
}
118+
groupByBuildTool.get(buildType)?.push(uri);
119+
}
120+
121+
const buildTypeArray = Array.from(groupByBuildTool.keys());
122+
buildTypeArray.sort((a, b) => a.displayName.localeCompare(b.displayName));
123+
const addedFolders: Map<string, IBuildFilePicker> = new Map<string, IBuildFilePicker>();
124+
for (const buildType of buildTypeArray) {
125+
const uris = groupByBuildTool.get(buildType);
126+
for (const uri of uris) {
127+
const containingFolder = path.dirname(uri.fsPath);
128+
if (addedFolders.has(containingFolder)) {
129+
const picker = addedFolders.get(containingFolder);
130+
if (!picker.buildTypeAndUri.has(buildType)) {
131+
picker.detail += `, ./${workspace.asRelativePath(uri)}`;
132+
picker.description += `, ${buildType.displayName}`;
133+
picker.buildTypeAndUri.set(buildType, uri);
134+
}
135+
} else {
136+
addedFolders.set(containingFolder, {
137+
label: path.basename(containingFolder),
138+
detail: `./${workspace.asRelativePath(uri)}`,
139+
description: buildType.displayName,
140+
buildTypeAndUri: new Map<IBuildTool, Uri>([[buildType, uri]]),
141+
picked: true,
142+
});
143+
}
144+
}
145+
}
146+
147+
const pickers: IBuildFilePicker[] = Array.from(addedFolders.values());
148+
return this.addSeparator(pickers);
149+
}
150+
151+
/**
152+
* Add a separator pickers between pickers that belong to different workspace folders.
153+
*/
154+
private addSeparator(pickers: IBuildFilePicker[]): IBuildFilePicker[] {
155+
// group pickers by their containing workspace folder
156+
const workspaceFolders = new Map<WorkspaceFolder, IBuildFilePicker[]>();
157+
for (const picker of pickers) {
158+
const folder = workspace.getWorkspaceFolder(picker.buildTypeAndUri.values().next().value);
159+
if (!folder) {
160+
continue;
161+
}
162+
if (!workspaceFolders.has(folder)) {
163+
workspaceFolders.set(folder, []);
164+
}
165+
workspaceFolders.get(folder)?.push(picker);
166+
}
167+
168+
const newPickers: IBuildFilePicker[] = [];
169+
const folderArray = Array.from(workspaceFolders.keys());
170+
folderArray.sort((a, b) => a.name.localeCompare(b.name));
171+
for (const folder of folderArray) {
172+
const pickersInFolder = workspaceFolders.get(folder);
173+
newPickers.push({
174+
label: folder.name,
175+
kind: QuickPickItemKind.Separator,
176+
buildTypeAndUri: null
177+
});
178+
newPickers.push(...this.sortPickers(pickersInFolder));
179+
}
180+
return newPickers;
181+
}
182+
183+
private sortPickers(pickers: IBuildFilePicker[]): IBuildFilePicker[] {
184+
return pickers.sort((a, b) => {
185+
const pathA = path.dirname(a.buildTypeAndUri.values().next().value.fsPath);
186+
const pathB = path.dirname(b.buildTypeAndUri.values().next().value.fsPath);
187+
return pathA.localeCompare(pathB);
188+
});
189+
}
190+
191+
/**
192+
* Ask user to choose a build tool when there are multiple build tools in the same folder.
193+
*/
194+
private async eliminateBuildToolConflict(choice?: IBuildFilePicker[]): Promise<string[]> {
195+
if (!choice) {
196+
return [];
197+
}
198+
const conflictBuildTypeAndUris = new Map<IBuildTool, Uri[]>();
199+
const result: string[] = [];
200+
for (const picker of choice) {
201+
if (picker.buildTypeAndUri.size > 1) {
202+
for (const [buildType, uri] of picker.buildTypeAndUri) {
203+
if (!conflictBuildTypeAndUris.has(buildType)) {
204+
conflictBuildTypeAndUris.set(buildType, []);
205+
}
206+
conflictBuildTypeAndUris.get(buildType)?.push(uri);
207+
}
208+
} else {
209+
result.push(picker.buildTypeAndUri.values().next().value.toString());
210+
}
211+
}
212+
213+
if (conflictBuildTypeAndUris.size > 0) {
214+
const conflictItems: IConflictItem[] = [];
215+
for (const buildType of conflictBuildTypeAndUris.keys()) {
216+
conflictItems.push({
217+
title: buildType.displayName,
218+
uris: conflictBuildTypeAndUris.get(buildType),
219+
});
220+
}
221+
conflictItems.sort((a, b) => a.title.localeCompare(b.title));
222+
conflictItems.push({
223+
title: "Skip",
224+
isCloseAffordance: true,
225+
});
226+
227+
const choice = await window.showInformationMessage<IConflictItem>(
228+
"Which build tool would you like to use for the workspace?",
229+
{
230+
modal: true,
231+
},
232+
...conflictItems
233+
);
234+
235+
if (choice?.title !== "Skip" && choice?.uris) {
236+
result.push(...choice.uris.map(uri => uri.toString()));
237+
}
238+
}
239+
return result;
240+
}
241+
}
242+
243+
interface IBuildTool {
244+
displayName: string;
245+
buildFileNames: string[];
246+
}
247+
248+
interface IConflictItem extends MessageItem {
249+
uris?: Uri[];
250+
}
251+
252+
interface IBuildFilePicker extends QuickPickItem {
253+
buildTypeAndUri: Map<IBuildTool, Uri>;
254+
}
255+
256+
export function cleanupProjectPickerCache(context: ExtensionContext) {
257+
context.workspaceState.update(PICKED_BUILD_FILES, undefined);
258+
}

0 commit comments

Comments
 (0)