From da78c6bc598726d2f3656cf5a556749a4e8e7458 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Fri, 27 Oct 2023 11:41:51 +0800 Subject: [PATCH 1/4] 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 --- package.json | 24 +++ schemas/package.schema.json | 17 ++ src/buildFilesSelector.ts | 210 +++++++++++++++++++++ src/extension.ts | 35 +++- src/languageStatusItemFactory.ts | 16 +- src/serverStatusBarProvider.ts | 17 +- src/settings.ts | 35 ++++ src/utils.ts | 17 +- test/standard-mode-suite/extension.test.ts | 1 + test/standard-mode-suite/utils.test.ts | 10 +- 10 files changed, 360 insertions(+), 22 deletions(-) create mode 100644 src/buildFilesSelector.ts diff --git a/package.json b/package.json index 4caefe86b..ec1fc6b9e 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,16 @@ "^pom\\.xml$", ".*\\.gradle(\\.kts)?$" ], + "javaBuildTools": [ + { + "displayName": "Maven", + "fileSearchPattern": "**/pom.xml" + }, + { + "displayName": "Gradle", + "fileSearchPattern": "**/{build,settings}.gradle*" + } + ], "semanticTokenTypes": [ { "id": "annotation", @@ -397,6 +407,20 @@ "default": false, "scope": "window" }, + "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" + }, "java.project.importOnFirstTimeStartup": { "type": "string", "enum": [ diff --git a/schemas/package.schema.json b/schemas/package.schema.json index a147e2d3a..183026fcb 100644 --- a/schemas/package.schema.json +++ b/schemas/package.schema.json @@ -21,6 +21,23 @@ "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" + }, + "fileSearchPattern": { + "description": "The glob pattern used to search the build files.", + "type": "string" + } + } + } } } } diff --git a/src/buildFilesSelector.ts b/src/buildFilesSelector.ts new file mode 100644 index 000000000..0d2abdf90 --- /dev/null +++ b/src/buildFilesSelector.ts @@ -0,0 +1,210 @@ +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: IBuildTool[] = []; + 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 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); + } + } + } + + /** + * @returns `true` if there are build files in the workspace, `false` otherwise. + */ + public async hasBuildFiles(): Promise { + 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 { + const cache = this.context.workspaceState.get(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.fileSearchPattern; + } + + private async chooseBuildFilePickers(): Promise { + 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 { + const addedFolders: Map = new Map(); + 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([[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(); + 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 { + if (!choice) { + return []; + } + const conflictPickers = new Set(); + 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( + `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 IBuildTool { + displayName: string; + fileSearchPattern: string; +} + +interface IConflictItem extends MessageItem { + uri?: Uri; +} + +interface IBuildFilePicker extends QuickPickItem { + buildTypeAndUri: Map; +} + +export function cleanupProjectPickerCache(context: ExtensionContext) { + context.workspaceState.update(PICKED_BUILD_FILES, undefined); +} diff --git a/src/extension.ts b/src/extension.ts index 29f155870..5286b6c54 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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'; @@ -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, deleteClientLog, deleteDirectory, ensureExists, getBuildFilePatterns, getExclusionBlob, getInclusionPatternsFromNegatedExclusion, getJavaConfig, getJavaConfiguration, hasBuildToolConflicts } from './utils'; +import { convertToGlob, deleteClientLog, 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(); @@ -323,6 +324,7 @@ export async function activate(context: ExtensionContext): Promise const data = {}; try { cleanupLombokCache(context); + cleanupProjectPickerCache(context); deleteDirectory(workspacePath); deleteDirectory(syntaxServerWorkspacePath); } catch (error) { @@ -381,7 +383,7 @@ export async function activate(context: ExtensionContext): Promise } const api: ExtensionAPI = apiManager.getApiInstance(); - if (api.serverMode === switchTo || api.serverMode === ServerMode.standard) { + if (!force && (api.serverMode === switchTo || api.serverMode === ServerMode.standard)) { return; } @@ -467,9 +469,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) { @@ -496,7 +512,7 @@ async function workspaceContainsBuildFiles(): Promise { // 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; } @@ -922,8 +938,7 @@ async function getTriggerFiles(): Promise { } // 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)) { diff --git a/src/languageStatusItemFactory.ts b/src/languageStatusItemFactory.ts index 12c310ec8..19aa99b3e 100644 --- a/src/languageStatusItemFactory.ts +++ b/src/languageStatusItemFactory.ts @@ -22,7 +22,7 @@ export namespace StatusCommands { export const switchToStandardCommand = { title: "Load Projects", command: Commands.SWITCH_SERVER_MODE, - arguments: ["Standard", true], + arguments: ['Standard', true], tooltip: "LightWeight mode only provides limited features, please load projects to get full feature set" }; @@ -38,6 +38,13 @@ export namespace StatusCommands { arguments: ["java.configuration.runtimes"], tooltip: "Configure Java Runtime" }; + + export const startStandardServerCommand = { + title: "Load Projects", + command: Commands.SWITCH_SERVER_MODE, + arguments: ['Standard', true], + tooltip: "Load Projects" + }; } export namespace ServerStatusItemFactory { @@ -57,6 +64,13 @@ export namespace ServerStatusItemFactory { item.command = StatusCommands.switchToStandardCommand; } + export function showNotImportedStatus(item: any): void { + item.severity = vscode.LanguageStatusSeverity?.Warning; + item.text = StatusIcon.notImported; + item.detail = "No projects are Imported"; + item.command = StatusCommands.startStandardServerCommand; + } + export function showStandardStatus(item: any): void { item.severity = vscode.LanguageStatusSeverity?.Information; item.command = StatusCommands.showServerStatusCommand; diff --git a/src/serverStatusBarProvider.ts b/src/serverStatusBarProvider.ts index 57003877c..12c4fa216 100644 --- a/src/serverStatusBarProvider.ts +++ b/src/serverStatusBarProvider.ts @@ -43,6 +43,20 @@ class ServerStatusBarProvider implements Disposable { } } + public showNotImportedStatus(): void { + if (supportsLanguageStatus()) { + ServerStatusItemFactory.showNotImportedStatus(this.languageStatusItem); + } else { + if (this.isAdvancedStatusBarItem) { + (this.statusBarItem as any).name = "No projects are imported"; + } + this.statusBarItem.text = StatusIcon.notImported; + this.statusBarItem.command = StatusCommands.startStandardServerCommand; + this.statusBarItem.tooltip = "No projects are imported, click to load projects"; + this.statusBarItem.show(); + } + } + public showStandardStatus(): void { if (supportsLanguageStatus()) { ServerStatusItemFactory.showStandardStatus(this.languageStatusItem); @@ -112,7 +126,8 @@ export enum StatusIcon { busy = "$(sync~spin)", ready = "$(thumbsup)", warning = "$(thumbsdown)", - error = "$(thumbsdown)" + error = "$(thumbsdown)", + notImported = "$(info)" } export const serverStatusBarProvider: ServerStatusBarProvider = new ServerStatusBarProvider(); diff --git a/src/settings.ts b/src/settings.ts index b79c6f3c9..c601ff030 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -8,6 +8,7 @@ import { cleanupLombokCache } from './lombokSupport'; import { ensureExists, getJavaConfiguration } from './utils'; import { apiManager } from './apiManager'; import { isActive, setActive, smartSemicolonDetection } from './smartSemicolonDetection'; +import { BuildFileSelector, PICKED_BUILD_FILES } from './buildFilesSelector'; const DEFAULT_HIDDEN_FILES: string[] = ['**/.classpath', '**/.project', '**/.settings', '**/.factorypath']; const IS_WORKSPACE_JDK_ALLOWED = "java.ls.isJdkAllowed"; @@ -366,4 +367,38 @@ export function handleTextDocumentChanges(document: TextDocument, changes: reado } } +export async function getImportMode(context: ExtensionContext, selector: BuildFileSelector): Promise { + const mode = getJavaConfiguration().get("import.projectSelection"); + if (mode === "manual") { + // if no selectable build files, use automatic mode + const hasBuildFiles = await selector.hasBuildFiles(); + if (!hasBuildFiles) { + return ImportMode.automatic; + } + + // If the the manually picked build files has already cached, return manual mode. + if (context.workspaceState.get(PICKED_BUILD_FILES) !== undefined) { + return ImportMode.manual; + } + + const answer: string = await window.showInformationMessage( + "Java build files are detected in the workspace. How do you want to import them?", + { modal: true }, + "Import All", "Let Me Select"); + if (answer === "Import All") { + return ImportMode.automatic; + } else if (answer === "Let Me Select") { + return ImportMode.manual; + } + return ImportMode.skip; + } + + return ImportMode.automatic; +} + +export enum ImportMode { + automatic = 'automatic', + manual = 'manual', + skip = 'skip', +} diff --git a/src/utils.ts b/src/utils.ts index b68b010bb..5ca920ee2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -110,7 +110,11 @@ export function convertToGlob(filePatterns: string[], basePatterns?: string[]): return parseToStringGlob(patterns); } -export function getExclusionBlob(): string { +/** + * Merge the values of setting 'java.import.exclusions' into one glob pattern. + * @param additionalExclusions Additional exclusions to be merged into the glob pattern. + */ +export function getExclusionGlob(additionalExclusions?: string[]): string { const config = getJavaConfiguration(); const exclusions: string[] = config.get("import.exclusions", []); const patterns: string[] = []; @@ -121,6 +125,9 @@ export function getExclusionBlob(): string { patterns.push(exclusion); } + if (additionalExclusions) { + patterns.push(...additionalExclusions); + } return parseToStringGlob(patterns); } @@ -173,10 +180,10 @@ async function getBuildFilesInWorkspace(): Promise { buildFiles.push(...await workspace.findFiles(convertToGlob(inclusionFilePatterns, inclusionFolderPatterns), null /* force not use default exclusion */)); } - const inclusionBlob: string = convertToGlob(inclusionFilePatterns); - const exclusionBlob: string = getExclusionBlob(); - if (inclusionBlob) { - buildFiles.push(...await workspace.findFiles(inclusionBlob, exclusionBlob)); + const inclusionGlob: string = convertToGlob(inclusionFilePatterns); + const exclusionGlob: string = getExclusionGlob(); + if (inclusionGlob) { + buildFiles.push(...await workspace.findFiles(inclusionGlob, exclusionGlob)); } return buildFiles; diff --git a/test/standard-mode-suite/extension.test.ts b/test/standard-mode-suite/extension.test.ts index cfa9db42c..3fd5828b7 100644 --- a/test/standard-mode-suite/extension.test.ts +++ b/test/standard-mode-suite/extension.test.ts @@ -54,6 +54,7 @@ suite('Java Language Extension - Standard', () => { Commands.CLIPBOARD_ONPASTE, Commands.COMPILE_WORKSPACE, Commands.CONFIGURATION_UPDATE, + "java.action.configureFavoriteStaticMembers", Commands.CREATE_MODULE_INFO, Commands.CREATE_MODULE_INFO_COMMAND, Commands.EXECUTE_WORKSPACE_COMMAND, diff --git a/test/standard-mode-suite/utils.test.ts b/test/standard-mode-suite/utils.test.ts index 7659ba2c3..8ec72f1b8 100644 --- a/test/standard-mode-suite/utils.test.ts +++ b/test/standard-mode-suite/utils.test.ts @@ -1,7 +1,7 @@ 'use strict'; import * as assert from 'assert'; -import { getJavaConfiguration, getBuildFilePatterns, getInclusionPatternsFromNegatedExclusion, getExclusionBlob, convertToGlob } from '../../src/utils'; +import { getJavaConfiguration, getBuildFilePatterns, getInclusionPatternsFromNegatedExclusion, getExclusionGlob, convertToGlob } from '../../src/utils'; import { WorkspaceConfiguration } from 'vscode'; import { listJdks } from '../../src/jdkUtils'; import { platform } from 'os'; @@ -77,7 +77,7 @@ suite('Utils Test', () => { assert.deepEqual(result, ["**/node_modules/test/**"]); }); - test('getExclusionBlob() - no negated exclusions', async function () { + test('getExclusionGlob() - no negated exclusions', async function () { await config.update(IMPORT_EXCLUSION, [ "**/node_modules/**", "**/.metadata/**", @@ -85,12 +85,12 @@ suite('Utils Test', () => { "**/META-INF/maven/**" ]); - const result: string = getExclusionBlob(); + const result: string = getExclusionGlob(); assert.equal(result, "{**/node_modules/**,**/.metadata/**,**/archetype-resources/**,**/META-INF/maven/**}"); }); - test('getExclusionBlob() - has negated exclusions', async function () { + test('getExclusionGlob() - has negated exclusions', async function () { await config.update(IMPORT_EXCLUSION, [ "**/node_modules/**", "!**/node_modules/test/**", @@ -99,7 +99,7 @@ suite('Utils Test', () => { "**/META-INF/maven/**" ]); - const result: string = getExclusionBlob(); + const result: string = getExclusionGlob(); assert.equal(result, "{**/node_modules/**,**/.metadata/**,**/archetype-resources/**,**/META-INF/maven/**}"); }); From 64edcaf0296a1d03c8ef10e3b547a839496c3941 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Mon, 13 Nov 2023 13:17:59 +0800 Subject: [PATCH 2/4] Address comments Signed-off-by: Sheng Chen --- package.json | 4 +- schemas/package.schema.json | 9 ++- src/apiManager.ts | 6 ++ src/buildFilesSelector.ts | 132 +++++++++++++++++++------------ src/extension.ts | 1 + src/languageStatusItemFactory.ts | 7 +- 6 files changed, 100 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index ec1fc6b9e..b4f6c485c 100644 --- a/package.json +++ b/package.json @@ -75,11 +75,11 @@ "javaBuildTools": [ { "displayName": "Maven", - "fileSearchPattern": "**/pom.xml" + "buildFileNames": ["pom.xml"] }, { "displayName": "Gradle", - "fileSearchPattern": "**/{build,settings}.gradle*" + "buildFileNames": ["build.gradle", "settings.gradle", "build.gradle.kts", "settings.gradle.kts"] } ], "semanticTokenTypes": [ diff --git a/schemas/package.schema.json b/schemas/package.schema.json index 183026fcb..6b24ea87a 100644 --- a/schemas/package.schema.json +++ b/schemas/package.schema.json @@ -32,9 +32,12 @@ "description": "The display name of the build file type.", "type": "string" }, - "fileSearchPattern": { - "description": "The glob pattern used to search the build files.", - "type": "string" + "buildFileNames": { + "description": "The build file names that supported by the build tool.", + "type": "array", + "items": { + "type": "string" + } } } } diff --git a/src/apiManager.ts b/src/apiManager.ts index 03eebfda3..b4f155c39 100644 --- a/src/apiManager.ts +++ b/src/apiManager.ts @@ -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 { @@ -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("import.projectSelection") === "manual") { + serverMode = ServerMode.lightWeight; + } const getDocumentSymbols: GetDocumentSymbolsCommand = getDocumentSymbolsProvider(); const goToDefinition: GoToDefinitionCommand = goToDefinitionProvider(); diff --git a/src/buildFilesSelector.ts b/src/buildFilesSelector.ts index 0d2abdf90..8ab5d87c0 100644 --- a/src/buildFilesSelector.ts +++ b/src/buildFilesSelector.ts @@ -1,5 +1,5 @@ import { ExtensionContext, MessageItem, QuickPickItem, QuickPickItemKind, Uri, WorkspaceFolder, extensions, window, workspace } from "vscode"; -import { getExclusionGlob as getExclusionGlobPattern } from "./utils"; +import { convertToGlob, getExclusionGlob as getExclusionGlobPattern, getInclusionPatternsFromNegatedExclusion } from "./utils"; import * as path from "path"; export const PICKED_BUILD_FILES = "java.pickedBuildFiles"; @@ -7,6 +7,11 @@ 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; @@ -26,18 +31,34 @@ export class BuildFileSelector { 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 { - for (const buildType of this.buildTypes) { - const uris: Uri[] = await workspace.findFiles(buildType.fileSearchPattern, this.exclusionGlobPattern, 1); + 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; } @@ -61,7 +82,7 @@ export class BuildFileSelector { } private isValidBuildTypeConfiguration(buildType: IBuildTool): boolean { - return !!buildType.displayName && !!buildType.fileSearchPattern; + return !!buildType.displayName && !!buildType.buildFileNames; } private async chooseBuildFilePickers(): Promise { @@ -80,26 +101,31 @@ export class BuildFileSelector { */ private async getBuildFilePickers(): Promise { const addedFolders: Map = new Map(); - 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([[buildType, uri]]), - picked: true, - }); + 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 */)); + } + for (const uri of uris) { + const buildType = this.buildTypes.find(buildType => buildType.buildFileNames.includes(path.basename(uri.fsPath))); + if (!buildType) { + continue; + } + 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([[buildType, uri]]), + picked: true, + }); } } const pickers: IBuildFilePicker[] = Array.from(addedFolders.values()); @@ -153,39 +179,45 @@ export class BuildFileSelector { if (!choice) { return []; } - const conflictPickers = new Set(); + const conflictBuildTypeAndUris = new Map(); const result: string[] = []; for (const picker of choice) { if (picker.buildTypeAndUri.size > 1) { - conflictPickers.add(picker); + 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 (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( - `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()); - } + 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( + "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; @@ -194,11 +226,11 @@ export class BuildFileSelector { interface IBuildTool { displayName: string; - fileSearchPattern: string; + buildFileNames: string[]; } interface IConflictItem extends MessageItem { - uri?: Uri; + uris?: Uri[]; } interface IBuildFilePicker extends QuickPickItem { diff --git a/src/extension.ts b/src/extension.ts index 5286b6c54..818b4cb7c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -482,6 +482,7 @@ async function startStandardServer(context: ExtensionContext, requirements: requ } if (buildFiles.length === 0) { // cancelled by user + commands.executeCommand('setContext', 'java:serverMode', ServerMode.lightWeight); serverStatusBarProvider.showNotImportedStatus(); return; } diff --git a/src/languageStatusItemFactory.ts b/src/languageStatusItemFactory.ts index 19aa99b3e..e882ee8c9 100644 --- a/src/languageStatusItemFactory.ts +++ b/src/languageStatusItemFactory.ts @@ -40,10 +40,10 @@ export namespace StatusCommands { }; export const startStandardServerCommand = { - title: "Load Projects", + title: "Select Projects...", command: Commands.SWITCH_SERVER_MODE, arguments: ['Standard', true], - tooltip: "Load Projects" + tooltip: "Select Projects..." }; } @@ -66,8 +66,7 @@ export namespace ServerStatusItemFactory { export function showNotImportedStatus(item: any): void { item.severity = vscode.LanguageStatusSeverity?.Warning; - item.text = StatusIcon.notImported; - item.detail = "No projects are Imported"; + item.text = "No projects are imported"; item.command = StatusCommands.startStandardServerCommand; } From a14fd7ebe196990b36a5611527f65bd8563db694 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 21 Nov 2023 11:18:58 +0800 Subject: [PATCH 3/4] Change the order of 'java.import.projectSelection' Signed-off-by: Sheng Chen --- package.json | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index b4f6c485c..ddd68693d 100644 --- a/package.json +++ b/package.json @@ -356,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" @@ -368,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", @@ -407,20 +422,6 @@ "default": false, "scope": "window" }, - "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" - }, "java.project.importOnFirstTimeStartup": { "type": "string", "enum": [ From 81ff12c1b4cd624a49929b46ecabd36c91905c19 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 21 Nov 2023 14:35:38 +0800 Subject: [PATCH 4/4] Address comments Signed-off-by: Sheng Chen --- src/buildFilesSelector.ts | 48 ++++++++++++++++++++++++++------------- src/settings.ts | 4 ++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/buildFilesSelector.ts b/src/buildFilesSelector.ts index 8ab5d87c0..88d1a38cc 100644 --- a/src/buildFilesSelector.ts +++ b/src/buildFilesSelector.ts @@ -100,34 +100,50 @@ export class BuildFileSelector { * Get pickers for all build files in the workspace. */ private async getBuildFilePickers(): Promise { - const addedFolders: Map = new Map(); 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(); for (const uri of uris) { const buildType = this.buildTypes.find(buildType => buildType.buildFileNames.includes(path.basename(uri.fsPath))); if (!buildType) { continue; } - 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); + 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 = new Map(); + 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([[buildType, uri]]), + picked: true, + }); } - } else { - addedFolders.set(containingFolder, { - label: path.basename(containingFolder), - detail: `./${workspace.asRelativePath(uri)}`, - description: buildType.displayName, - buildTypeAndUri: new Map([[buildType, uri]]), - picked: true, - }); } } + const pickers: IBuildFilePicker[] = Array.from(addedFolders.values()); return this.addSeparator(pickers); } diff --git a/src/settings.ts b/src/settings.ts index c601ff030..49db09f0b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -384,10 +384,10 @@ export async function getImportMode(context: ExtensionContext, selector: BuildFi const answer: string = await window.showInformationMessage( "Java build files are detected in the workspace. How do you want to import them?", { modal: true }, - "Import All", "Let Me Select"); + "Import All", "Let Me Select..."); if (answer === "Import All") { return ImportMode.automatic; - } else if (answer === "Let Me Select") { + } else if (answer === "Let Me Select...") { return ImportMode.manual; }