From 03ae5ce69fea24053d2f025d7895f989c88cbf61 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Tue, 8 Dec 2020 17:57:13 +0800 Subject: [PATCH 1/8] Use a progress reporter to hint the current build status when run or debug a program --- README.md | 1 + package-lock.json | 36 ++++-- package.json | 12 +- package.nls.json | 3 +- package.nls.zh.json | 3 +- src/build.ts | 18 ++- src/configurationProvider.ts | 130 ++++++++++++++-------- src/debugCodeLensProvider.ts | 5 +- src/extension.ts | 210 ++++++++++++++++++++--------------- src/processTree.ts | 16 +-- src/progressAPI.ts | 60 ++++++++++ src/progressImpl.ts | 136 +++++++++++++++++++++++ src/utility.ts | 87 ++++++++++++--- tslint.json | 2 +- 14 files changed, 539 insertions(+), 180 deletions(-) create mode 100644 src/progressAPI.ts create mode 100644 src/progressImpl.ts diff --git a/README.md b/README.md index f1ebd50e..39bcdf83 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Please also check the documentation of [Language Support for Java by Red Hat](ht - `java.debug.settings.jdwp.limitOfVariablesPerJdwpRequest`: The maximum number of variables or fields that can be requested in one JDWP request. The higher the value, the less frequently debuggee will be requested when expanding the variable view. Also a large number can cause JDWP request timeout. Defaults to 100. - `java.debug.settings.jdwp.requestTimeout`: The timeout (ms) of JDWP request when the debugger communicates with the target JVM. Defaults to 3000. - `java.debug.settings.vmArgs`: The default VM arguments to launch the Java program. Eg. Use '-Xmx1G -ea' to increase the heap size to 1GB and enable assertions. If you want to customize the VM arguments for a specific debug session, please modify the 'vmArgs' config in launch.json. +- `java.debug.settings.showRunStatusAsNotification`: Show the build status as the progress notification every time you run or debug a program. Defaults to `true`. Pro Tip: The documentation [Configuration.md](https://github.com/microsoft/vscode-java-debug/blob/master/Configuration.md) provides lots of samples to demonstrate how to use these debug configurations, recommend to take a look. diff --git a/package-lock.json b/package-lock.json index e335d0fb..886f30bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,9 +59,15 @@ "dev": true }, "@types/node": { - "version": "8.10.51", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.51.tgz", - "integrity": "sha512-cArrlJp3Yv6IyFT/DYe+rlO8o3SIHraALbBW/+CcCYW/a9QucpLI+n2p4sRxAvl2O35TiecpX2heSZtJjvEO+Q==", + "version": "14.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.10.tgz", + "integrity": "sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ==", + "dev": true + }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", "dev": true }, "@types/vscode": { @@ -1135,6 +1141,11 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -5797,9 +5808,9 @@ "dev": true }, "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz", + "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==", "dev": true }, "unc-path-regex": { @@ -5973,9 +5984,9 @@ "dev": true }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" }, "v8-compile-cache": { "version": "2.0.3", @@ -6083,6 +6094,13 @@ "requires": { "uuid": "^3.4.0", "vscode-extension-telemetry": "^0.1.6" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "vscode-test": { diff --git a/package.json b/package.json index aa001ad3..a21a1ccb 100644 --- a/package.json +++ b/package.json @@ -741,6 +741,11 @@ "type": "string", "description": "%java.debugger.configuration.vmArgs.description%", "default": "" + }, + "java.debug.settings.showRunStatusAsNotification": { + "type": "boolean", + "description": "%java.debugger.configuration.showRunStatusAsNotification%", + "default": true } } } @@ -756,7 +761,8 @@ "@types/glob": "^7.1.3", "@types/lodash": "^4.14.137", "@types/mocha": "^5.2.7", - "@types/node": "^8.10.51", + "@types/node": "^14.14.10", + "@types/uuid": "^8.3.0", "@types/vscode": "1.49.0", "cross-env": "^5.2.0", "gulp": "^4.0.2", @@ -765,13 +771,15 @@ "shelljs": "^0.8.3", "ts-loader": "^5.4.5", "tslint": "^5.18.0", - "typescript": "^3.5.3", + "typescript": "^4.1.2", "vscode-test": "^1.2.0", "webpack": "^4.39.2", "webpack-cli": "^3.3.7" }, "dependencies": { + "compare-versions": "^3.6.0", "lodash": "^4.17.19", + "uuid": "^8.3.1", "vscode-extension-telemetry": "^0.1.6", "vscode-extension-telemetry-wrapper": "^0.8.0" } diff --git a/package.nls.json b/package.nls.json index e4088a6b..6ae0d005 100644 --- a/package.nls.json +++ b/package.nls.json @@ -58,5 +58,6 @@ "java.debugger.configuration.exceptionBreakpoint.skipClasses": "Skip the specified classes when breaking on exception. You could use the built-in variables such as '$JDK' and '$Libraries' to skip a group of classes, or add a specific class name expression, e.g. java.*, *.Foo", "java.debugger.configuration.jdwp.limitOfVariablesPerJdwpRequest.description": "The maximum number of variables or fields that can be requested in one JDWP request. The higher the value, the less frequently debuggee will be requested when expanding the variable view. Also a large number can cause JDWP request timeout.", "java.debugger.configuration.jdwp.requestTimeout.description": "The timeout (ms) of JDWP request when the debugger communicates with the target JVM.", - "java.debugger.configuration.vmArgs.description": "The default VM arguments to launch the Java program. Eg. Use '-Xmx1G -ea' to increase the heap size to 1GB and enable assertions. If you want to customize the VM arguments for a specific debug session, please modify the 'vmArgs' config in launch.json." + "java.debugger.configuration.vmArgs.description": "The default VM arguments to launch the Java program. Eg. Use '-Xmx1G -ea' to increase the heap size to 1GB and enable assertions. If you want to customize the VM arguments for a specific debug session, please modify the 'vmArgs' config in launch.json.", + "java.debugger.configuration.showRunStatusAsNotification": "Show the build status as the progress notification every time you run or debug a program." } diff --git a/package.nls.zh.json b/package.nls.zh.json index 2a88b28e..89b75818 100644 --- a/package.nls.zh.json +++ b/package.nls.zh.json @@ -56,5 +56,6 @@ "java.debugger.configuration.exceptionBreakpoint.skipClasses": "当发生异常时,跳过指定的类。你可以使用内置变量,如'$JDK'和'$Libraries'来跳过一组类,或者添加一个特定的类名表达式,如java.*,*.Foo。", "java.debugger.configuration.jdwp.limitOfVariablesPerJdwpRequest.description": "一次JDWP请求中可以请求的变量或字段的最大数量。该值越高,在展开变量视图时,请求debuggee的频率就越低。同时数量过大也会导致JDWP请求超时。", "java.debugger.configuration.jdwp.requestTimeout.description": "调试器与目标JVM通信时JDWP请求的超时时间(ms)。", - "java.debugger.configuration.vmArgs.description": "启动Java程序的默认VM参数。例如,使用'-Xmx1G -ea'将堆大小增加到1GB并启用断言。如果要为特定的调试会话定制VM参数,请修改launch.json中的'vmArgs'配置。" + "java.debugger.configuration.vmArgs.description": "启动Java程序的默认VM参数。例如,使用'-Xmx1G -ea'将堆大小增加到1GB并启用断言。如果要为特定的调试会话定制VM参数,请修改launch.json中的'vmArgs'配置。", + "java.debugger.configuration.showRunStatusAsNotification": "每次运行或调试程序时,将构建状态显示为进度通知。" } \ No newline at end of file diff --git a/src/build.ts b/src/build.ts index 93560e9f..48f1959a 100644 --- a/src/build.ts +++ b/src/build.ts @@ -7,16 +7,24 @@ import { instrumentOperation, sendInfo, sendOperationError, setErrorCode } from import * as anchor from "./anchor"; import * as commands from "./commands"; import * as lsPlugin from "./languageServerPlugin"; +import { IProgressReporter } from "./progressAPI"; import * as utility from "./utility"; const JAVA_DEBUG_CONFIGURATION = "java.debug.settings"; const ON_BUILD_FAILURE_PROCEED = "onBuildFailureProceed"; -export async function buildWorkspace(): Promise { +enum CompileWorkspaceStatus { + FAILED = 0, + SUCCEED = 1, + WITHERROR = 2, + CANCELLED = 3, +} + +export async function buildWorkspace(progressReporter: IProgressReporter): Promise { const buildResult = await instrumentOperation("build", async (operationId: string) => { let error; try { - await commands.executeJavaExtensionCommand(commands.JAVA_BUILD_WORKSPACE, false); + await commands.executeJavaExtensionCommand(commands.JAVA_BUILD_WORKSPACE, false, progressReporter.getCancellationToken()); } catch (err) { error = err; } @@ -27,11 +35,11 @@ export async function buildWorkspace(): Promise { }; })(); - if (buildResult.error) { + if (buildResult.error === CompileWorkspaceStatus.CANCELLED) { + return false; + } else { return handleBuildFailure(buildResult.operationId, buildResult.error); } - - return true; } async function handleBuildFailure(operationId: string, err: any): Promise { diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index 9505bfb4..1bc0e198 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -16,6 +16,7 @@ import { addMoreHelpfulVMArgs, detectLaunchCommandStyle, validateRuntime } from import { logger, Type } from "./logger"; import { mainClassPicker } from "./mainClassPicker"; import { resolveJavaProcess } from "./processPicker"; +import { progressReporterManager } from "./progressImpl"; import * as utility from "./utility"; const platformNameMappings: {[key: string]: string} = { @@ -42,10 +43,10 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } // Returns an initial debug configurations based on contextual information. - public provideDebugConfigurations(folder: vscode.WorkspaceFolder | undefined, _token?: vscode.CancellationToken): + public provideDebugConfigurations(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken): vscode.ProviderResult { const provideDebugConfigurationsHandler = instrumentOperation("provideDebugConfigurations", (_operationId: string) => { - return >this.provideDebugConfigurationsAsync(folder); + return >this.provideDebugConfigurationsAsync(folder, token); }); return provideDebugConfigurationsHandler(); } @@ -68,13 +69,13 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration public resolveDebugConfigurationWithSubstitutedVariables( folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, - _token?: vscode.CancellationToken): vscode.ProviderResult { + token?: vscode.CancellationToken): vscode.ProviderResult { const resolveDebugConfigurationHandler = instrumentOperation("resolveDebugConfiguration", (_operationId: string) => { try { // See https://github.com/microsoft/vscode-java-debug/issues/778 // Merge the platform specific properties to the global config to simplify the subsequent resolving logic. this.mergePlatformProperties(config, folder); - return this.resolveAndValidateDebugConfiguration(folder, config); + return this.resolveAndValidateDebugConfiguration(folder, config, token); } catch (ex) { utility.showErrorMessage({ type: Type.EXCEPTION, @@ -86,44 +87,56 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration return resolveDebugConfigurationHandler(); } - private provideDebugConfigurationsAsync(folder: vscode.WorkspaceFolder | undefined, _token?: vscode.CancellationToken) { - return vscode.window.withProgress({ location: vscode.ProgressLocation.Window }, (p) => { - return new Promise(async (resolve, _reject) => { - p.report({ message: "Auto generating configuration..." }); - const defaultLaunchConfig = { - type: "java", - name: "Debug (Launch) - Current File", - request: "launch", - // tslint:disable-next-line - mainClass: "${file}", - }; - try { - const isOnStandardMode = await utility.waitForStandardMode(); - if (!isOnStandardMode) { - resolve([defaultLaunchConfig]); - return ; - } + private provideDebugConfigurationsAsync(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken) { + return new Promise(async (resolve, _reject) => { + const progressReporter = progressReporterManager.create("Create launch.json", true); + progressReporter.observe(token); + const defaultLaunchConfig = { + type: "java", + name: "Debug (Launch) - Current File", + request: "launch", + // tslint:disable-next-line + mainClass: "${file}", + }; + try { + const isOnStandardMode = await utility.waitForStandardMode(progressReporter); + if (!isOnStandardMode) { + resolve([defaultLaunchConfig]); + return ; + } - const mainClasses = await lsPlugin.resolveMainClass(folder ? folder.uri : undefined); - let cache: {[key: string]: any}; - cache = {}; - const launchConfigs = mainClasses.map((item) => { - return { - ...defaultLaunchConfig, - name: this.constructLaunchConfigName(item.mainClass, cache, item.projectName), - mainClass: item.mainClass, - projectName: item.projectName, - }; - }); - resolve([defaultLaunchConfig, ...launchConfigs]); - } catch (ex) { - if (ex instanceof utility.JavaExtensionNotEnabledError) { - utility.guideToInstallJavaExtension(); - } - p.report({ message: `failed to generate configuration. ${ex}` }); - resolve(defaultLaunchConfig); + if (progressReporter.isCancelled()) { + resolve([defaultLaunchConfig]); + return; } - }); + progressReporter.report("Resolve Java Configs", "Auto generating Java configuration..."); + const mainClasses = await lsPlugin.resolveMainClass(folder ? folder.uri : undefined); + const cache = {}; + const launchConfigs = mainClasses.map((item) => { + return { + ...defaultLaunchConfig, + name: this.constructLaunchConfigName(item.mainClass, cache, item.projectName), + mainClass: item.mainClass, + projectName: item.projectName, + }; + }); + if (progressReporter.isCancelled()) { + resolve([defaultLaunchConfig]); + return; + } + resolve([defaultLaunchConfig, ...launchConfigs]); + } catch (ex) { + if (ex instanceof utility.JavaExtensionNotEnabledError) { + utility.guideToInstallJavaExtension(); + } else { + // tslint:disable-next-line + console.error(ex); + } + + resolve([defaultLaunchConfig]); + } finally { + progressReporter.cancel(); + } }); } @@ -155,10 +168,21 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } } - private async resolveAndValidateDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration) { + private async resolveAndValidateDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, + token?: vscode.CancellationToken) { + let progressReporter = progressReporterManager.get(config.__progressId); + if (!progressReporter && config.__progressId) { + return undefined; + } + progressReporter = progressReporter || progressReporterManager.create(config.noDebug ? "Run" : "Debug"); + progressReporter.observe(token); + if (progressReporter.isCancelled()) { + return undefined; + } + try { - const isOnStandardMode = await utility.waitForStandardMode(); - if (!isOnStandardMode) { + const isOnStandardMode = await utility.waitForStandardMode(progressReporter); + if (!isOnStandardMode || progressReporter.isCancelled()) { return undefined; } @@ -191,12 +215,17 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } if (needsBuildWorkspace()) { - const proceed = await buildWorkspace(); + progressReporter.report("Compile", "Compiling Java workspace..."); + const proceed = await buildWorkspace(progressReporter); if (!proceed) { return undefined; } } + if (progressReporter.isCancelled()) { + return undefined; + } + progressReporter.report("Resolve Configuration", "Resolving launch configuration..."); const mainClassOption = await this.resolveLaunchConfig(folder ? folder.uri : undefined, config); if (!mainClassOption || !mainClassOption.mainClass) { // Exit silently if the user cancels the prompt fix by ESC. // Exit the debug session. @@ -206,6 +235,9 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration config.mainClass = mainClassOption.mainClass; config.projectName = mainClassOption.projectName; + if (progressReporter.isCancelled()) { + return undefined; + } if (_.isEmpty(config.classPaths) && _.isEmpty(config.modulePaths)) { const result = (await lsPlugin.resolveClasspath(config.mainClass, config.projectName)); config.modulePaths = result[0]; @@ -229,6 +261,9 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration config.vmArgs = this.concatArgs(config.vmArgs); } + if (progressReporter.isCancelled()) { + return undefined; + } // Populate the class filters to the debug configuration. await populateStepFilters(config); @@ -293,6 +328,11 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration }); } + if (token?.isCancellationRequested || progressReporter.isCancelled()) { + return undefined; + } + + delete config.__progressId; return config; } catch (ex) { if (ex instanceof utility.JavaExtensionNotEnabledError) { @@ -306,6 +346,8 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration utility.showErrorMessageWithTroubleshooting(utility.convertErrorToMessage(ex)); return undefined; + } finally { + progressReporter.cancel(); } } diff --git a/src/debugCodeLensProvider.ts b/src/debugCodeLensProvider.ts index 6821fe09..13b81f38 100644 --- a/src/debugCodeLensProvider.ts +++ b/src/debugCodeLensProvider.ts @@ -9,6 +9,7 @@ import { instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-w import { JAVA_LANGID } from "./constants"; import { initializeHoverProvider } from "./hoverProvider"; import { IMainMethod, isOnClasspath, resolveMainMethod } from "./languageServerPlugin"; +import { IProgressReporter } from "./progressAPI"; import { getJavaExtensionAPI, isJavaExtEnabled, ServerMode } from "./utility"; const JAVA_RUN_CODELENS_COMMAND = "java.debug.runCodeLens"; @@ -184,7 +185,8 @@ async function launchJsonExists(workspace?: vscode.Uri): Promise { return !!results.find((launchJson) => vscode.workspace.getWorkspaceFolder(launchJson) === workspaceFolder); } -export async function startDebugging(mainClass: string, projectName: string, uri: vscode.Uri, noDebug: boolean): Promise { +export async function startDebugging(mainClass: string, projectName: string, uri: vscode.Uri, noDebug: boolean, + progressReporter?: IProgressReporter): Promise { const workspaceFolder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(uri); const workspaceUri: vscode.Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; const onClasspath = await isOnClasspath(uri.toString()); @@ -195,6 +197,7 @@ export async function startDebugging(mainClass: string, projectName: string, uri const debugConfig: vscode.DebugConfiguration = await constructDebugConfig(mainClass, projectName, workspaceUri); debugConfig.projectName = projectName; debugConfig.noDebug = noDebug; + debugConfig.__progressId = progressReporter?.getId(); return vscode.debug.startDebugging(workspaceFolder, debugConfig); } diff --git a/src/extension.ts b/src/extension.ts index eab2847e..19ca2ee1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +import * as compareVersions from "compare-versions"; import * as _ from "lodash"; import * as path from "path"; import * as vscode from "vscode"; @@ -18,21 +19,24 @@ import { IMainClassOption, IMainMethod, resolveMainMethod } from "./languageServ import { logger, Type } from "./logger"; import { mainClassPicker } from "./mainClassPicker"; import { pickJavaProcess } from "./processPicker"; +import { IProgressReporter } from "./progressAPI"; +import { progressReporterManager, registerProgressReporters } from "./progressImpl"; import { JavaTerminalLinkProvder } from "./terminalLinkProvider"; import { initializeThreadOperations } from "./threadOperations"; import * as utility from "./utility"; -export async function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext): Promise { await initializeFromJsonFile(context.asAbsolutePath("./package.json"), { firstParty: true, }); - await instrumentOperation("activation", initializeExtension)(context); + return instrumentOperation("activation", initializeExtension)(context); } -function initializeExtension(_operationId: string, context: vscode.ExtensionContext) { +function initializeExtension(_operationId: string, context: vscode.ExtensionContext): any { // Deprecated logger.initialize(context, true); + registerProgressReporters(context); registerDebugEventListener(context); context.subscriptions.push(logger); context.subscriptions.push(vscode.window.registerTerminalLinkProvider(new JavaTerminalLinkProvder())); @@ -72,6 +76,10 @@ function initializeExtension(_operationId: string, context: vscode.ExtensionCont initializeHotCodeReplace(context); initializeCodeLensProvider(context); initializeThreadOperations(context); + + return { + progressReporterManager, + }; } // this method is called when your extension is deactivated @@ -225,69 +233,63 @@ async function applyHCR(hcrStatusBar: NotificationBar) { } async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { - const alreadyActivated: boolean = utility.isJavaExtActivated(); + const progressReporter = progressReporterManager.create(noDebug ? "Run" : "Debug"); try { // Wait for Java Language Support extension being on Standard mode. - const isOnStandardMode = await utility.waitForStandardMode(); + const isOnStandardMode = await utility.waitForStandardMode(progressReporter); if (!isOnStandardMode) { - return; + throw new utility.OperationCancelledError(""); + } + + const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!uri && activeEditor && _.endsWith(path.basename(activeEditor.document.fileName), ".java")) { + uri = activeEditor.document.uri; + } + + if (!uri) { + vscode.window.showErrorMessage(`${noDebug ? "Run" : "Debug"} failed. Please open a Java file with main method first.`); + throw new utility.OperationCancelledError(""); + } + + const mainMethods: IMainMethod[] = await resolveMainMethod(uri); + const hasMainMethods: boolean = mainMethods.length > 0; + const canRunTests: boolean = await canDelegateToJavaTestRunner(uri); + const defaultPlaceHolder: string = "Select the main class to run"; + + if (!hasMainMethods && !canRunTests) { + progressReporter.report("Resolve mainClass", "Resolving main class..."); + const mainClasses: IMainClassOption[] = await utility.searchMainMethods(); + const placeHolder: string = `The file '${path.basename(uri.fsPath)}' is not executable, please select a main class you want to run.`; + await launchMain(mainClasses, uri, noDebug, progressReporter, placeHolder, false /*autoPick*/); + } else if (hasMainMethods && !canRunTests) { + await launchMain(mainMethods, uri, noDebug, progressReporter, defaultPlaceHolder); + } else if (!hasMainMethods && canRunTests) { + launchTesting(uri, noDebug, progressReporter); + } else { + const launchMainChoice: string = "main() method"; + const launchTestChoice: string = "unit tests"; + const choice: string | undefined = await vscode.window.showQuickPick( + [launchMainChoice, launchTestChoice], + { placeHolder: "Please select which kind of task you would like to launch" }, + ); + if (choice === launchMainChoice) { + await launchMain(mainMethods, uri, noDebug, progressReporter, defaultPlaceHolder); + } else if (choice === launchTestChoice) { + launchTesting(uri, noDebug, progressReporter); + } } } catch (ex) { - if (ex instanceof utility.JavaExtensionNotEnabledError) { - utility.guideToInstallJavaExtension(); + progressReporter.cancel(); + if (ex instanceof utility.OperationCancelledError) { return; } - if (alreadyActivated) { - vscode.window.showErrorMessage(String((ex && ex.message) || ex)); + if (ex instanceof utility.JavaExtensionNotEnabledError) { + utility.guideToInstallJavaExtension(); return; } - throw ex; - } - - const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!uri && activeEditor && _.endsWith(path.basename(activeEditor.document.fileName), ".java")) { - uri = activeEditor.document.uri; - } - - if (!uri) { - vscode.window.showErrorMessage(`${noDebug ? "Run" : "Debug"} failed. Please open a Java file with main method first.`); - return; - } - - let mainMethods: IMainMethod[] = []; - try { - mainMethods = await resolveMainMethod(uri); - } catch (ex) { vscode.window.showErrorMessage(String((ex && ex.message) || ex)); - throw ex; - } - - const hasMainMethods: boolean = mainMethods.length > 0; - const canRunTests: boolean = await canDelegateToJavaTestRunner(uri); - const defaultPlaceHolder: string = "Select the main class to run"; - - if (!hasMainMethods && !canRunTests) { - const mainClasses: IMainClassOption[] = await utility.searchMainMethods(); - const placeHolder: string = `The file '${path.basename(uri.fsPath)}' is not executable, please select a main class you want to run.`; - await launchMain(mainClasses, uri, noDebug, placeHolder, false /*autoPick*/); - } else if (hasMainMethods && !canRunTests) { - await launchMain(mainMethods, uri, noDebug, defaultPlaceHolder); - } else if (!hasMainMethods && canRunTests) { - await launchTesting(uri, noDebug); - } else { - const launchMainChoice: string = "main() method"; - const launchTestChoice: string = "unit tests"; - const choice: string | undefined = await vscode.window.showQuickPick( - [launchMainChoice, launchTestChoice], - { placeHolder: "Please select which kind of task you would like to launch" }, - ); - if (choice === launchMainChoice) { - await launchMain(mainMethods, uri, noDebug, defaultPlaceHolder); - } else if (choice === launchTestChoice) { - await launchTesting(uri, noDebug); - } } } @@ -300,24 +302,34 @@ async function canDelegateToJavaTestRunner(uri: vscode.Uri): Promise { return (await vscode.commands.getCommands()).includes("java.test.editor.run"); } -async function launchTesting(uri: vscode.Uri, noDebug: boolean): Promise { - noDebug ? vscode.commands.executeCommand("java.test.editor.run", uri) : vscode.commands.executeCommand("java.test.editor.debug", uri); +function launchTesting(uri: vscode.Uri, noDebug: boolean, progressReporter: IProgressReporter) { + const command: string = noDebug ? "java.test.editor.run" : "java.test.editor.debug"; + vscode.commands.executeCommand(command, uri, progressReporter); + if (compareVersions(getTestExtensionVersion(), "0.26.1") <= 0) { + throw new utility.OperationCancelledError(""); + } } -async function launchMain(mainMethods: IMainClassOption[], uri: vscode.Uri, noDebug: boolean, placeHolder: string, - autoPick: boolean = true): Promise { +function getTestExtensionVersion(): string { + const extension: vscode.Extension | undefined = vscode.extensions.getExtension("vscjava.vscode-java-test"); + return extension?.packageJSON.version || "0.0.0"; +} + +async function launchMain(mainMethods: IMainClassOption[], uri: vscode.Uri, noDebug: boolean, progressReporter: IProgressReporter, + placeHolder: string, autoPick: boolean = true): Promise { if (!mainMethods || !mainMethods.length) { vscode.window.showErrorMessage( "Error: Main method not found in the file, please define the main method as: public static void main(String[] args)"); - return; + throw new utility.OperationCancelledError(""); } + progressReporter.report("Select mainClass", "Selecting the main class to run..."); const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainMethods, placeHolder, autoPick); if (!pick) { - return; + throw new utility.OperationCancelledError(""); } - await startDebugging(pick.mainClass, pick.projectName || "", uri, noDebug); + startDebugging(pick.mainClass, pick.projectName || "", uri, noDebug, progressReporter); } async function runJavaProject(node: any, noDebug: boolean) { @@ -329,35 +341,53 @@ async function runJavaProject(node: any, noDebug: boolean) { throw error; } - const mainClassesOptions: IMainClassOption[] = await utility.searchMainMethods(vscode.Uri.parse(node.uri)); - 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; - } + const progressReporter = progressReporterManager.create(noDebug ? "Run" : "Debug"); + try { + progressReporter.report("Resolve mainClass", "Resolving main class..."); + const mainClassesOptions: IMainClassOption[] = await utility.searchMainMethods(vscode.Uri.parse(node.uri)); + if (progressReporter.isCancelled()) { + throw new utility.OperationCancelledError(""); + } - const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainClassesOptions, - "Select the main class to run."); - if (!pick) { - return; - } + 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."); + throw new utility.OperationCancelledError(""); + } - const projectName: string | undefined = pick.projectName; - const mainClass: string = pick.mainClass; - const filePath: string | undefined = pick.filePath; - const workspaceFolder: vscode.WorkspaceFolder | undefined = 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 | undefined = _.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); + progressReporter.report("Select mainClass", "Selecting the main class to run..."); + const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainClassesOptions, + "Select the main class to run."); + if (!pick || progressReporter.isCancelled()) { + throw new utility.OperationCancelledError(""); + } + + const projectName: string | undefined = pick.projectName; + const mainClass: string = pick.mainClass; + const filePath: string | undefined = pick.filePath; + const workspaceFolder: vscode.WorkspaceFolder | undefined = + 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 | undefined = _.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; + debugConfig.__progressId = progressReporter.getId(); + vscode.debug.startDebugging(workspaceFolder, debugConfig); + } catch (ex) { + progressReporter.cancel(); + if (ex instanceof utility.OperationCancelledError) { + return; + } + + throw ex; + } } diff --git a/src/processTree.ts b/src/processTree.ts index a82c0619..a8f50f99 100644 --- a/src/processTree.ts +++ b/src/processTree.ts @@ -77,8 +77,8 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a const wmic = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'wbem', 'WMIC.exe'); proc = spawn(wmic, [ 'process', 'get', 'CommandLine,CreationDate,ParentProcessId,ProcessId' ]); - proc.stdout.setEncoding('utf8'); - proc.stdout.on('data', lines(line => { + proc.stdout?.setEncoding('utf8'); + proc.stdout?.on('data', lines(line => { let matches = CMD_PAT.exec(line.trim()); if (matches && matches.length === 5) { const pid = Number(matches[4]); @@ -110,8 +110,8 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a } else if (process.platform === 'darwin') { // OS X proc = spawn('/bin/ps', [ '-x', '-o', `pid,ppid,comm=${'a'.repeat(256)},command` ]); - proc.stdout.setEncoding('utf8'); - proc.stdout.on('data', lines(line => { + proc.stdout?.setEncoding('utf8'); + proc.stdout?.on('data', lines(line => { const pid = Number(line.substr(0, 5)); const ppid = Number(line.substr(6, 5)); @@ -126,8 +126,8 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a } else { // linux proc = spawn('/bin/ps', [ '-ax', '-o', 'pid,ppid,comm:20,command' ]); - proc.stdout.setEncoding('utf8'); - proc.stdout.on('data', lines(line => { + proc.stdout?.setEncoding('utf8'); + proc.stdout?.on('data', lines(line => { const pid = Number(line.substr(0, 5)); const ppid = Number(line.substr(6, 5)); @@ -157,8 +157,8 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a reject(err); }); - proc.stderr.setEncoding('utf8'); - proc.stderr.on('data', data => { + proc.stderr?.setEncoding('utf8'); + proc.stderr?.on('data', data => { const e = data.toString(); if (e.indexOf('screen size is bogus') >= 0) { // ignore this error silently; see https://github.com/microsoft/vscode/issues/75932 diff --git a/src/progressAPI.ts b/src/progressAPI.ts new file mode 100644 index 00000000..fb36f659 --- /dev/null +++ b/src/progressAPI.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { CancellationToken } from "vscode"; + +export interface IProgressReporter { + /** + * Returns the id of the progress reporter. + */ + getId(): string; + + /** + * Update the progress message. + * @param subTaskName the sub task name to update + * @param detailedMessage the detailed message to update + */ + report(subTaskName: string, detailedMessage: string): void; + + /** + * Cancel the progress reporter. + */ + cancel(): void; + + /** + * Is the progress reporter cancelled. + */ + isCancelled(): boolean; + + /** + * The CancellationToken to monitor if the progress reporter has been cancelled by the user. + */ + getCancellationToken(): CancellationToken; + + /** + * Dispose the progress reporter if the observed token has been cancelled. + * @param token the cancellation token to observe + */ + observe(token?: CancellationToken): void; +} + +export interface IProgressReporterManager { + /** + * Create a progress reporter. + * @param jobName the job name + * @param showInStatusBar whether to show the progress in the status bar + */ + create(jobName: string, showInStatusBar?: boolean): IProgressReporter; + + /** + * Return the progress repoter with the progress id. + * @param progressId the progress id + */ + get(progressId: string): IProgressReporter | undefined; + + /** + * Remove the progress reporter from the progress reporter manager. + * @param progressReporter the progress reporter to remove + */ + remove(progressReporter: IProgressReporter): void; +} diff --git a/src/progressImpl.ts b/src/progressImpl.ts new file mode 100644 index 00000000..61b3ed70 --- /dev/null +++ b/src/progressImpl.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { v4 } from "uuid"; +import { CancellationToken, CancellationTokenSource, commands, Disposable, EventEmitter, ExtensionContext, ProgressLocation, + StatusBarAlignment, StatusBarItem, window, workspace } from "vscode"; +import { IProgressReporter, IProgressReporterManager } from "./progressAPI"; + +const showProgressNotificationCommand = "_java.debug.showProgressNotification"; +export function registerProgressReporters(context: ExtensionContext) { + context.subscriptions.push(commands.registerCommand(showProgressNotificationCommand, async (progressReporter: JavaProgressReporter) => { + progressReporter.showProgressNotification(); + })); +} + +class JavaProgressReporter implements IProgressReporter { + public jobName: string; + public subTaskName: string; + public detailedMessage: string = "Building Java workspace..."; + + private _id: string = v4(); + private _tokenSource = new CancellationTokenSource(); + private _statusBarItem: StatusBarItem | undefined; + private _isProgressNotificationRunning: boolean = false; + private _cancelEventEmitter: EventEmitter; + private _progressEventEmitter: EventEmitter; + private _disposables: Disposable[] = []; + + constructor(jobName: string, showInStatusBar?: boolean) { + this.jobName = jobName || "Java Job Status"; + const config = workspace.getConfiguration("java.debug.settings"); + if (showInStatusBar || !config.showRunStatusAsNotification) { + this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 1); + this._statusBarItem.text = "$(sync~spin) Building..."; + this._statusBarItem.command = { + title: "Check Java Build Status", + command: showProgressNotificationCommand, + arguments: [ this ], + }; + this._statusBarItem.tooltip = "Check Java Build Status"; + this._disposables.push(this._statusBarItem); + } + + this._cancelEventEmitter = new EventEmitter(); + this._progressEventEmitter = new EventEmitter(); + this._disposables.push(this._cancelEventEmitter); + this._disposables.push(this._progressEventEmitter); + this._disposables.push(this._tokenSource); + } + + public getId(): string { + return this._id; + } + + public report(subTaskName: string, detailedMessage: string): void { + this.subTaskName = subTaskName; + this.detailedMessage = detailedMessage || subTaskName || "Building..."; + this._progressEventEmitter.fire(undefined); + if (this._statusBarItem) { + this._statusBarItem.text = subTaskName ? `$(sync~spin) Building - ${subTaskName}...` : "$(sync~spin) Building..."; + this._statusBarItem.show(); + } else { + this.showProgressNotification(); + } + } + + public cancel() { + this._tokenSource.cancel(); + this._cancelEventEmitter.fire(undefined); + this._statusBarItem?.hide(); + this._disposables.forEach((disposable) => disposable.dispose()); + progressReporterManager.remove(this); + } + + public isCancelled(): boolean { + return this.getCancellationToken().isCancellationRequested; + } + + public getCancellationToken(): CancellationToken { + return this._tokenSource.token; + } + + public observe(token?: CancellationToken): void { + token?.onCancellationRequested(() => { + this.cancel(); + }); + } + + public showProgressNotification() { + if (this._isProgressNotificationRunning) { + return; + } + + this._isProgressNotificationRunning = true; + window.withProgress({ + location: ProgressLocation.Notification, + title: `[${this.jobName}](command:java.show.server.task.status)`, + cancellable: true, + }, (progress, token) => { + progress.report({ + message: this.detailedMessage, + }); + this._progressEventEmitter.event(() => { + progress.report({ + message: this.detailedMessage, + }); + }); + this.observe(token); + return new Promise((resolve) => { + this._cancelEventEmitter.event(() => { + resolve(true); + }); + }); + }); + } +} + +class JavaProgressReporterManager implements IProgressReporterManager { + private store: { [key: string]: IProgressReporter } = {}; + + public create(jobName: string, showInStatusBar?: boolean): IProgressReporter { + const progressReporter = new JavaProgressReporter(jobName, showInStatusBar); + this.store[progressReporter.getId()] = progressReporter; + return progressReporter; + } + + public get(progressId: string): IProgressReporter | undefined { + return this.store[progressId]; + } + + public remove(progressReporter: IProgressReporter) { + delete this.store[progressReporter.getId()]; + } +} + +export const progressReporterManager: IProgressReporterManager = new JavaProgressReporterManager(); diff --git a/src/utility.ts b/src/utility.ts index 6746c57a..524fa1e9 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -6,6 +6,7 @@ import * as vscode from "vscode"; import { sendError, sendInfo, setUserError } from "vscode-extension-telemetry-wrapper"; import { IMainClassOption, resolveMainClass } from "./languageServerPlugin"; import { logger, Type } from "./logger"; +import { IProgressReporter } from "./progressAPI"; const TROUBLESHOOTING_LINK = "https://github.com/Microsoft/vscode-java-debug/blob/master/Troubleshooting.md"; const LEARN_MORE = "Learn More"; @@ -29,6 +30,12 @@ export class JavaExtensionNotEnabledError extends Error { } } +export class OperationCancelledError extends Error { + constructor(message: string) { + super(message); + } +} + interface ILoggingMessage { type?: Type; message: string; @@ -174,13 +181,19 @@ export async function getJavaHome(): Promise { return ""; } -export function getJavaExtensionAPI(): Thenable { +export function getJavaExtensionAPI(progressReporter?: IProgressReporter): Thenable { const extension = vscode.extensions.getExtension(JAVA_EXTENSION_ID); if (!extension) { throw new JavaExtensionNotEnabledError("VS Code Java Extension is not enabled."); } - return extension.activate(); + return new Promise(async (resolve) => { + progressReporter?.getCancellationToken().onCancellationRequested(() => { + resolve(undefined); + }); + + resolve(await extension.activate()); + }); } export function getJavaExtension(): vscode.Extension | undefined { @@ -212,33 +225,46 @@ export enum ServerMode { * Wait for Java Language Support extension being on Standard mode, * and return true if the final status is on Standard mode. */ -export async function waitForStandardMode(): Promise { - const api = await getJavaExtensionAPI(); +export async function waitForStandardMode(progressReporter: IProgressReporter): Promise { + if (await isImportingProjects()) { + progressReporter.report("Import", "Importing Java projects..."); + } + + const api = await getJavaExtensionAPI(progressReporter); + if (!api) { + return false; + } + if (api && api.serverMode === ServerMode.LIGHTWEIGHT) { const answer = await vscode.window.showInformationMessage("Run/Debug feature requires Java language server to run in Standard mode. " + "Do you want to switch it to Standard mode now?", "Yes", "Cancel"); if (answer === "Yes") { - return vscode.window.withProgress({ location: vscode.ProgressLocation.Window }, async (progress) => { - if (api.serverMode === ServerMode.STANDARD) { - return true; - } - - progress.report({ message: "Switching to Standard mode..." }); - return new Promise((resolve) => { - api.onDidServerModeChange((mode: string) => { - if (mode === ServerMode.STANDARD) { - resolve(true); - } - }); - - vscode.commands.executeCommand("java.server.mode.switch", ServerMode.STANDARD, true); + if (api.serverMode === ServerMode.STANDARD) { + return true; + } + + progressReporter?.report("Import", "Importing Java projects..."); + return new Promise((resolve) => { + progressReporter.getCancellationToken().onCancellationRequested(() => { + resolve(false); }); + api.onDidServerModeChange((mode: string) => { + if (mode === ServerMode.STANDARD) { + resolve(true); + } + }); + + vscode.commands.executeCommand("java.server.mode.switch", ServerMode.STANDARD, true); }); } return false; } else if (api && api.serverMode === ServerMode.HYBRID) { + progressReporter.report("Import", "Importing Java projects..."); return new Promise((resolve) => { + progressReporter.getCancellationToken().onCancellationRequested(() => { + resolve(false); + }); api.onDidServerModeChange((mode: string) => { if (mode === ServerMode.STANDARD) { resolve(true); @@ -251,6 +277,10 @@ export async function waitForStandardMode(): Promise { } export async function searchMainMethods(uri?: vscode.Uri): Promise { + return resolveMainClass(uri); +} + +export async function searchMainMethodsWithProgress(uri?: vscode.Uri): Promise { try { return await vscode.window.withProgress( { location: vscode.ProgressLocation.Window }, @@ -263,3 +293,24 @@ export async function searchMainMethods(uri?: vscode.Uri): Promise { + const extension = vscode.extensions.getExtension(JAVA_EXTENSION_ID); + if (!extension) { + return false; + } + + const serverMode = getJavaServerMode(); + if (serverMode === ServerMode.STANDARD || serverMode === ServerMode.HYBRID) { + const allCommands = await vscode.commands.getCommands(); + return (!extension.isActive && allCommands.includes("java.show.server.task.status")) + || (extension.isActive && extension.exports?.serverMode === ServerMode.HYBRID); + } + + return false; +} + +function getJavaServerMode(): ServerMode { + return vscode.workspace.getConfiguration().get("java.server.launchMode") + || ServerMode.HYBRID; +} diff --git a/tslint.json b/tslint.json index ac9dc0e3..b51ab794 100644 --- a/tslint.json +++ b/tslint.json @@ -9,7 +9,7 @@ "no-duplicate-variable": true, "max-classes-per-file": false, "no-implicit-dependencies": [ - false, // Turned off due to: https://github.com/microsoft/vscode/issues/78019 + false, "dev" ], "no-empty": false, From 3937ed1ff7ad7772822a514a8ab5671214b3c174 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Wed, 9 Dec 2020 10:48:30 +0800 Subject: [PATCH 2/8] Align build and configuration error messages with progress reporter --- src/build.ts | 7 +++++-- src/configurationProvider.ts | 28 ++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/build.ts b/src/build.ts index 48f1959a..800de348 100644 --- a/src/build.ts +++ b/src/build.ts @@ -38,11 +38,11 @@ export async function buildWorkspace(progressReporter: IProgressReporter): Promi if (buildResult.error === CompileWorkspaceStatus.CANCELLED) { return false; } else { - return handleBuildFailure(buildResult.operationId, buildResult.error); + return handleBuildFailure(buildResult.operationId, buildResult.error, progressReporter); } } -async function handleBuildFailure(operationId: string, err: any): Promise { +async function handleBuildFailure(operationId: string, err: any, progressReporter: IProgressReporter): Promise { const configuration = vscode.workspace.getConfiguration(JAVA_DEBUG_CONFIGURATION); const onBuildFailureProceed = configuration.get(ON_BUILD_FAILURE_PROCEED); @@ -61,6 +61,9 @@ async function handleBuildFailure(operationId: string, err: any): Promise key !== "noDebug").length === 0; } - private async resolveLaunchConfig(folder: vscode.Uri | undefined, - config: vscode.DebugConfiguration): Promise { + private async resolveAndValidateMainClass(folder: vscode.Uri | undefined, config: vscode.DebugConfiguration, + progressReporter: IProgressReporter): Promise { if (!config.mainClass || this.isFile(config.mainClass)) { 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) { + progressReporter.report("Select mainClass", "Selecting the main class to run..."); return mainClassPicker.showQuickPick(mainEntries, "Please select a main class you want to run."); } } @@ -389,13 +396,13 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration const hintMessage = currentFile ? `The file '${path.basename(currentFile)}' is not executable, please select a main class you want to run.` : "Please select a main class you want to run."; - return this.promptMainClass(folder, hintMessage); + return this.promptMainClass(folder, progressReporter, hintMessage); } const containsExternalClasspaths = !_.isEmpty(config.classPaths) || !_.isEmpty(config.modulePaths); const validationResponse = await lsPlugin.validateLaunchConfig(config.mainClass, config.projectName, containsExternalClasspaths, folder); if (!validationResponse.mainClass.isValid || !validationResponse.projectName.isValid) { - return this.fixMainClass(folder, config, validationResponse); + return this.fixMainClass(folder, config, validationResponse, progressReporter); } return { @@ -414,7 +421,8 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } private async fixMainClass(folder: vscode.Uri | undefined, config: vscode.DebugConfiguration, - validationResponse: lsPlugin.ILaunchValidationResponse): Promise { + validationResponse: lsPlugin.ILaunchValidationResponse, progressReporter: IProgressReporter): + Promise { const errors: string[] = []; if (!validationResponse.mainClass.isValid) { errors.push(String(validationResponse.mainClass.message)); @@ -425,12 +433,14 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } if (validationResponse.proposals && validationResponse.proposals.length) { + progressReporter.report("Confirm Config Error", "Config error, please select the next action..."); const answer = await utility.showErrorMessageWithTroubleshooting({ message: errors.join(os.EOL), type: Type.USAGEERROR, anchor: anchor.FAILED_TO_RESOLVE_CLASSPATH, }, "Fix"); if (answer === "Fix") { + progressReporter.report("Select mainClass", "Select the main class to run..."); const selectedFix = await mainClassPicker.showQuickPick(validationResponse.proposals, "Please select main class.", false); if (selectedFix) { @@ -480,7 +490,8 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } } - private async promptMainClass(folder: vscode.Uri | undefined, hintMessage?: string): Promise { + private async promptMainClass(folder: vscode.Uri | undefined, progressReporter: IProgressReporter, hintMessage?: string): + Promise { const res = await lsPlugin.resolveMainClass(folder); if (res.length === 0) { const workspaceFolder = folder ? vscode.workspace.getWorkspaceFolder(folder) : undefined; @@ -491,6 +502,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration }); } + progressReporter.report("Select mainClass", "Selecting the main class to run..."); return mainClassPicker.showQuickPickWithRecentlyUsed(res, hintMessage || "Select main class"); } } From 6bee9dde52f6d048eb5eaa23f459bd70265d1176 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Mon, 14 Dec 2020 14:37:55 +0800 Subject: [PATCH 3/8] refactor the progress api based on feedback --- README.md | 2 +- package.json | 6 +- package.nls.json | 2 +- package.nls.zh.json | 2 +- src/build.ts | 9 +-- src/configurationProvider.ts | 19 ++--- src/extension.ts | 15 ++-- src/progressAPI.ts | 54 +++++++++----- src/progressImpl.ts | 141 ++++++++++++++++++++++------------- 9 files changed, 155 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 39bcdf83..58ac8c16 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Please also check the documentation of [Language Support for Java by Red Hat](ht - `java.debug.settings.jdwp.limitOfVariablesPerJdwpRequest`: The maximum number of variables or fields that can be requested in one JDWP request. The higher the value, the less frequently debuggee will be requested when expanding the variable view. Also a large number can cause JDWP request timeout. Defaults to 100. - `java.debug.settings.jdwp.requestTimeout`: The timeout (ms) of JDWP request when the debugger communicates with the target JVM. Defaults to 3000. - `java.debug.settings.vmArgs`: The default VM arguments to launch the Java program. Eg. Use '-Xmx1G -ea' to increase the heap size to 1GB and enable assertions. If you want to customize the VM arguments for a specific debug session, please modify the 'vmArgs' config in launch.json. -- `java.debug.settings.showRunStatusAsNotification`: Show the build status as the progress notification every time you run or debug a program. Defaults to `true`. +- `java.silentNotification`: Control whether the progress notifications can be shown. Defaults to `false`. Pro Tip: The documentation [Configuration.md](https://github.com/microsoft/vscode-java-debug/blob/master/Configuration.md) provides lots of samples to demonstrate how to use these debug configurations, recommend to take a look. diff --git a/package.json b/package.json index a21a1ccb..d9be89df 100644 --- a/package.json +++ b/package.json @@ -742,10 +742,10 @@ "description": "%java.debugger.configuration.vmArgs.description%", "default": "" }, - "java.debug.settings.showRunStatusAsNotification": { + "java.silentNotification": { "type": "boolean", - "description": "%java.debugger.configuration.showRunStatusAsNotification%", - "default": true + "description": "%java.debugger.configuration.silentNotification%", + "default": false } } } diff --git a/package.nls.json b/package.nls.json index 6ae0d005..6b1d97e0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -59,5 +59,5 @@ "java.debugger.configuration.jdwp.limitOfVariablesPerJdwpRequest.description": "The maximum number of variables or fields that can be requested in one JDWP request. The higher the value, the less frequently debuggee will be requested when expanding the variable view. Also a large number can cause JDWP request timeout.", "java.debugger.configuration.jdwp.requestTimeout.description": "The timeout (ms) of JDWP request when the debugger communicates with the target JVM.", "java.debugger.configuration.vmArgs.description": "The default VM arguments to launch the Java program. Eg. Use '-Xmx1G -ea' to increase the heap size to 1GB and enable assertions. If you want to customize the VM arguments for a specific debug session, please modify the 'vmArgs' config in launch.json.", - "java.debugger.configuration.showRunStatusAsNotification": "Show the build status as the progress notification every time you run or debug a program." + "java.debugger.configuration.silentNotification": "Control whether the progress notifications can be shown." } diff --git a/package.nls.zh.json b/package.nls.zh.json index 89b75818..38d761bf 100644 --- a/package.nls.zh.json +++ b/package.nls.zh.json @@ -57,5 +57,5 @@ "java.debugger.configuration.jdwp.limitOfVariablesPerJdwpRequest.description": "一次JDWP请求中可以请求的变量或字段的最大数量。该值越高,在展开变量视图时,请求debuggee的频率就越低。同时数量过大也会导致JDWP请求超时。", "java.debugger.configuration.jdwp.requestTimeout.description": "调试器与目标JVM通信时JDWP请求的超时时间(ms)。", "java.debugger.configuration.vmArgs.description": "启动Java程序的默认VM参数。例如,使用'-Xmx1G -ea'将堆大小增加到1GB并启用断言。如果要为特定的调试会话定制VM参数,请修改launch.json中的'vmArgs'配置。", - "java.debugger.configuration.showRunStatusAsNotification": "每次运行或调试程序时,将构建状态显示为进度通知。" + "java.debugger.configuration.silentNotification": "控制是否可以显示进度通知。" } \ No newline at end of file diff --git a/src/build.ts b/src/build.ts index 800de348..71b8c898 100644 --- a/src/build.ts +++ b/src/build.ts @@ -56,16 +56,13 @@ async function handleBuildFailure(operationId: string, err: any, progressReporte }); setErrorCode(error, Number(err)); sendOperationError(operationId, "build", error); - if (err === lsPlugin.CompileWorkspaceStatus.WITHERROR || err === lsPlugin.CompileWorkspaceStatus.FAILED) { + if (!onBuildFailureProceed && (err === lsPlugin.CompileWorkspaceStatus.WITHERROR || err === lsPlugin.CompileWorkspaceStatus.FAILED)) { if (checkErrorsReportedByJavaExtension()) { vscode.commands.executeCommand("workbench.actions.view.problems"); } - if (!onBuildFailureProceed) { - progressReporter.report("Confirm Build Errors", "Build failed, please select the next action..."); - } - const ans = onBuildFailureProceed ? "Proceed" : (await vscode.window.showErrorMessage("Build failed, do you want to continue?", - "Proceed", "Fix...", "Cancel")); + progressReporter.hide(true); + const ans = await vscode.window.showErrorMessage("Build failed, do you want to continue?", "Proceed", "Fix...", "Cancel"); sendInfo(operationId, { operationName: "build", choiceForBuildError: ans || "esc", diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index b315ba55..2222766a 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -17,7 +17,7 @@ import { logger, Type } from "./logger"; import { mainClassPicker } from "./mainClassPicker"; import { resolveJavaProcess } from "./processPicker"; import { IProgressReporter } from "./progressAPI"; -import { progressReporterManager } from "./progressImpl"; +import { progressProvider } from "./progressImpl"; import * as utility from "./utility"; const platformNameMappings: {[key: string]: string} = { @@ -90,7 +90,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration private provideDebugConfigurationsAsync(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken) { return new Promise(async (resolve, _reject) => { - const progressReporter = progressReporterManager.create("Create launch.json", true); + const progressReporter = progressProvider.createProgressReporter("Create launch.json", vscode.ProgressLocation.Window); progressReporter.observe(token); const defaultLaunchConfig = { type: "java", @@ -171,11 +171,11 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration private async resolveAndValidateDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken) { - let progressReporter = progressReporterManager.get(config.__progressId); + let progressReporter = progressProvider.getProgressReporter(config.__progressId); if (!progressReporter && config.__progressId) { return undefined; } - progressReporter = progressReporter || progressReporterManager.create(config.noDebug ? "Run" : "Debug"); + progressReporter = progressReporter || progressProvider.createProgressReporterForPreLaunchTask(config.noDebug ? "Run" : "Debug"); progressReporter.observe(token); if (progressReporter.isCancelled()) { return undefined; @@ -221,6 +221,8 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration if (!proceed) { return undefined; } + + progressReporter.show(); } if (progressReporter.isCancelled()) { @@ -353,7 +355,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration utility.showErrorMessageWithTroubleshooting(utility.convertErrorToMessage(ex)); return undefined; } finally { - progressReporter.cancel(); + progressReporter.done(); } } @@ -388,7 +390,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration if (currentFile) { const mainEntries = await lsPlugin.resolveMainMethod(vscode.Uri.file(currentFile)); if (mainEntries.length) { - progressReporter.report("Select mainClass", "Selecting the main class to run..."); + progressReporter.hide(true); return mainClassPicker.showQuickPick(mainEntries, "Please select a main class you want to run."); } } @@ -433,14 +435,13 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } if (validationResponse.proposals && validationResponse.proposals.length) { - progressReporter.report("Confirm Config Error", "Config error, please select the next action..."); + progressReporter.hide(true); const answer = await utility.showErrorMessageWithTroubleshooting({ message: errors.join(os.EOL), type: Type.USAGEERROR, anchor: anchor.FAILED_TO_RESOLVE_CLASSPATH, }, "Fix"); if (answer === "Fix") { - progressReporter.report("Select mainClass", "Select the main class to run..."); const selectedFix = await mainClassPicker.showQuickPick(validationResponse.proposals, "Please select main class.", false); if (selectedFix) { @@ -502,7 +503,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration }); } - progressReporter.report("Select mainClass", "Selecting the main class to run..."); + progressReporter.hide(true); return mainClassPicker.showQuickPickWithRecentlyUsed(res, hintMessage || "Select main class"); } } diff --git a/src/extension.ts b/src/extension.ts index 19ca2ee1..71f92b68 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,7 +20,7 @@ import { logger, Type } from "./logger"; import { mainClassPicker } from "./mainClassPicker"; import { pickJavaProcess } from "./processPicker"; import { IProgressReporter } from "./progressAPI"; -import { progressReporterManager, registerProgressReporters } from "./progressImpl"; +import { progressProvider } from "./progressImpl"; import { JavaTerminalLinkProvder } from "./terminalLinkProvider"; import { initializeThreadOperations } from "./threadOperations"; import * as utility from "./utility"; @@ -36,7 +36,6 @@ function initializeExtension(_operationId: string, context: vscode.ExtensionCont // Deprecated logger.initialize(context, true); - registerProgressReporters(context); registerDebugEventListener(context); context.subscriptions.push(logger); context.subscriptions.push(vscode.window.registerTerminalLinkProvider(new JavaTerminalLinkProvder())); @@ -78,7 +77,7 @@ function initializeExtension(_operationId: string, context: vscode.ExtensionCont initializeThreadOperations(context); return { - progressReporterManager, + progressProvider, }; } @@ -233,7 +232,7 @@ async function applyHCR(hcrStatusBar: NotificationBar) { } async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { - const progressReporter = progressReporterManager.create(noDebug ? "Run" : "Debug"); + const progressReporter = progressProvider.createProgressReporterForPreLaunchTask(noDebug ? "Run" : "Debug"); try { // Wait for Java Language Support extension being on Standard mode. const isOnStandardMode = await utility.waitForStandardMode(progressReporter); @@ -323,12 +322,13 @@ async function launchMain(mainMethods: IMainClassOption[], uri: vscode.Uri, noDe throw new utility.OperationCancelledError(""); } - progressReporter.report("Select mainClass", "Selecting the main class to run..."); + progressReporter.hide(true); const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainMethods, placeHolder, autoPick); if (!pick) { throw new utility.OperationCancelledError(""); } + progressReporter.show(); startDebugging(pick.mainClass, pick.projectName || "", uri, noDebug, progressReporter); } @@ -341,7 +341,7 @@ async function runJavaProject(node: any, noDebug: boolean) { throw error; } - const progressReporter = progressReporterManager.create(noDebug ? "Run" : "Debug"); + const progressReporter = progressProvider.createProgressReporterForPreLaunchTask(noDebug ? "Run" : "Debug"); try { progressReporter.report("Resolve mainClass", "Resolving main class..."); const mainClassesOptions: IMainClassOption[] = await utility.searchMainMethods(vscode.Uri.parse(node.uri)); @@ -355,13 +355,14 @@ async function runJavaProject(node: any, noDebug: boolean) { throw new utility.OperationCancelledError(""); } - progressReporter.report("Select mainClass", "Selecting the main class to run..."); + progressReporter.hide(true); const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainClassesOptions, "Select the main class to run."); if (!pick || progressReporter.isCancelled()) { throw new utility.OperationCancelledError(""); } + progressReporter.show(); const projectName: string | undefined = pick.projectName; const mainClass: string = pick.mainClass; const filePath: string | undefined = pick.filePath; diff --git a/src/progressAPI.ts b/src/progressAPI.ts index fb36f659..20b3193e 100644 --- a/src/progressAPI.ts +++ b/src/progressAPI.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { CancellationToken } from "vscode"; +import { CancellationToken, ProgressLocation } from "vscode"; export interface IProgressReporter { /** @@ -10,51 +10,71 @@ export interface IProgressReporter { getId(): string; /** - * Update the progress message. - * @param subTaskName the sub task name to update - * @param detailedMessage the detailed message to update + * Reports a progress message update. + * @param subTaskName the message shown in the status bar + * @param detailedMessage the detailed message shown in the notification */ report(subTaskName: string, detailedMessage: string): void; /** - * Cancel the progress reporter. + * Shows the progress reporter. + */ + show(): void; + + /** + * Hides the progress reporter. + * @param onlyNotifications only hide the progress reporter when it's shown as notification + */ + hide(onlyNotifications?: boolean): void; + + /** + * Cancels the progress reporter. */ cancel(): void; /** - * Is the progress reporter cancelled. + * Returns whether the progress reporter has been cancelled or completed. */ isCancelled(): boolean; + /** + * Notifies the work is done that is either the task is completed or the user has cancelled it. + */ + done(): void; + /** * The CancellationToken to monitor if the progress reporter has been cancelled by the user. */ getCancellationToken(): CancellationToken; /** - * Dispose the progress reporter if the observed token has been cancelled. + * Disposes the progress reporter if the observed token has been cancelled. * @param token the cancellation token to observe */ observe(token?: CancellationToken): void; } -export interface IProgressReporterManager { +export interface IProgressReporterProvider { /** - * Create a progress reporter. + * Creates a progress reporter. * @param jobName the job name - * @param showInStatusBar whether to show the progress in the status bar + * @param progressLocation The location at which progress should show + * @param cancellable Controls if a cancel button should show to allow the user + * to cancel the progress reporter. Note that currently only + * `ProgressLocation.Notification` is supporting to show a cancel + * button. */ - create(jobName: string, showInStatusBar?: boolean): IProgressReporter; + createProgressReporter(jobName: string, progressLocation: ProgressLocation | { viewId: string }, cancellable?: boolean): IProgressReporter; /** - * Return the progress repoter with the progress id. - * @param progressId the progress id + * Creates a progress reporter for the task to run before the debug session starts. + * @param jobName the job name */ - get(progressId: string): IProgressReporter | undefined; + createProgressReporterForPreLaunchTask(jobName: string): IProgressReporter; /** - * Remove the progress reporter from the progress reporter manager. - * @param progressReporter the progress reporter to remove + * Returns the progress reporter with the progress id. + * @param progressId the progress id */ - remove(progressReporter: IProgressReporter): void; + getProgressReporter(progressId: string): IProgressReporter | undefined; } diff --git a/src/progressImpl.ts b/src/progressImpl.ts index 61b3ed70..9fabe4ce 100644 --- a/src/progressImpl.ts +++ b/src/progressImpl.ts @@ -2,48 +2,51 @@ // Licensed under the MIT license. import { v4 } from "uuid"; -import { CancellationToken, CancellationTokenSource, commands, Disposable, EventEmitter, ExtensionContext, ProgressLocation, +import { CancellationToken, CancellationTokenSource, Disposable, EventEmitter, Progress, ProgressLocation, StatusBarAlignment, StatusBarItem, window, workspace } from "vscode"; -import { IProgressReporter, IProgressReporterManager } from "./progressAPI"; +import { IProgressReporter, IProgressReporterProvider } from "./progressAPI"; -const showProgressNotificationCommand = "_java.debug.showProgressNotification"; -export function registerProgressReporters(context: ExtensionContext) { - context.subscriptions.push(commands.registerCommand(showProgressNotificationCommand, async (progressReporter: JavaProgressReporter) => { - progressReporter.showProgressNotification(); - })); -} +const STATUS_COMMAND: string = "java.show.server.task.status"; +class ProgressReporter implements IProgressReporter { + private _id: string = v4(); + private _jobName: string; + private _progressLocation: ProgressLocation | { viewId: string }; + private _cancellable: boolean = false; -class JavaProgressReporter implements IProgressReporter { - public jobName: string; - public subTaskName: string; - public detailedMessage: string = "Building Java workspace..."; + private _subTaskName: string; + private _detailedMessage: string; + private _isShown: boolean; - private _id: string = v4(); private _tokenSource = new CancellationTokenSource(); private _statusBarItem: StatusBarItem | undefined; - private _isProgressNotificationRunning: boolean = false; - private _cancelEventEmitter: EventEmitter; + private _cancelProgressEventEmitter: EventEmitter; private _progressEventEmitter: EventEmitter; private _disposables: Disposable[] = []; - constructor(jobName: string, showInStatusBar?: boolean) { - this.jobName = jobName || "Java Job Status"; - const config = workspace.getConfiguration("java.debug.settings"); - if (showInStatusBar || !config.showRunStatusAsNotification) { + constructor(jobName: string, progressLocation: ProgressLocation | { viewId: string }, cancellable?: boolean) { + this._jobName = jobName; + this._progressLocation = progressLocation || ProgressLocation.Notification; + this._cancellable = !!cancellable; + const config = workspace.getConfiguration("java"); + if (config.silentNotification && this._progressLocation === ProgressLocation.Notification) { + this._progressLocation = ProgressLocation.Window; + } + + if (this._progressLocation === ProgressLocation.Window) { this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 1); - this._statusBarItem.text = "$(sync~spin) Building..."; + this._statusBarItem.text = `$(sync~spin) ${this._jobName}...`; this._statusBarItem.command = { title: "Check Java Build Status", - command: showProgressNotificationCommand, - arguments: [ this ], + command: STATUS_COMMAND, + arguments: [], }; this._statusBarItem.tooltip = "Check Java Build Status"; this._disposables.push(this._statusBarItem); } - this._cancelEventEmitter = new EventEmitter(); + this._cancelProgressEventEmitter = new EventEmitter(); this._progressEventEmitter = new EventEmitter(); - this._disposables.push(this._cancelEventEmitter); + this._disposables.push(this._cancelProgressEventEmitter); this._disposables.push(this._progressEventEmitter); this._disposables.push(this._tokenSource); } @@ -53,29 +56,48 @@ class JavaProgressReporter implements IProgressReporter { } public report(subTaskName: string, detailedMessage: string): void { - this.subTaskName = subTaskName; - this.detailedMessage = detailedMessage || subTaskName || "Building..."; + this._subTaskName = subTaskName; + this._detailedMessage = detailedMessage || subTaskName || this._jobName; this._progressEventEmitter.fire(undefined); if (this._statusBarItem) { - this._statusBarItem.text = subTaskName ? `$(sync~spin) Building - ${subTaskName}...` : "$(sync~spin) Building..."; + this._statusBarItem.text = `$(sync~spin) ${this._subTaskName || this._jobName}...`; + } + + this.show(); + } + + public show(): void { + if (this._statusBarItem) { this._statusBarItem.show(); - } else { - this.showProgressNotification(); + return; + } + + this.showNativeProgress(); + } + + public hide(onlyNotifications?: boolean): void { + if (onlyNotifications && this._progressLocation === ProgressLocation.Notification) { + this._cancelProgressEventEmitter.fire(undefined); + this._isShown = false; } } public cancel() { this._tokenSource.cancel(); - this._cancelEventEmitter.fire(undefined); + this._cancelProgressEventEmitter.fire(undefined); this._statusBarItem?.hide(); this._disposables.forEach((disposable) => disposable.dispose()); - progressReporterManager.remove(this); + ( progressProvider).remove(this); } public isCancelled(): boolean { return this.getCancellationToken().isCancellationRequested; } + public done(): void { + this.cancel(); + } + public getCancellationToken(): CancellationToken { return this._tokenSource.token; } @@ -86,45 +108,64 @@ class JavaProgressReporter implements IProgressReporter { }); } - public showProgressNotification() { - if (this._isProgressNotificationRunning) { + private showNativeProgress() { + if (this._isShown) { return; } - this._isProgressNotificationRunning = true; + this._isShown = true; window.withProgress({ - location: ProgressLocation.Notification, - title: `[${this.jobName}](command:java.show.server.task.status)`, - cancellable: true, + location: this._progressLocation, + title: this._jobName ? `[${this._jobName}](command:${STATUS_COMMAND})` : undefined, + cancellable: this._cancellable, }, (progress, token) => { - progress.report({ - message: this.detailedMessage, - }); + this.reportMessage(progress); + this.observe(token); this._progressEventEmitter.event(() => { - progress.report({ - message: this.detailedMessage, - }); + this.reportMessage(progress); }); - this.observe(token); return new Promise((resolve) => { - this._cancelEventEmitter.event(() => { + this._cancelProgressEventEmitter.event(() => { resolve(true); }); }); }); } + + private reportMessage(progress: Progress<{ message?: string; increment?: number }>): void { + const message: string = this._progressLocation === ProgressLocation.Notification ? this._detailedMessage : this._subTaskName; + progress.report({ + message, + }); + } } -class JavaProgressReporterManager implements IProgressReporterManager { +class PreLaunchTaskProgressReporter extends ProgressReporter implements IProgressReporter { + constructor(jobName: string) { + super(jobName, ProgressLocation.Notification, true); + } + + public report(subTaskName: string, detailedMessage: string): void { + super.report("Building - " + subTaskName, detailedMessage); + } +} + +class ProgressReporterProvider implements IProgressReporterProvider { private store: { [key: string]: IProgressReporter } = {}; - public create(jobName: string, showInStatusBar?: boolean): IProgressReporter { - const progressReporter = new JavaProgressReporter(jobName, showInStatusBar); + public createProgressReporter(jobName: string, progressLocation: ProgressLocation, cancellable?: boolean): IProgressReporter { + const progressReporter = new ProgressReporter(jobName, progressLocation, cancellable); + this.store[progressReporter.getId()] = progressReporter; + return progressReporter; + } + + public createProgressReporterForPreLaunchTask(jobName: string): IProgressReporter { + const progressReporter = new PreLaunchTaskProgressReporter(jobName); this.store[progressReporter.getId()] = progressReporter; return progressReporter; } - public get(progressId: string): IProgressReporter | undefined { + public getProgressReporter(progressId: string): IProgressReporter | undefined { return this.store[progressId]; } @@ -133,4 +174,4 @@ class JavaProgressReporterManager implements IProgressReporterManager { } } -export const progressReporterManager: IProgressReporterManager = new JavaProgressReporterManager(); +export const progressProvider: IProgressReporterProvider = new ProgressReporterProvider(); From f5c9f834cdc68f707430b1d26ea819918ee44048 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Mon, 14 Dec 2020 15:48:23 +0800 Subject: [PATCH 4/8] Hide progress reporter only when necessary --- src/configurationProvider.ts | 10 ++++++---- src/extension.ts | 13 +++++++++---- src/mainClassPicker.ts | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index 2222766a..0781f8d8 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -221,8 +221,6 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration if (!proceed) { return undefined; } - - progressReporter.show(); } if (progressReporter.isCancelled()) { @@ -390,7 +388,9 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration if (currentFile) { const mainEntries = await lsPlugin.resolveMainMethod(vscode.Uri.file(currentFile)); if (mainEntries.length) { - progressReporter.hide(true); + if (!mainClassPicker.isAutoPicked(mainEntries)) { + progressReporter.hide(true); + } return mainClassPicker.showQuickPick(mainEntries, "Please select a main class you want to run."); } } @@ -503,7 +503,9 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration }); } - progressReporter.hide(true); + if (!mainClassPicker.isAutoPicked(res)) { + progressReporter.hide(true); + } return mainClassPicker.showQuickPickWithRecentlyUsed(res, hintMessage || "Select main class"); } } diff --git a/src/extension.ts b/src/extension.ts index 71f92b68..98409b84 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -322,13 +322,16 @@ async function launchMain(mainMethods: IMainClassOption[], uri: vscode.Uri, noDe throw new utility.OperationCancelledError(""); } - progressReporter.hide(true); + if (!mainClassPicker.isAutoPicked(mainMethods, autoPick)) { + progressReporter.hide(true); + } + const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainMethods, placeHolder, autoPick); if (!pick) { throw new utility.OperationCancelledError(""); } - progressReporter.show(); + progressReporter.report("Launch mainClass", "Launching the main class..."); startDebugging(pick.mainClass, pick.projectName || "", uri, noDebug, progressReporter); } @@ -355,14 +358,16 @@ async function runJavaProject(node: any, noDebug: boolean) { throw new utility.OperationCancelledError(""); } - progressReporter.hide(true); + if (!mainClassPicker.isAutoPicked(mainClassesOptions)) { + progressReporter.hide(true); + } const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainClassesOptions, "Select the main class to run."); if (!pick || progressReporter.isCancelled()) { throw new utility.OperationCancelledError(""); } - progressReporter.show(); + progressReporter.report("Launch mainClass", "Launching the main class..."); const projectName: string | undefined = pick.projectName; const mainClass: string = pick.mainClass; const filePath: string | undefined = pick.filePath; diff --git a/src/mainClassPicker.ts b/src/mainClassPicker.ts index caee665d..38bd13be 100644 --- a/src/mainClassPicker.ts +++ b/src/mainClassPicker.ts @@ -153,6 +153,22 @@ class MainClassPicker { return undefined; } + /** + * Checks whether the item options can be picked automatically without popping up the QuickPick box. + * @param options the item options to pick + * @param autoPick pick it automatically if only one option is available + */ + public isAutoPicked(options: IMainClassOption[], autoPick?: boolean) { + const shouldAutoPick: boolean = (autoPick === undefined ? true : !!autoPick); + if (!options || !options.length) { + return true; + } else if (shouldAutoPick && options.length === 1) { + return true; + } + + return false; + } + private getMRUTimestamp(mainClassOption: IMainClassOption): number { return this.cache[this.getKey(mainClassOption)] || 0; } From c8a10a837db99e7932ed48da0f0fc06261018cf0 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Mon, 14 Dec 2020 21:40:32 +0800 Subject: [PATCH 5/8] Refactor the progressReporter api interfaces per comments --- README.md | 2 +- package.nls.json | 2 +- package.nls.zh.json | 2 +- src/configurationProvider.ts | 2 +- src/extension.ts | 4 +-- src/progressAPI.ts | 37 +++++++++++++------ src/progressImpl.ts | 70 +++++++++++++++++++----------------- 7 files changed, 69 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 58ac8c16..9ac581ba 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Please also check the documentation of [Language Support for Java by Red Hat](ht - `java.debug.settings.jdwp.limitOfVariablesPerJdwpRequest`: The maximum number of variables or fields that can be requested in one JDWP request. The higher the value, the less frequently debuggee will be requested when expanding the variable view. Also a large number can cause JDWP request timeout. Defaults to 100. - `java.debug.settings.jdwp.requestTimeout`: The timeout (ms) of JDWP request when the debugger communicates with the target JVM. Defaults to 3000. - `java.debug.settings.vmArgs`: The default VM arguments to launch the Java program. Eg. Use '-Xmx1G -ea' to increase the heap size to 1GB and enable assertions. If you want to customize the VM arguments for a specific debug session, please modify the 'vmArgs' config in launch.json. -- `java.silentNotification`: Control whether the progress notifications can be shown. Defaults to `false`. +- `java.silentNotification`: Controls whether notifications can be used to report progress. If true, use status bar to report progress instead. Defaults to `false`. Pro Tip: The documentation [Configuration.md](https://github.com/microsoft/vscode-java-debug/blob/master/Configuration.md) provides lots of samples to demonstrate how to use these debug configurations, recommend to take a look. diff --git a/package.nls.json b/package.nls.json index 6b1d97e0..ad012756 100644 --- a/package.nls.json +++ b/package.nls.json @@ -59,5 +59,5 @@ "java.debugger.configuration.jdwp.limitOfVariablesPerJdwpRequest.description": "The maximum number of variables or fields that can be requested in one JDWP request. The higher the value, the less frequently debuggee will be requested when expanding the variable view. Also a large number can cause JDWP request timeout.", "java.debugger.configuration.jdwp.requestTimeout.description": "The timeout (ms) of JDWP request when the debugger communicates with the target JVM.", "java.debugger.configuration.vmArgs.description": "The default VM arguments to launch the Java program. Eg. Use '-Xmx1G -ea' to increase the heap size to 1GB and enable assertions. If you want to customize the VM arguments for a specific debug session, please modify the 'vmArgs' config in launch.json.", - "java.debugger.configuration.silentNotification": "Control whether the progress notifications can be shown." + "java.debugger.configuration.silentNotification": "Controls whether notifications can be used to report progress. If true, use status bar to report progress instead." } diff --git a/package.nls.zh.json b/package.nls.zh.json index 38d761bf..9e951069 100644 --- a/package.nls.zh.json +++ b/package.nls.zh.json @@ -57,5 +57,5 @@ "java.debugger.configuration.jdwp.limitOfVariablesPerJdwpRequest.description": "一次JDWP请求中可以请求的变量或字段的最大数量。该值越高,在展开变量视图时,请求debuggee的频率就越低。同时数量过大也会导致JDWP请求超时。", "java.debugger.configuration.jdwp.requestTimeout.description": "调试器与目标JVM通信时JDWP请求的超时时间(ms)。", "java.debugger.configuration.vmArgs.description": "启动Java程序的默认VM参数。例如,使用'-Xmx1G -ea'将堆大小增加到1GB并启用断言。如果要为特定的调试会话定制VM参数,请修改launch.json中的'vmArgs'配置。", - "java.debugger.configuration.silentNotification": "控制是否可以显示进度通知。" + "java.debugger.configuration.silentNotification": "控制是否可以使用通知来报告进度。如果为真,则使用状态栏来报告进度。" } \ No newline at end of file diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index 0781f8d8..c033fbe6 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -136,7 +136,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration resolve([defaultLaunchConfig]); } finally { - progressReporter.cancel(); + progressReporter.done(); } }); } diff --git a/src/extension.ts b/src/extension.ts index 98409b84..a959e66b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -278,7 +278,7 @@ async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { } } } catch (ex) { - progressReporter.cancel(); + progressReporter.done(); if (ex instanceof utility.OperationCancelledError) { return; } @@ -389,7 +389,7 @@ async function runJavaProject(node: any, noDebug: boolean) { debugConfig.__progressId = progressReporter.getId(); vscode.debug.startDebugging(workspaceFolder, debugConfig); } catch (ex) { - progressReporter.cancel(); + progressReporter.done(); if (ex instanceof utility.OperationCancelledError) { return; } diff --git a/src/progressAPI.ts b/src/progressAPI.ts index 20b3193e..44413c0f 100644 --- a/src/progressAPI.ts +++ b/src/progressAPI.ts @@ -3,6 +3,7 @@ import { CancellationToken, ProgressLocation } from "vscode"; +/* tslint:disable */ export interface IProgressReporter { /** * Returns the id of the progress reporter. @@ -11,10 +12,27 @@ export interface IProgressReporter { /** * Reports a progress message update. - * @param subTaskName the message shown in the status bar - * @param detailedMessage the detailed message shown in the notification + * @param subTaskName the sub task name that's running + * @param detailedMessage a more detailed message to be shown in progress notifications */ - report(subTaskName: string, detailedMessage: string): void; + report(subTaskName: string, detailedMessage?: string): void; + + /** + * Reports a progress message update. + * @param subTaskName the sub task name that's running + * @param increment use `increment` to report discrete progress. Each call with a `increment` + * value will be summed up and reflected as overall progress until 100% is reached. + */ + report(subTaskName: string, increment?: number): void; + + /** + * Reports a progress message update. + * @param subTaskName the sub task name that's running + * @param detailedMessage a more detailed message to be shown in progress notifications + * @param increment use `increment` to report discrete progress. Each call with a `increment` + * value will be summed up and reflected as overall progress until 100% is reached. + */ + report(subTaskName: string, detailedMessage: string, increment?: number): void; /** * Shows the progress reporter. @@ -23,15 +41,10 @@ export interface IProgressReporter { /** * Hides the progress reporter. - * @param onlyNotifications only hide the progress reporter when it's shown as notification + * @param onlyNotifications only hide the progress reporter when it's shown as notification. */ hide(onlyNotifications?: boolean): void; - /** - * Cancels the progress reporter. - */ - cancel(): void; - /** * Returns whether the progress reporter has been cancelled or completed. */ @@ -54,7 +67,7 @@ export interface IProgressReporter { observe(token?: CancellationToken): void; } -export interface IProgressReporterProvider { +export interface IProgressProvider { /** * Creates a progress reporter. * @param jobName the job name @@ -67,7 +80,9 @@ export interface IProgressReporterProvider { createProgressReporter(jobName: string, progressLocation: ProgressLocation | { viewId: string }, cancellable?: boolean): IProgressReporter; /** - * Creates a progress reporter for the task to run before the debug session starts. + * Creates a progress reporter for the preLaunch task that runs before the debug session starts. + * For example, building the workspace and calculating the launch configuration are the typical + * preLaunch tasks. * @param jobName the job name */ createProgressReporterForPreLaunchTask(jobName: string): IProgressReporter; diff --git a/src/progressImpl.ts b/src/progressImpl.ts index 9fabe4ce..b36a97af 100644 --- a/src/progressImpl.ts +++ b/src/progressImpl.ts @@ -2,9 +2,9 @@ // Licensed under the MIT license. import { v4 } from "uuid"; -import { CancellationToken, CancellationTokenSource, Disposable, EventEmitter, Progress, ProgressLocation, +import { CancellationToken, CancellationTokenSource, Disposable, EventEmitter, ProgressLocation, StatusBarAlignment, StatusBarItem, window, workspace } from "vscode"; -import { IProgressReporter, IProgressReporterProvider } from "./progressAPI"; +import { IProgressProvider, IProgressReporter } from "./progressAPI"; const STATUS_COMMAND: string = "java.show.server.task.status"; class ProgressReporter implements IProgressReporter { @@ -13,8 +13,8 @@ class ProgressReporter implements IProgressReporter { private _progressLocation: ProgressLocation | { viewId: string }; private _cancellable: boolean = false; - private _subTaskName: string; - private _detailedMessage: string; + private _message: string; + private _increment: number | undefined; private _isShown: boolean; private _tokenSource = new CancellationTokenSource(); @@ -55,14 +55,23 @@ class ProgressReporter implements IProgressReporter { return this._id; } - public report(subTaskName: string, detailedMessage: string): void { - this._subTaskName = subTaskName; - this._detailedMessage = detailedMessage || subTaskName || this._jobName; - this._progressEventEmitter.fire(undefined); + public report(subTaskName: string, detailedMessage?: string | number, increment?: number): void { + this._message = subTaskName || this._jobName; if (this._statusBarItem) { - this._statusBarItem.text = `$(sync~spin) ${this._subTaskName || this._jobName}...`; + this._statusBarItem.text = `$(sync~spin) ${this._message}...`; + } else { + if (typeof increment === "number") { + this._increment = increment; + } else if (typeof detailedMessage === "number") { + this._increment = detailedMessage; + } + + if (typeof detailedMessage === "string") { + this._message = detailedMessage || this._message; + } } + this._progressEventEmitter.fire(undefined); this.show(); } @@ -82,20 +91,16 @@ class ProgressReporter implements IProgressReporter { } } - public cancel() { - this._tokenSource.cancel(); - this._cancelProgressEventEmitter.fire(undefined); - this._statusBarItem?.hide(); - this._disposables.forEach((disposable) => disposable.dispose()); - ( progressProvider).remove(this); - } - public isCancelled(): boolean { return this.getCancellationToken().isCancellationRequested; } public done(): void { - this.cancel(); + this._tokenSource.cancel(); + this._cancelProgressEventEmitter.fire(undefined); + this._statusBarItem?.hide(); + this._disposables.forEach((disposable) => disposable.dispose()); + ( progressProvider).remove(this); } public getCancellationToken(): CancellationToken { @@ -104,7 +109,7 @@ class ProgressReporter implements IProgressReporter { public observe(token?: CancellationToken): void { token?.onCancellationRequested(() => { - this.cancel(); + this.done(); }); } @@ -119,10 +124,16 @@ class ProgressReporter implements IProgressReporter { title: this._jobName ? `[${this._jobName}](command:${STATUS_COMMAND})` : undefined, cancellable: this._cancellable, }, (progress, token) => { - this.reportMessage(progress); + progress.report({ + message: this._message, + increment: this._increment, + }); this.observe(token); this._progressEventEmitter.event(() => { - this.reportMessage(progress); + progress.report({ + message: this._message, + increment: this._increment, + }); }); return new Promise((resolve) => { this._cancelProgressEventEmitter.event(() => { @@ -131,26 +142,19 @@ class ProgressReporter implements IProgressReporter { }); }); } - - private reportMessage(progress: Progress<{ message?: string; increment?: number }>): void { - const message: string = this._progressLocation === ProgressLocation.Notification ? this._detailedMessage : this._subTaskName; - progress.report({ - message, - }); - } } -class PreLaunchTaskProgressReporter extends ProgressReporter implements IProgressReporter { +class PreLaunchTaskProgressReporter extends ProgressReporter { constructor(jobName: string) { super(jobName, ProgressLocation.Notification, true); } - public report(subTaskName: string, detailedMessage: string): void { - super.report("Building - " + subTaskName, detailedMessage); + public report(subTaskName: string, detailedMessage?: string | number, increment?: number): void { + super.report("Building - " + subTaskName, detailedMessage, increment); } } -class ProgressReporterProvider implements IProgressReporterProvider { +class ProgressProvider implements IProgressProvider { private store: { [key: string]: IProgressReporter } = {}; public createProgressReporter(jobName: string, progressLocation: ProgressLocation, cancellable?: boolean): IProgressReporter { @@ -174,4 +178,4 @@ class ProgressReporterProvider implements IProgressReporterProvider { } } -export const progressProvider: IProgressReporterProvider = new ProgressReporterProvider(); +export const progressProvider: IProgressProvider = new ProgressProvider(); From 6989dfaf3b03dd76282e440e5cb009459f3c7f20 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Tue, 15 Dec 2020 10:02:22 +0800 Subject: [PATCH 6/8] Simplify report method --- src/configurationProvider.ts | 14 ++++++++------ src/extension.ts | 12 ++++++------ src/progressAPI.ts | 31 ++++--------------------------- src/progressImpl.ts | 33 +++++---------------------------- src/utility.ts | 6 +++--- 5 files changed, 26 insertions(+), 70 deletions(-) diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index c033fbe6..932bd6b3 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -110,7 +110,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration resolve([defaultLaunchConfig]); return; } - progressReporter.report("Resolve Java Configs", "Auto generating Java configuration..."); + progressReporter.report("Generating Java configuration..."); const mainClasses = await lsPlugin.resolveMainClass(folder ? folder.uri : undefined); const cache = {}; const launchConfigs = mainClasses.map((item) => { @@ -174,8 +174,10 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration let progressReporter = progressProvider.getProgressReporter(config.__progressId); if (!progressReporter && config.__progressId) { return undefined; + } else if (!progressReporter) { + progressReporter = progressProvider.createProgressReporter(config.noDebug ? "Run" : "Debug", vscode.ProgressLocation.Notification, true); } - progressReporter = progressReporter || progressProvider.createProgressReporterForPreLaunchTask(config.noDebug ? "Run" : "Debug"); + progressReporter.observe(token); if (progressReporter.isCancelled()) { return undefined; @@ -216,7 +218,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } if (needsBuildWorkspace()) { - progressReporter.report("Compile", "Compiling Java workspace..."); + progressReporter.report("Compiling..."); const proceed = await buildWorkspace(progressReporter); if (!proceed) { return undefined; @@ -227,9 +229,9 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration return undefined; } if (!config.mainClass) { - progressReporter.report("Resolve mainClass", "Resolving main class..."); + progressReporter.report("Resolving mainClass..."); } else { - progressReporter.report("Resolve Configuration", "Resolving launch configuration..."); + progressReporter.report("Resolving launch configuration..."); } const mainClassOption = await this.resolveAndValidateMainClass(folder && folder.uri, config, progressReporter); if (!mainClassOption || !mainClassOption.mainClass) { // Exit silently if the user cancels the prompt fix by ESC. @@ -237,7 +239,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration return undefined; } - progressReporter.report("Resolve Configuration", "Resolving launch configuration..."); + progressReporter.report("Resolving launch configuration..."); config.mainClass = mainClassOption.mainClass; config.projectName = mainClassOption.projectName; diff --git a/src/extension.ts b/src/extension.ts index a959e66b..5c9ab103 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -232,7 +232,7 @@ async function applyHCR(hcrStatusBar: NotificationBar) { } async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { - const progressReporter = progressProvider.createProgressReporterForPreLaunchTask(noDebug ? "Run" : "Debug"); + const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug", vscode.ProgressLocation.Notification, true); try { // Wait for Java Language Support extension being on Standard mode. const isOnStandardMode = await utility.waitForStandardMode(progressReporter); @@ -256,7 +256,7 @@ async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { const defaultPlaceHolder: string = "Select the main class to run"; if (!hasMainMethods && !canRunTests) { - progressReporter.report("Resolve mainClass", "Resolving main class..."); + progressReporter.report("Resolving mainClass..."); const mainClasses: IMainClassOption[] = await utility.searchMainMethods(); const placeHolder: string = `The file '${path.basename(uri.fsPath)}' is not executable, please select a main class you want to run.`; await launchMain(mainClasses, uri, noDebug, progressReporter, placeHolder, false /*autoPick*/); @@ -331,7 +331,7 @@ async function launchMain(mainMethods: IMainClassOption[], uri: vscode.Uri, noDe throw new utility.OperationCancelledError(""); } - progressReporter.report("Launch mainClass", "Launching the main class..."); + progressReporter.report("Launching mainClass..."); startDebugging(pick.mainClass, pick.projectName || "", uri, noDebug, progressReporter); } @@ -344,9 +344,9 @@ async function runJavaProject(node: any, noDebug: boolean) { throw error; } - const progressReporter = progressProvider.createProgressReporterForPreLaunchTask(noDebug ? "Run" : "Debug"); + const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug", vscode.ProgressLocation.Notification, true); try { - progressReporter.report("Resolve mainClass", "Resolving main class..."); + progressReporter.report("Resolving mainClass..."); const mainClassesOptions: IMainClassOption[] = await utility.searchMainMethods(vscode.Uri.parse(node.uri)); if (progressReporter.isCancelled()) { throw new utility.OperationCancelledError(""); @@ -367,7 +367,7 @@ async function runJavaProject(node: any, noDebug: boolean) { throw new utility.OperationCancelledError(""); } - progressReporter.report("Launch mainClass", "Launching the main class..."); + progressReporter.report("Launching mainClass..."); const projectName: string | undefined = pick.projectName; const mainClass: string = pick.mainClass; const filePath: string | undefined = pick.filePath; diff --git a/src/progressAPI.ts b/src/progressAPI.ts index 44413c0f..70442f01 100644 --- a/src/progressAPI.ts +++ b/src/progressAPI.ts @@ -3,7 +3,6 @@ import { CancellationToken, ProgressLocation } from "vscode"; -/* tslint:disable */ export interface IProgressReporter { /** * Returns the id of the progress reporter. @@ -12,27 +11,13 @@ export interface IProgressReporter { /** * Reports a progress message update. - * @param subTaskName the sub task name that's running - * @param detailedMessage a more detailed message to be shown in progress notifications - */ - report(subTaskName: string, detailedMessage?: string): void; - - /** - * Reports a progress message update. - * @param subTaskName the sub task name that's running - * @param increment use `increment` to report discrete progress. Each call with a `increment` - * value will be summed up and reflected as overall progress until 100% is reached. - */ - report(subTaskName: string, increment?: number): void; - - /** - * Reports a progress message update. - * @param subTaskName the sub task name that's running - * @param detailedMessage a more detailed message to be shown in progress notifications + * @param message the message to update * @param increment use `increment` to report discrete progress. Each call with a `increment` * value will be summed up and reflected as overall progress until 100% is reached. + * Note that currently only `ProgressLocation.Notification` is capable of showing + * discrete progress. */ - report(subTaskName: string, detailedMessage: string, increment?: number): void; + report(message: string, increment?: number): void; /** * Shows the progress reporter. @@ -79,14 +64,6 @@ export interface IProgressProvider { */ createProgressReporter(jobName: string, progressLocation: ProgressLocation | { viewId: string }, cancellable?: boolean): IProgressReporter; - /** - * Creates a progress reporter for the preLaunch task that runs before the debug session starts. - * For example, building the workspace and calculating the launch configuration are the typical - * preLaunch tasks. - * @param jobName the job name - */ - createProgressReporterForPreLaunchTask(jobName: string): IProgressReporter; - /** * Returns the progress reporter with the progress id. * @param progressId the progress id diff --git a/src/progressImpl.ts b/src/progressImpl.ts index b36a97af..2deb2900 100644 --- a/src/progressImpl.ts +++ b/src/progressImpl.ts @@ -55,20 +55,13 @@ class ProgressReporter implements IProgressReporter { return this._id; } - public report(subTaskName: string, detailedMessage?: string | number, increment?: number): void { - this._message = subTaskName || this._jobName; + public report(message: string, increment?: number): void { if (this._statusBarItem) { - this._statusBarItem.text = `$(sync~spin) ${this._message}...`; + const text = message ? `${this._jobName} - ${message}` : `${this._jobName}...`; + this._statusBarItem.text = `$(sync~spin) ${text}`; } else { - if (typeof increment === "number") { - this._increment = increment; - } else if (typeof detailedMessage === "number") { - this._increment = detailedMessage; - } - - if (typeof detailedMessage === "string") { - this._message = detailedMessage || this._message; - } + this._message = message; + this._increment = increment; } this._progressEventEmitter.fire(undefined); @@ -144,16 +137,6 @@ class ProgressReporter implements IProgressReporter { } } -class PreLaunchTaskProgressReporter extends ProgressReporter { - constructor(jobName: string) { - super(jobName, ProgressLocation.Notification, true); - } - - public report(subTaskName: string, detailedMessage?: string | number, increment?: number): void { - super.report("Building - " + subTaskName, detailedMessage, increment); - } -} - class ProgressProvider implements IProgressProvider { private store: { [key: string]: IProgressReporter } = {}; @@ -163,12 +146,6 @@ class ProgressProvider implements IProgressProvider { return progressReporter; } - public createProgressReporterForPreLaunchTask(jobName: string): IProgressReporter { - const progressReporter = new PreLaunchTaskProgressReporter(jobName); - this.store[progressReporter.getId()] = progressReporter; - return progressReporter; - } - public getProgressReporter(progressId: string): IProgressReporter | undefined { return this.store[progressId]; } diff --git a/src/utility.ts b/src/utility.ts index 524fa1e9..55510f4d 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -227,7 +227,7 @@ export enum ServerMode { */ export async function waitForStandardMode(progressReporter: IProgressReporter): Promise { if (await isImportingProjects()) { - progressReporter.report("Import", "Importing Java projects..."); + progressReporter.report("Importing projects..."); } const api = await getJavaExtensionAPI(progressReporter); @@ -243,7 +243,7 @@ export async function waitForStandardMode(progressReporter: IProgressReporter): return true; } - progressReporter?.report("Import", "Importing Java projects..."); + progressReporter?.report("Importing projects..."); return new Promise((resolve) => { progressReporter.getCancellationToken().onCancellationRequested(() => { resolve(false); @@ -260,7 +260,7 @@ export async function waitForStandardMode(progressReporter: IProgressReporter): return false; } else if (api && api.serverMode === ServerMode.HYBRID) { - progressReporter.report("Import", "Importing Java projects..."); + progressReporter.report("Importing projects..."); return new Promise((resolve) => { progressReporter.getCancellationToken().onCancellationRequested(() => { resolve(false); From 3423013faa77565240f859f9a2ab55778c4c9891 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Tue, 15 Dec 2020 12:54:17 +0800 Subject: [PATCH 7/8] Refine the progress message --- src/build.ts | 2 +- src/configurationProvider.ts | 14 ++++++++++---- src/extension.ts | 12 ++++++++---- src/progressImpl.ts | 15 +++++++-------- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/build.ts b/src/build.ts index 71b8c898..ed9b36ac 100644 --- a/src/build.ts +++ b/src/build.ts @@ -35,7 +35,7 @@ export async function buildWorkspace(progressReporter: IProgressReporter): Promi }; })(); - if (buildResult.error === CompileWorkspaceStatus.CANCELLED) { + if (progressReporter.isCancelled() || buildResult.error === CompileWorkspaceStatus.CANCELLED) { return false; } else { return handleBuildFailure(buildResult.operationId, buildResult.error, progressReporter); diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index 932bd6b3..313ec3b8 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -229,7 +229,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration return undefined; } if (!config.mainClass) { - progressReporter.report("Resolving mainClass..."); + progressReporter.report("Resolving main class..."); } else { progressReporter.report("Resolving launch configuration..."); } @@ -389,7 +389,9 @@ 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) { + if (progressReporter.isCancelled()) { + return undefined; + } else if (mainEntries.length) { if (!mainClassPicker.isAutoPicked(mainEntries)) { progressReporter.hide(true); } @@ -405,7 +407,9 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration const containsExternalClasspaths = !_.isEmpty(config.classPaths) || !_.isEmpty(config.modulePaths); const validationResponse = await lsPlugin.validateLaunchConfig(config.mainClass, config.projectName, containsExternalClasspaths, folder); - if (!validationResponse.mainClass.isValid || !validationResponse.projectName.isValid) { + if (progressReporter.isCancelled()) { + return undefined; + } else if (!validationResponse.mainClass.isValid || !validationResponse.projectName.isValid) { return this.fixMainClass(folder, config, validationResponse, progressReporter); } @@ -496,7 +500,9 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration private async promptMainClass(folder: vscode.Uri | undefined, progressReporter: IProgressReporter, hintMessage?: string): Promise { const res = await lsPlugin.resolveMainClass(folder); - if (res.length === 0) { + if (progressReporter.isCancelled()) { + return undefined; + } else if (res.length === 0) { const workspaceFolder = folder ? vscode.workspace.getWorkspaceFolder(folder) : undefined; throw new utility.UserError({ message: `Cannot find a class with the main method${ workspaceFolder ? " in the folder '" + workspaceFolder.name + "'" : ""}.`, diff --git a/src/extension.ts b/src/extension.ts index 5c9ab103..c7e1efda 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -256,8 +256,12 @@ async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { const defaultPlaceHolder: string = "Select the main class to run"; if (!hasMainMethods && !canRunTests) { - progressReporter.report("Resolving mainClass..."); + progressReporter.report("Resolving main class..."); const mainClasses: IMainClassOption[] = await utility.searchMainMethods(); + if (progressReporter.isCancelled()) { + throw new utility.OperationCancelledError(""); + } + const placeHolder: string = `The file '${path.basename(uri.fsPath)}' is not executable, please select a main class you want to run.`; await launchMain(mainClasses, uri, noDebug, progressReporter, placeHolder, false /*autoPick*/); } else if (hasMainMethods && !canRunTests) { @@ -331,7 +335,7 @@ async function launchMain(mainMethods: IMainClassOption[], uri: vscode.Uri, noDe throw new utility.OperationCancelledError(""); } - progressReporter.report("Launching mainClass..."); + progressReporter.report("Launching main class..."); startDebugging(pick.mainClass, pick.projectName || "", uri, noDebug, progressReporter); } @@ -346,7 +350,7 @@ async function runJavaProject(node: any, noDebug: boolean) { const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug", vscode.ProgressLocation.Notification, true); try { - progressReporter.report("Resolving mainClass..."); + progressReporter.report("Resolving main class..."); const mainClassesOptions: IMainClassOption[] = await utility.searchMainMethods(vscode.Uri.parse(node.uri)); if (progressReporter.isCancelled()) { throw new utility.OperationCancelledError(""); @@ -367,7 +371,7 @@ async function runJavaProject(node: any, noDebug: boolean) { throw new utility.OperationCancelledError(""); } - progressReporter.report("Launching mainClass..."); + progressReporter.report("Launching main class..."); const projectName: string | undefined = pick.projectName; const mainClass: string = pick.mainClass; const filePath: string | undefined = pick.filePath; diff --git a/src/progressImpl.ts b/src/progressImpl.ts index 2deb2900..fdc426b7 100644 --- a/src/progressImpl.ts +++ b/src/progressImpl.ts @@ -19,8 +19,8 @@ class ProgressReporter implements IProgressReporter { private _tokenSource = new CancellationTokenSource(); private _statusBarItem: StatusBarItem | undefined; - private _cancelProgressEventEmitter: EventEmitter; - private _progressEventEmitter: EventEmitter; + private _cancelProgressEventEmitter: EventEmitter; + private _progressEventEmitter: EventEmitter; private _disposables: Disposable[] = []; constructor(jobName: string, progressLocation: ProgressLocation | { viewId: string }, cancellable?: boolean) { @@ -59,12 +59,11 @@ class ProgressReporter implements IProgressReporter { if (this._statusBarItem) { const text = message ? `${this._jobName} - ${message}` : `${this._jobName}...`; this._statusBarItem.text = `$(sync~spin) ${text}`; - } else { - this._message = message; - this._increment = increment; } - this._progressEventEmitter.fire(undefined); + this._message = message; + this._increment = increment; + this._progressEventEmitter.fire(); this.show(); } @@ -79,7 +78,7 @@ class ProgressReporter implements IProgressReporter { public hide(onlyNotifications?: boolean): void { if (onlyNotifications && this._progressLocation === ProgressLocation.Notification) { - this._cancelProgressEventEmitter.fire(undefined); + this._cancelProgressEventEmitter.fire(); this._isShown = false; } } @@ -90,7 +89,7 @@ class ProgressReporter implements IProgressReporter { public done(): void { this._tokenSource.cancel(); - this._cancelProgressEventEmitter.fire(undefined); + this._cancelProgressEventEmitter.fire(); this._statusBarItem?.hide(); this._disposables.forEach((disposable) => disposable.dispose()); ( progressProvider).remove(this); From 877d60d7914b20f27633d8aecb576ad6ecbaf9d1 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Tue, 15 Dec 2020 14:32:03 +0800 Subject: [PATCH 8/8] Add overload api for createProgressReporter --- src/configurationProvider.ts | 2 +- src/extension.ts | 4 ++-- src/progressAPI.ts | 11 +++++------ src/progressImpl.ts | 10 ++++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index 313ec3b8..9aa23bd5 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -175,7 +175,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration if (!progressReporter && config.__progressId) { return undefined; } else if (!progressReporter) { - progressReporter = progressProvider.createProgressReporter(config.noDebug ? "Run" : "Debug", vscode.ProgressLocation.Notification, true); + progressReporter = progressProvider.createProgressReporter(config.noDebug ? "Run" : "Debug"); } progressReporter.observe(token); diff --git a/src/extension.ts b/src/extension.ts index c7e1efda..0eca7ae7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -232,7 +232,7 @@ async function applyHCR(hcrStatusBar: NotificationBar) { } async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { - const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug", vscode.ProgressLocation.Notification, true); + const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug"); try { // Wait for Java Language Support extension being on Standard mode. const isOnStandardMode = await utility.waitForStandardMode(progressReporter); @@ -348,7 +348,7 @@ async function runJavaProject(node: any, noDebug: boolean) { throw error; } - const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug", vscode.ProgressLocation.Notification, true); + const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug"); try { progressReporter.report("Resolving main class..."); const mainClassesOptions: IMainClassOption[] = await utility.searchMainMethods(vscode.Uri.parse(node.uri)); diff --git a/src/progressAPI.ts b/src/progressAPI.ts index 70442f01..02b12f14 100644 --- a/src/progressAPI.ts +++ b/src/progressAPI.ts @@ -56,13 +56,12 @@ export interface IProgressProvider { /** * Creates a progress reporter. * @param jobName the job name - * @param progressLocation The location at which progress should show - * @param cancellable Controls if a cancel button should show to allow the user - * to cancel the progress reporter. Note that currently only - * `ProgressLocation.Notification` is supporting to show a cancel - * button. + * @param progressLocation The location at which progress should show, defaults to `ProgressLocation.Notification`. + * @param cancellable Controls if a cancel button should show to allow the user to cancel the progress reporter, + * defaults to true. Note that currently only `ProgressLocation.Notification` is supporting + * to show a cancel button. */ - createProgressReporter(jobName: string, progressLocation: ProgressLocation | { viewId: string }, cancellable?: boolean): IProgressReporter; + createProgressReporter(jobName: string, progressLocation?: ProgressLocation | { viewId: string }, cancellable?: boolean): IProgressReporter; /** * Returns the progress reporter with the progress id. diff --git a/src/progressImpl.ts b/src/progressImpl.ts index fdc426b7..954cce9f 100644 --- a/src/progressImpl.ts +++ b/src/progressImpl.ts @@ -23,10 +23,10 @@ class ProgressReporter implements IProgressReporter { private _progressEventEmitter: EventEmitter; private _disposables: Disposable[] = []; - constructor(jobName: string, progressLocation: ProgressLocation | { viewId: string }, cancellable?: boolean) { + constructor(jobName: string, progressLocation: ProgressLocation | { viewId: string }, cancellable: boolean) { this._jobName = jobName; this._progressLocation = progressLocation || ProgressLocation.Notification; - this._cancellable = !!cancellable; + this._cancellable = cancellable; const config = workspace.getConfiguration("java"); if (config.silentNotification && this._progressLocation === ProgressLocation.Notification) { this._progressLocation = ProgressLocation.Window; @@ -139,8 +139,10 @@ class ProgressReporter implements IProgressReporter { class ProgressProvider implements IProgressProvider { private store: { [key: string]: IProgressReporter } = {}; - public createProgressReporter(jobName: string, progressLocation: ProgressLocation, cancellable?: boolean): IProgressReporter { - const progressReporter = new ProgressReporter(jobName, progressLocation, cancellable); + public createProgressReporter(jobName: string, progressLocation?: ProgressLocation | { viewId: string }, + cancellable?: boolean): IProgressReporter { + const progressReporter = new ProgressReporter(jobName, progressLocation || ProgressLocation.Notification, + cancellable === undefined ? true : !!cancellable); this.store[progressReporter.getId()] = progressReporter; return progressReporter; }