|
| 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