Skip to content

Commit

Permalink
Support to manually select Maven pom files to import
Browse files Browse the repository at this point in the history
- 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]>
  • Loading branch information
jdneo committed Oct 27, 2023
1 parent 418e36a commit 91de44f
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 22 deletions.
23 changes: 23 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
"^pom\\.xml$",
".*\\.gradle(\\.kts)?$"
],
"javaBuildTypes": [
{
"displayName": "Maven",
"fileSearchPattern": "**/pom.xml",
"controlledBy": "java.import.maven.enabled"
}
],
"semanticTokenTypes": [
{
"id": "annotation",
Expand Down Expand Up @@ -271,6 +278,22 @@
"description": "Traces the communication between VS Code and the Java language server.",
"scope": "window"
},
"java.import.configurationFileCollectionMode": {
"type": "string",
"enum": [
"manual",
"automatic",
"askUser"
],
"enumDescriptions": [
"Manually select the build configuration files.",
"Let extension automatically scan and select the build configuration files.",
"Always ask for the mode before import"
],
"default": "automatic",
"markdownDescription": "[Experimental] Specifies how to collect build configuration files to import. \nNote: Currently, only `Maven` projects will be searched. Please use `automatic` when you want to import `Gradle` projects",
"scope": "window"
},
"java.import.maven.enabled": {
"type": "boolean",
"default": true,
Expand Down
21 changes: 21 additions & 0 deletions schemas/package.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@
"type": "string",
"description": "Regular expressions for specifying build file"
}
},
"javaBuildTypes": {
"type": "array",
"description": "Information about the cared build files. Will be used when 'java.import.configurationFileCollectionMode' is 'manual'.",
"items": {
"type": "object",
"properties": {
"displayName": {
"description": "The display name of the build file type.",
"type": "string"
},
"fileSearchPattern": {
"description": "The glob pattern used to search the build files.",
"type": "string"
},
"controlledBy": {
"description": "The boolean typed extension settings that enables/disables the build file type. If not provided, the build file type is always enabled.",
"type": "string"
}
}
}
}
}
}
Expand Down
214 changes: 214 additions & 0 deletions src/buildFilesSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { ExtensionContext, MessageItem, QuickPickItem, QuickPickItemKind, Uri, WorkspaceFolder, extensions, window, workspace } from "vscode";
import { getExclusionGlob as getExclusionGlobPattern } from "./utils";
import * as path from "path";

export const PICKED_BUILD_FILES = "java.pickedBuildFiles";
export class BuildFileSelector {
private buildTypes: IBuildType[] = [];
private context: ExtensionContext;
private exclusionGlobPattern: string;

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 javaBuildTypes: IBuildType[] = extension.packageJSON.contributes?.javaBuildTypes;
if (!Array.isArray(javaBuildTypes)) {
continue;
}

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

if (buildType.controlledBy && !workspace.getConfiguration().get(buildType.controlledBy)) {
continue;
}

this.buildTypes.push(buildType);
}
}
}

/**
* @returns `true` if there are build files in the workspace, `false` otherwise.
*/
public async hasBuildFiles(): Promise<boolean> {
for (const buildType of this.buildTypes) {
const uris: Uri[] = await workspace.findFiles(buildType.fileSearchPattern, 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: IBuildType): boolean {
return !!buildType.displayName && !!buildType.fileSearchPattern;
}

private async chooseBuildFilePickers(): Promise<IBuildFilePicker[]> {
return window.showQuickPick(this.getBuildFilePickers(), {
placeHolder: "Select build file 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 addedFolders: Map<string, IBuildFilePicker> = new Map<string, IBuildFilePicker>();
for (const buildType of this.buildTypes) {
const uris: Uri[] = await workspace.findFiles(buildType.fileSearchPattern, this.exclusionGlobPattern);
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<IBuildType, 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[]> {
if (!choice) {
return [];
}
const conflictPickers = new Set<IBuildFilePicker>();
const result: string[] = [];
for (const picker of choice) {
if (picker.buildTypeAndUri.size > 1) {
conflictPickers.add(picker);
} else {
result.push(picker.buildTypeAndUri.values().next().value.toString());
}
}

if (conflictPickers.size > 0) {
for (const picker of conflictPickers) {
const conflictItems: IConflictItem[] = [{
title: "Skip",
isCloseAffordance: true,
}];
for (const buildType of picker.buildTypeAndUri.keys()) {
conflictItems.push({
title: buildType.displayName,
uri: picker.buildTypeAndUri.get(buildType),
});
}
const choice = await window.showInformationMessage<IConflictItem>(
`Which build tool would you like to use for folder: ${picker.label}?`,
{
modal: true,
},
...conflictItems
);

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

interface IBuildType {
displayName: string;
fileSearchPattern: string;
controlledBy: string | undefined;
}

interface IConflictItem extends MessageItem {
uri?: Uri;
}

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

export function cleanupProjectPickerCache(context: ExtensionContext) {
context.workspaceState.update(PICKED_BUILD_FILES, undefined);
}
35 changes: 25 additions & 10 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as fs from 'fs';
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { CodeActionContext, commands, ConfigurationTarget, Diagnostic, env, EventEmitter, ExtensionContext, extensions, IndentAction, InputBoxOptions, languages, RelativePattern, TextDocument, UIKind, Uri, ViewColumn, window, workspace, WorkspaceConfiguration, ProgressLocation, Position, Selection, Range } from 'vscode';
import { CodeActionContext, commands, ConfigurationTarget, Diagnostic, env, EventEmitter, ExtensionContext, extensions, IndentAction, InputBoxOptions, languages, RelativePattern, TextDocument, UIKind, Uri, ViewColumn, window, workspace, WorkspaceConfiguration, version } from 'vscode';
import { CancellationToken, CodeActionParams, CodeActionRequest, Command, CompletionRequest, DidChangeConfigurationNotification, ExecuteCommandParams, ExecuteCommandRequest, LanguageClientOptions, RevealOutputChannelOn } from 'vscode-languageclient';
import { LanguageClient } from 'vscode-languageclient/node';
import { apiManager } from './apiManager';
Expand All @@ -24,18 +24,19 @@ import { initialize as initializeRecommendation } from './recommendation';
import * as requirements from './requirements';
import { runtimeStatusBarProvider } from './runtimeStatusBarProvider';
import { serverStatusBarProvider } from './serverStatusBarProvider';
import { ACTIVE_BUILD_TOOL_STATE, cleanWorkspaceFileName, getJavaServerMode, handleTextDocumentChanges, onConfigurationChange, ServerMode } from './settings';
import { ACTIVE_BUILD_TOOL_STATE, cleanWorkspaceFileName, getJavaServerMode, handleTextDocumentChanges, getImportMode, onConfigurationChange, ServerMode, ImportMode } from './settings';
import { snippetCompletionProvider } from './snippetCompletionProvider';
import { JavaClassEditorProvider } from './javaClassEditor';
import { StandardLanguageClient } from './standardLanguageClient';
import { SyntaxLanguageClient } from './syntaxLanguageClient';
import { convertToGlob, deleteDirectory, ensureExists, getBuildFilePatterns, getExclusionBlob, getInclusionPatternsFromNegatedExclusion, getJavaConfig, getJavaConfiguration, hasBuildToolConflicts } from './utils';
import { convertToGlob, deleteDirectory, ensureExists, getBuildFilePatterns, getExclusionGlob, getInclusionPatternsFromNegatedExclusion, getJavaConfig, getJavaConfiguration, hasBuildToolConflicts } from './utils';
import glob = require('glob');
import { Telemetry } from './telemetry';
import { getMessage } from './errorUtils';
import { TelemetryService } from '@redhat-developer/vscode-redhat-telemetry/lib';
import { activationProgressNotification } from "./serverTaskPresenter";
import { loadSupportedJreNames } from './jdkUtils';
import { BuildFileSelector, cleanupProjectPickerCache } from './buildFilesSelector';

const syntaxClient: SyntaxLanguageClient = new SyntaxLanguageClient();
const standardClient: StandardLanguageClient = new StandardLanguageClient();
Expand Down Expand Up @@ -320,6 +321,7 @@ export async function activate(context: ExtensionContext): Promise<ExtensionAPI>
const data = {};
try {
cleanupLombokCache(context);
cleanupProjectPickerCache(context);
deleteDirectory(workspacePath);
deleteDirectory(syntaxServerWorkspacePath);
} catch (error) {
Expand Down Expand Up @@ -378,7 +380,7 @@ export async function activate(context: ExtensionContext): Promise<ExtensionAPI>
}

const api: ExtensionAPI = apiManager.getApiInstance();
if (api.serverMode === switchTo || api.serverMode === ServerMode.standard) {
if (!force && (api.serverMode === switchTo || api.serverMode === ServerMode.standard)) {
return;
}

Expand Down Expand Up @@ -464,9 +466,23 @@ async function startStandardServer(context: ExtensionContext, requirements: requ
return;
}

const checkConflicts: boolean = await ensureNoBuildToolConflicts(context, clientOptions);
if (!checkConflicts) {
return;
const selector: BuildFileSelector = new BuildFileSelector(context);
const importMode: ImportMode = await getImportMode(context, selector);
if (importMode === ImportMode.automatic) {
if (!await ensureNoBuildToolConflicts(context, clientOptions)) {
return;
}
} else {
const buildFiles: string[] = [];
if (importMode === ImportMode.manual) {
buildFiles.push(...await selector.getBuildFiles());
}
if (buildFiles.length === 0) {
// cancelled by user
serverStatusBarProvider.showNotImportedStatus();
return;
}
clientOptions.initializationOptions.projectConfigurations = buildFiles;
}

if (apiManager.getApiInstance().serverMode === ServerMode.lightWeight) {
Expand All @@ -493,7 +509,7 @@ async function workspaceContainsBuildFiles(): Promise<boolean> {

// Nothing found in negated exclusion pattern, do a normal search then.
const inclusionBlob: string = convertToGlob(inclusionPatterns);
const exclusionBlob: string = getExclusionBlob();
const exclusionBlob: string = getExclusionGlob();
if (inclusionBlob && (await workspace.findFiles(inclusionBlob, exclusionBlob, 1 /* maxResults */)).length > 0) {
return true;
}
Expand Down Expand Up @@ -919,8 +935,7 @@ async function getTriggerFiles(): Promise<string[]> {
}

// Paths set by 'java.import.exclusions' will be ignored when searching trigger files.
const exclusionGlob = getExclusionBlob();

const exclusionGlob = getExclusionGlob();
const javaFilesUnderRoot: Uri[] = await workspace.findFiles(new RelativePattern(rootFolder, "*.java"), exclusionGlob, 1);
for (const javaFile of javaFilesUnderRoot) {
if (isPrefix(rootPath, javaFile.fsPath)) {
Expand Down
Loading

0 comments on commit 91de44f

Please sign in to comment.