From a0a94748f1c320820b6bf56040a62cd283f02f44 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Mon, 7 Sep 2020 20:06:47 +0800 Subject: [PATCH] Contribute Run and Debug menus to Project Explorer --- package.json | 28 +++++++ src/configurationProvider.ts | 125 ++---------------------------- src/extension.ts | 78 ++++++++++++++++--- src/mainClassPicker.ts | 146 +++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 129 deletions(-) create mode 100644 src/mainClassPicker.ts diff --git a/package.json b/package.json index e5fdfa69..3dc7b114 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,14 @@ "title": "Debug", "icon": "$(debug-alt-small)" }, + { + "command": "java.debug.runFromProjectView", + "title": "Run" + }, + { + "command": "java.debug.debugFromProjectView", + "title": "Debug" + }, { "command": "java.debug.continueAll", "title": "Continue All" @@ -86,6 +94,18 @@ } ], "menus": { + "view/item/context": [ + { + "command": "java.debug.runFromProjectView", + "when": "view == javaProjectExplorer && viewItem =~ /java:project(?=.*?\\b\\+java\\b)(?=.*?\\b\\+uri\\b)/", + "group": "debugger@1" + }, + { + "command": "java.debug.debugFromProjectView", + "when": "view == javaProjectExplorer && viewItem =~ /java:project(?=.*?\\b\\+java\\b)(?=.*?\\b\\+uri\\b)/", + "group": "debugger@2" + } + ], "explorer/context": [ { "command": "java.debug.runJavaFile", @@ -175,6 +195,14 @@ { "command": "java.debug.pauseOthers", "when": "false" + }, + { + "command": "java.debug.runFromProjectView", + "when": "false" + }, + { + "command": "java.debug.debugFromProjectView", + "when": "false" } ] }, diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index 47a04337..be2bb878 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -14,6 +14,7 @@ import * as commands from "./commands"; import * as lsPlugin from "./languageServerPlugin"; import { addMoreHelpfulVMArgs, detectLaunchCommandStyle, validateRuntime } from "./launchCommand"; import { logger, Type } from "./logger"; +import { mainClassPicker } from "./mainClassPicker"; import { resolveJavaProcess } from "./processPicker"; import * as utility from "./utility"; @@ -26,7 +27,6 @@ const platformName = platformNameMappings[process.platform]; export class JavaDebugConfigurationProvider implements vscode.DebugConfigurationProvider { private isUserSettingsDirty: boolean = true; - private debugHistory: MostRecentlyUsedHistory = new MostRecentlyUsedHistory(); constructor() { vscode.workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration("java.debug")) { @@ -331,11 +331,8 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration const currentFile = config.mainClass || _.get(vscode.window.activeTextEditor, "document.uri.fsPath"); if (currentFile) { const mainEntries = await lsPlugin.resolveMainMethod(vscode.Uri.file(currentFile)); - if (mainEntries.length === 1) { - return mainEntries[0]; - } else if (mainEntries.length > 1) { - return this.showMainClassQuickPick(this.formatMainClassOptions(mainEntries), - `Please select a main class you want to run.`); + if (mainEntries.length) { + return mainClassPicker.showQuickPick(mainEntries, "Please select a main class you want to run."); } } @@ -384,9 +381,8 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration anchor: anchor.FAILED_TO_RESOLVE_CLASSPATH, }, "Fix"); if (answer === "Fix") { - const pickItems: IMainClassQuickPickItem[] = this.formatMainClassOptions(validationResponse.proposals); - const selectedFix: lsPlugin.IMainClassOption = - await this.showMainClassQuickPick(pickItems, "Please select main class.", false); + const selectedFix = await mainClassPicker.showQuickPick(validationResponse.proposals, + "Please select main class.", false); if (selectedFix) { sendInfo(null, { fix: "yes", @@ -445,92 +441,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration }); } - const pickItems: IMainClassQuickPickItem[] = this.formatRecentlyUsedMainClassOptions(res); - const selected = await this.showMainClassQuickPick(pickItems, hintMessage || "Select main class"); - if (selected) { - this.debugHistory.updateMRUTimestamp(selected); - } - - return selected; - } - - private async showMainClassQuickPick(pickItems: IMainClassQuickPickItem[], quickPickHintMessage: string, autoPick: boolean = true): - Promise { - // return undefined when the user cancels QuickPick by pressing ESC. - const selected = (pickItems.length === 1 && autoPick) ? - pickItems[0] : await vscode.window.showQuickPick(pickItems, { placeHolder: quickPickHintMessage }); - - return selected && selected.item; - } - - private formatRecentlyUsedMainClassOptions(options: lsPlugin.IMainClassOption[]): IMainClassQuickPickItem[] { - // Sort the Main Class options with the recently used timestamp. - options.sort((a: lsPlugin.IMainClassOption, b: lsPlugin.IMainClassOption) => { - return this.debugHistory.getMRUTimestamp(b) - this.debugHistory.getMRUTimestamp(a); - }); - - const mostRecentlyUsedOption: lsPlugin.IMainClassOption = (options.length && this.debugHistory.contains(options[0])) ? options[0] : undefined; - const isMostRecentlyUsed = (option: lsPlugin.IMainClassOption): boolean => { - return mostRecentlyUsedOption - && mostRecentlyUsedOption.mainClass === option.mainClass - && mostRecentlyUsedOption.projectName === option.projectName; - }; - const isFromActiveEditor = (option: lsPlugin.IMainClassOption): boolean => { - const activeEditor: vscode.TextEditor = vscode.window.activeTextEditor; - const currentActiveFile: string = _.get(activeEditor, "document.uri.fsPath"); - return option.filePath && currentActiveFile && path.relative(option.filePath, currentActiveFile) === ""; - }; - const isPrivileged = (option: lsPlugin.IMainClassOption): boolean => { - return isMostRecentlyUsed(option) || isFromActiveEditor(option); - }; - - // Show the most recently used Main Class as the first one, - // then the Main Class from Active Editor as second, - // finally other Main Class. - const adjustedOptions: lsPlugin.IMainClassOption[] = []; - options.forEach((option: lsPlugin.IMainClassOption) => { - if (isPrivileged(option)) { - adjustedOptions.push(option); - } - }); - options.forEach((option: lsPlugin.IMainClassOption) => { - if (!isPrivileged(option)) { - adjustedOptions.push(option); - } - }); - - const pickItems: IMainClassQuickPickItem[] = this.formatMainClassOptions(adjustedOptions); - pickItems.forEach((pickItem: IMainClassQuickPickItem) => { - const adjustedDetail = []; - if (isMostRecentlyUsed(pickItem.item)) { - adjustedDetail.push("$(clock) recently used"); - } - - if (isFromActiveEditor(pickItem.item)) { - adjustedDetail.push(`$(file-text) active editor (${path.basename(pickItem.item.filePath)})`); - } - - pickItem.detail = adjustedDetail.join(", "); - }); - - return pickItems; - } - - private formatMainClassOptions(options: lsPlugin.IMainClassOption[]): IMainClassQuickPickItem[] { - return options.map((item) => { - let label = item.mainClass; - const description = item.filePath ? path.basename(item.filePath) : ""; - if (item.projectName) { - label += `<${item.projectName}>`; - } - - return { - label, - description, - detail: null, - item, - }; - }); + return mainClassPicker.showQuickPickWithRecentlyUsed(res, hintMessage || "Select main class"); } } @@ -595,27 +506,3 @@ function convertLogLevel(commonLogLevel: string) { return "FINE"; } } - -export interface IMainClassQuickPickItem extends vscode.QuickPickItem { - item: lsPlugin.IMainClassOption; -} - -class MostRecentlyUsedHistory { - private cache: { [key: string]: number } = {}; - - public getMRUTimestamp(mainClassOption: lsPlugin.IMainClassOption): number { - return this.cache[this.getKey(mainClassOption)] || 0; - } - - public updateMRUTimestamp(mainClassOption: lsPlugin.IMainClassOption): void { - this.cache[this.getKey(mainClassOption)] = Date.now(); - } - - public contains(mainClassOption: lsPlugin.IMainClassOption): boolean { - return Boolean(this.cache[this.getKey(mainClassOption)]); - } - - private getKey(mainClassOption: lsPlugin.IMainClassOption): string { - return mainClassOption.mainClass + "|" + mainClassOption.projectName; - } -} diff --git a/src/extension.ts b/src/extension.ts index 0f7f8b7d..4f9df99b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,7 @@ import * as _ from "lodash"; import * as path from "path"; import * as vscode from "vscode"; import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, - instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper"; + instrumentOperationAsVsCodeCommand, setUserError } from "vscode-extension-telemetry-wrapper"; import * as commands from "./commands"; import { JavaDebugConfigurationProvider } from "./configurationProvider"; import { HCR_EVENT, JAVA_LANGID, USER_NOTIFICATION_EVENT } from "./constants"; @@ -14,8 +14,9 @@ import { initializeCodeLensProvider, startDebugging } from "./debugCodeLensProvi import { handleHotCodeReplaceCustomEvent, initializeHotCodeReplace, NO_BUTTON, YES_BUTTON } from "./hotCodeReplace"; import { JavaDebugAdapterDescriptorFactory } from "./javaDebugAdapterDescriptorFactory"; import { logJavaException, logJavaInfo } from "./javaLogger"; -import { IMainMethod, resolveMainMethod } from "./languageServerPlugin"; +import { IMainClassOption, IMainMethod, resolveMainClass, resolveMainMethod } from "./languageServerPlugin"; import { logger, Type } from "./logger"; +import { mainClassPicker } from "./mainClassPicker"; import { pickJavaProcess } from "./processPicker"; import { initializeThreadOperations } from "./threadOperations"; import * as utility from "./utility"; @@ -60,6 +61,12 @@ function initializeExtension(operationId: string, context: vscode.ExtensionConte context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.debugJavaFile", async (uri: vscode.Uri) => { await runJavaFile(uri, false); })); + context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.runFromProjectView", async (node: any) => { + await runJavaProject(node, true); + })); + context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.debugFromProjectView", async (node: any) => { + await runJavaProject(node, false); + })); initializeHotCodeReplace(context); initializeCodeLensProvider(context); initializeThreadOperations(context); @@ -261,17 +268,68 @@ async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { return; } - const projectName = mainMethods[0].projectName; - let mainClass = mainMethods[0].mainClass; - if (mainMethods.length > 1) { - mainClass = await vscode.window.showQuickPick(mainMethods.map((mainMethod) => mainMethod.mainClass), { - placeHolder: "Select the main class to launch.", - }); + const pick = await mainClassPicker.showQuickPick(mainMethods, "Select the main class to run.", (option) => option.mainClass); + if (!pick) { + return; + } + + await startDebugging(pick.mainClass, pick.projectName, uri, noDebug); +} + +async function runJavaProject(node: any, noDebug: boolean) { + if (!node || !node.name || !node.uri) { + vscode.window.showErrorMessage(`Failed to ${noDebug ? "run" : "debug"} the project because of invalid project node. ` + + "This command only applies to Project Explorer view."); + const error = new Error(`Failed to ${noDebug ? "run" : "debug"} the project because of invalid project node.`); + setUserError(error); + throw error; + } + + let mainClassesOptions: IMainClassOption[] = []; + try { + mainClassesOptions = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + }, + async (p) => { + p.report({ + message: "Searching main class...", + }); + return resolveMainClass(vscode.Uri.parse(node.uri)); + }); + } catch (ex) { + vscode.window.showErrorMessage(String((ex && ex.message) || ex)); + throw ex; } - if (!mainClass) { + if (!mainClassesOptions || !mainClassesOptions.length) { + vscode.window.showErrorMessage(`Failed to ${noDebug ? "run" : "debug"} this project '${node._nodeData.displayName || node.name}' ` + + "because it does not contain any main class."); return; } - await startDebugging(mainClass, projectName, uri, noDebug); + const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainClassesOptions, + "Select the main class to run.", (option) => option.mainClass); + if (!pick) { + return; + } + + const projectName: string = pick.projectName; + const mainClass: string = pick.mainClass; + const filePath: string = pick.filePath; + const workspaceFolder: vscode.WorkspaceFolder = filePath ? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)) : undefined; + const launchConfigurations: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("launch", workspaceFolder); + const existingConfigs: vscode.DebugConfiguration[] = launchConfigurations.configurations; + const existConfig: vscode.DebugConfiguration = _.find(existingConfigs, (config) => { + return config.mainClass === mainClass && _.toString(config.projectName) === _.toString(projectName); + }); + const debugConfig = existConfig || { + type: "java", + name: `Launch - ${mainClass.substr(mainClass.lastIndexOf(".") + 1)}`, + request: "launch", + mainClass, + projectName, + }; + debugConfig.noDebug = noDebug; + vscode.debug.startDebugging(workspaceFolder, debugConfig); } diff --git a/src/mainClassPicker.ts b/src/mainClassPicker.ts new file mode 100644 index 00000000..43a3deaf --- /dev/null +++ b/src/mainClassPicker.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as _ from "lodash"; +import * as path from "path"; +import { TextEditor, window } from "vscode"; +import { IMainClassOption } from "./languageServerPlugin"; + +const defaultLabelFormatter = (option: IMainClassOption) => { + return option.mainClass + `${option.projectName ? "<" + option.projectName + ">" : ""}`; +}; +type Formatter = (option: IMainClassOption) => string; + +class MainClassPicker { + private cache: { [key: string]: number } = {}; + + // tslint:disable-next-line + public async showQuickPick(options: IMainClassOption[], placeHolder: string): Promise; + // tslint:disable-next-line + public async showQuickPick(options: IMainClassOption[], placeHolder: string, labelFormatter: Formatter): Promise; + // tslint:disable-next-line + public async showQuickPick(options: IMainClassOption[], placeHolder: string, autoPick: boolean): Promise; + // tslint:disable-next-line + public async showQuickPick(options: IMainClassOption[], placeHolder: string, parameter3?: Formatter | boolean, + parameter4?: boolean): Promise { + let labelFormatter: Formatter = defaultLabelFormatter; + let autoPick: boolean = true; + if (typeof parameter3 === "function") { + labelFormatter = parameter3; + } else if (typeof parameter3 === "boolean") { + autoPick = parameter3; + } + if (typeof parameter4 === "boolean") { + autoPick = parameter4; + } + + if (!options || !options.length) { + return; + } else if (autoPick && options.length === 1) { + return options[0]; + } + + const pickItems = options.map((option) => { + return { + label: labelFormatter(option), + description: option.filePath ? path.basename(option.filePath) : "", + detail: undefined, + data: option, + }; + }); + + const selected = await window.showQuickPick(pickItems, { placeHolder }); + if (selected) { + return selected.data; + } + } + + // Record the main class picking history in a most recently used cache. + public async showQuickPickWithRecentlyUsed(options: IMainClassOption[], placeHolder: string, + labelFormatter?: (option: IMainClassOption) => string): Promise { + if (!options || !options.length) { + return; + } else if (options.length === 1) { + return options[0]; + } + + // Sort the Main Class options with the recently used timestamp. + options.sort((a: IMainClassOption, b: IMainClassOption) => { + return this.getMRUTimestamp(b) - this.getMRUTimestamp(a); + }); + + const mostRecentlyUsedOption: IMainClassOption = (options.length && this.contains(options[0])) ? options[0] : undefined; + const isMostRecentlyUsed = (option: IMainClassOption): boolean => { + return mostRecentlyUsedOption + && mostRecentlyUsedOption.mainClass === option.mainClass + && mostRecentlyUsedOption.projectName === option.projectName; + }; + const isFromActiveEditor = (option: IMainClassOption): boolean => { + const activeEditor: TextEditor = window.activeTextEditor; + const currentActiveFile: string = _.get(activeEditor, "document.uri.fsPath"); + return option.filePath && currentActiveFile && path.relative(option.filePath, currentActiveFile) === ""; + }; + const isPrivileged = (option: IMainClassOption): boolean => { + return isMostRecentlyUsed(option) || isFromActiveEditor(option); + }; + + // Show the most recently used Main Class as the first one, + // then the Main Class from Active Editor as second, + // finally other Main Class. + const adjustedOptions: IMainClassOption[] = []; + options.forEach((option: IMainClassOption) => { + if (isPrivileged(option)) { + adjustedOptions.push(option); + } + }); + options.forEach((option: IMainClassOption) => { + if (!isPrivileged(option)) { + adjustedOptions.push(option); + } + }); + + const pickItems = adjustedOptions.map((option) => { + const adjustedDetail = []; + if (isMostRecentlyUsed(option)) { + adjustedDetail.push("$(clock) recently used"); + } + + if (isFromActiveEditor(option)) { + adjustedDetail.push(`$(file-text) active editor (${path.basename(option.filePath)})`); + } + + const detail: string = adjustedDetail.join(", "); + + return { + label: labelFormatter ? labelFormatter(option) : defaultLabelFormatter(option), + description: option.filePath ? path.basename(option.filePath) : "", + detail, + data: option, + }; + }); + + const selected = await window.showQuickPick(pickItems, { placeHolder }); + if (selected) { + this.updateMRUTimestamp(selected.data); + return selected.data; + } + } + + private getMRUTimestamp(mainClassOption: IMainClassOption): number { + return this.cache[this.getKey(mainClassOption)] || 0; + } + + private updateMRUTimestamp(mainClassOption: IMainClassOption): void { + this.cache[this.getKey(mainClassOption)] = Date.now(); + } + + private contains(mainClassOption: IMainClassOption): boolean { + return Boolean(this.cache[this.getKey(mainClassOption)]); + } + + private getKey(mainClassOption: IMainClassOption): string { + return mainClassOption.mainClass + "|" + mainClassOption.projectName; + } +} + +export const mainClassPicker: MainClassPicker = new MainClassPicker();