Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add capability to select projects to be imported #3356

Merged
merged 4 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@
"^pom\\.xml$",
".*\\.gradle(\\.kts)?$"
],
"javaBuildTools": [
{
"displayName": "Maven",
"buildFileNames": ["pom.xml"]
},
{
"displayName": "Gradle",
"buildFileNames": ["build.gradle", "settings.gradle", "build.gradle.kts", "settings.gradle.kts"]
}
],
"semanticTokenTypes": [
{
"id": "annotation",
Expand Down Expand Up @@ -346,6 +356,21 @@
"title": "Project Import/Update",
"order": 20,
"properties": {
"java.import.projectSelection": {
"type": "string",
"enum": [
"manual",
"automatic"
],
"enumDescriptions": [
"Manually select the build configuration files.",
"Let extension automatically scan and select the build configuration files."
],
"default": "automatic",
"markdownDescription": "[Experimental] Specifies how to select build configuration files to import. \nNote: Currently, `Gradle` projects cannot be partially imported.",
"scope": "window",
"order": 10
},
"java.configuration.updateBuildConfiguration": {
"type": [
"string"
Expand All @@ -358,7 +383,7 @@
"default": "interactive",
"description": "Specifies how modifications on build files update the Java classpath/configuration",
"scope": "window",
"order": 10
"order": 20
},
"java.import.exclusions": {
"type": "array",
Expand Down
20 changes: 20 additions & 0 deletions schemas/package.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@
"type": "string",
"description": "Regular expressions for specifying build file"
}
},
"javaBuildTools": {
"type": "array",
"description": "Information about the cared build files. Will be used when 'java.import.projectSelection' is 'manual'.",
"items": {
"type": "object",
"properties": {
"displayName": {
"description": "The display name of the build file type.",
"type": "string"
},
"buildFileNames": {
"description": "The build file names that supported by the build tool.",
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/apiManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Emitter } from "vscode-languageclient";
import { ServerMode } from "./settings";
import { registerHoverCommand } from "./hoverAction";
import { onDidRequestEnd, onWillRequestStart } from "./TracingLanguageClient";
import { getJavaConfiguration } from "./utils";

class ApiManager {

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

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

Expand Down
258 changes: 258 additions & 0 deletions src/buildFilesSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import { ExtensionContext, MessageItem, QuickPickItem, QuickPickItemKind, Uri, WorkspaceFolder, extensions, window, workspace } from "vscode";
import { convertToGlob, getExclusionGlob as getExclusionGlobPattern, getInclusionPatternsFromNegatedExclusion } from "./utils";
import * as path from "path";

export const PICKED_BUILD_FILES = "java.pickedBuildFiles";
export class BuildFileSelector {
private buildTypes: IBuildTool[] = [];
private context: ExtensionContext;
private exclusionGlobPattern: string;
// cached glob pattern for build files.
private searchPattern: string;
// cached glob pattern for build files that are explicitly
// included from the setting: "java.import.exclusions" (negated exclusion).
private negatedExclusionSearchPattern: string | undefined;

constructor(context: ExtensionContext) {
this.context = context;
// TODO: should we introduce the exclusion globs into the contribution point?
this.exclusionGlobPattern = getExclusionGlobPattern(["**/target/**", "**/bin/**", "**/build/**"]);
for (const extension of extensions.all) {
const javaBuildTools: IBuildTool[] = extension.packageJSON.contributes?.javaBuildTools;
if (!Array.isArray(javaBuildTools)) {
continue;
}

for (const buildType of javaBuildTools) {
if (!this.isValidBuildTypeConfiguration(buildType)) {
continue;
}

this.buildTypes.push(buildType);
}
}
this.searchPattern = `**/{${this.buildTypes.map(buildType => buildType.buildFileNames.join(","))}}`;
const inclusionFolderPatterns: string[] = getInclusionPatternsFromNegatedExclusion();
if (inclusionFolderPatterns.length > 0) {
const buildFileNames: string[] = [];
this.buildTypes.forEach(buildType => buildFileNames.push(...buildType.buildFileNames));
this.negatedExclusionSearchPattern = convertToGlob(buildFileNames, inclusionFolderPatterns);
}
}

/**
* @returns `true` if there are build files in the workspace, `false` otherwise.
*/
public async hasBuildFiles(): Promise<boolean> {
if (this.buildTypes.length === 0) {
return false;
}

let uris: Uri[];
if (this.negatedExclusionSearchPattern) {
uris = await workspace.findFiles(this.negatedExclusionSearchPattern, null /* force not use default exclusion */, 1);
if (uris.length > 0) {
return true;
}
}
uris = await workspace.findFiles(this.searchPattern, this.exclusionGlobPattern, 1);
if (uris.length > 0) {
return true;
}
return false;
}

/**
* Get the uri strings for the build files that the user selected.
* @returns An array of uri string for the build files that the user selected.
* An empty array means user canceled the selection.
*/
public async getBuildFiles(): Promise<string[] | undefined> {
const cache = this.context.workspaceState.get<string[]>(PICKED_BUILD_FILES);
if (cache !== undefined) {
return cache;
}

const choice = await this.chooseBuildFilePickers();
const pickedUris = await this.eliminateBuildToolConflict(choice);
if (pickedUris.length > 0) {
this.context.workspaceState.update(PICKED_BUILD_FILES, pickedUris);
}
return pickedUris;
}

private isValidBuildTypeConfiguration(buildType: IBuildTool): boolean {
return !!buildType.displayName && !!buildType.buildFileNames;
}

private async chooseBuildFilePickers(): Promise<IBuildFilePicker[]> {
return window.showQuickPick(this.getBuildFilePickers(), {
placeHolder: "Note: Currently only Maven projects can be partially imported.",
title: "Select build files to import",
ignoreFocusOut: true,
canPickMany: true,
matchOnDescription: true,
matchOnDetail: true,
});
}

/**
* Get pickers for all build files in the workspace.
*/
private async getBuildFilePickers(): Promise<IBuildFilePicker[]> {
const uris: Uri[] = await workspace.findFiles(this.searchPattern, this.exclusionGlobPattern);
if (this.negatedExclusionSearchPattern) {
uris.push(...await workspace.findFiles(this.negatedExclusionSearchPattern, null /* force not use default exclusion */));
}

// group build files by build tool and then sort them by build tool name.
const groupByBuildTool = new Map<IBuildTool, Uri[]>();
for (const uri of uris) {
const buildType = this.buildTypes.find(buildType => buildType.buildFileNames.includes(path.basename(uri.fsPath)));
if (!buildType) {
continue;
}
if (!groupByBuildTool.has(buildType)) {
groupByBuildTool.set(buildType, []);
}
groupByBuildTool.get(buildType)?.push(uri);
}

const buildTypeArray = Array.from(groupByBuildTool.keys());
buildTypeArray.sort((a, b) => a.displayName.localeCompare(b.displayName));
const addedFolders: Map<string, IBuildFilePicker> = new Map<string, IBuildFilePicker>();
for (const buildType of buildTypeArray) {
const uris = groupByBuildTool.get(buildType);
for (const uri of uris) {
const containingFolder = path.dirname(uri.fsPath);
if (addedFolders.has(containingFolder)) {
const picker = addedFolders.get(containingFolder);
if (!picker.buildTypeAndUri.has(buildType)) {
picker.detail += `, ./${workspace.asRelativePath(uri)}`;
picker.description += `, ${buildType.displayName}`;
picker.buildTypeAndUri.set(buildType, uri);
}
} else {
addedFolders.set(containingFolder, {
label: path.basename(containingFolder),
detail: `./${workspace.asRelativePath(uri)}`,
description: buildType.displayName,
buildTypeAndUri: new Map<IBuildTool, Uri>([[buildType, uri]]),
picked: true,
});
}
}
}

const pickers: IBuildFilePicker[] = Array.from(addedFolders.values());
return this.addSeparator(pickers);
}

/**
* Add a separator pickers between pickers that belong to different workspace folders.
*/
private addSeparator(pickers: IBuildFilePicker[]): IBuildFilePicker[] {
// group pickers by their containing workspace folder
const workspaceFolders = new Map<WorkspaceFolder, IBuildFilePicker[]>();
for (const picker of pickers) {
const folder = workspace.getWorkspaceFolder(picker.buildTypeAndUri.values().next().value);
if (!folder) {
continue;
}
if (!workspaceFolders.has(folder)) {
workspaceFolders.set(folder, []);
}
workspaceFolders.get(folder)?.push(picker);
}

const newPickers: IBuildFilePicker[] = [];
const folderArray = Array.from(workspaceFolders.keys());
folderArray.sort((a, b) => a.name.localeCompare(b.name));
for (const folder of folderArray) {
const pickersInFolder = workspaceFolders.get(folder);
newPickers.push({
label: folder.name,
kind: QuickPickItemKind.Separator,
buildTypeAndUri: null
});
newPickers.push(...this.sortPickers(pickersInFolder));
}
return newPickers;
}

private sortPickers(pickers: IBuildFilePicker[]): IBuildFilePicker[] {
return pickers.sort((a, b) => {
const pathA = path.dirname(a.buildTypeAndUri.values().next().value.fsPath);
const pathB = path.dirname(b.buildTypeAndUri.values().next().value.fsPath);
return pathA.localeCompare(pathB);
});
}

/**
* Ask user to choose a build tool when there are multiple build tools in the same folder.
*/
private async eliminateBuildToolConflict(choice?: IBuildFilePicker[]): Promise<string[]> {
jdneo marked this conversation as resolved.
Show resolved Hide resolved
if (!choice) {
return [];
}
const conflictBuildTypeAndUris = new Map<IBuildTool, Uri[]>();
const result: string[] = [];
for (const picker of choice) {
if (picker.buildTypeAndUri.size > 1) {
for (const [buildType, uri] of picker.buildTypeAndUri) {
if (!conflictBuildTypeAndUris.has(buildType)) {
conflictBuildTypeAndUris.set(buildType, []);
}
conflictBuildTypeAndUris.get(buildType)?.push(uri);
}
} else {
result.push(picker.buildTypeAndUri.values().next().value.toString());
}
}

if (conflictBuildTypeAndUris.size > 0) {
const conflictItems: IConflictItem[] = [];
for (const buildType of conflictBuildTypeAndUris.keys()) {
conflictItems.push({
title: buildType.displayName,
uris: conflictBuildTypeAndUris.get(buildType),
});
}
conflictItems.sort((a, b) => a.title.localeCompare(b.title));
conflictItems.push({
title: "Skip",
isCloseAffordance: true,
});

const choice = await window.showInformationMessage<IConflictItem>(
"Which build tool would you like to use for the workspace?",
{
modal: true,
},
...conflictItems
);

if (choice?.title !== "Skip" && choice?.uris) {
result.push(...choice.uris.map(uri => uri.toString()));
}
}
return result;
}
}

interface IBuildTool {
displayName: string;
buildFileNames: string[];
}

interface IConflictItem extends MessageItem {
uris?: Uri[];
}

interface IBuildFilePicker extends QuickPickItem {
buildTypeAndUri: Map<IBuildTool, Uri>;
}

export function cleanupProjectPickerCache(context: ExtensionContext) {
context.workspaceState.update(PICKED_BUILD_FILES, undefined);
}
Loading
Loading