See More

// 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"; import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, instrumentOperationAsVsCodeCommand, setUserError } from "vscode-extension-telemetry-wrapper"; import * as commands from "./commands"; import { JavaDebugConfigurationProvider } from "./configurationProvider"; import { HCR_EVENT, JAVA_LANGID, USER_NOTIFICATION_EVENT } from "./constants"; import { NotificationBar } from "./customWidget"; import { initializeCodeLensProvider, startDebugging } from "./debugCodeLensProvider"; import { initExpService } from "./experimentationService"; import { handleHotCodeReplaceCustomEvent, initializeHotCodeReplace, NO_BUTTON, YES_BUTTON } from "./hotCodeReplace"; import { JavaDebugAdapterDescriptorFactory } from "./javaDebugAdapterDescriptorFactory"; import { JavaInlineValuesProvider } from "./JavaInlineValueProvider"; import { logJavaException, logJavaInfo } from "./javaLogger"; import { IMainClassOption, IMainMethod, resolveMainMethod } from "./languageServerPlugin"; import { mainClassPicker } from "./mainClassPicker"; import { pickJavaProcess } from "./processPicker"; import { IProgressReporter } from "./progressAPI"; import { progressProvider } from "./progressImpl"; import { JavaTerminalLinkProvder } from "./terminalLinkProvider"; import { initializeThreadOperations } from "./threadOperations"; import * as utility from "./utility"; import { registerVariableMenuCommands } from "./variableMenu"; export async function activate(context: vscode.ExtensionContext): Promise { await initializeFromJsonFile(context.asAbsolutePath("./package.json"), { firstParty: true, }); await initExpService(context); return instrumentOperation("activation", initializeExtension)(context); } function initializeExtension(_operationId: string, context: vscode.ExtensionContext): any { registerDebugEventListener(context); registerVariableMenuCommands(context); context.subscriptions.push(vscode.window.registerTerminalLinkProvider(new JavaTerminalLinkProvder())); context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("java", new JavaDebugConfigurationProvider())); context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("java", new JavaDebugAdapterDescriptorFactory())); context.subscriptions.push(instrumentOperationAsVsCodeCommand("JavaDebug.SpecifyProgramArgs", async () => { return specifyProgramArguments(context); })); context.subscriptions.push(instrumentOperationAsVsCodeCommand("JavaDebug.PickJavaProcess", async () => { let javaProcess; try { javaProcess = await pickJavaProcess(); } catch (error) { vscode.window.showErrorMessage(error.message ? error.message : String(error)); } // tslint:disable-next-line return javaProcess ? String(javaProcess.pid) : "${command:PickJavaProcess}"; })); const hcrStatusBar: NotificationBar = new NotificationBar(); context.subscriptions.push(hcrStatusBar); context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.hotCodeReplace", async () => { await applyHCR(hcrStatusBar); })); context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.runJavaFile", async (uri: vscode.Uri) => { await runJavaFile(uri, true); })); context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.debugJavaFile", async (uri: vscode.Uri) => { await runJavaFile(uri, false); })); context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.runFromProjectView", async (node: any) => { await runJavaProject(node, true); })); context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.debugFromProjectView", async (node: any) => { await runJavaProject(node, false); })); initializeHotCodeReplace(context); initializeCodeLensProvider(context); initializeThreadOperations(context); context.subscriptions.push(vscode.languages.registerInlineValuesProvider("java", new JavaInlineValuesProvider())); return { progressProvider, }; } // this method is called when your extension is deactivated export async function deactivate() { await disposeTelemetryWrapper(); } function registerDebugEventListener(context: vscode.ExtensionContext) { const measureKeys = ["duration"]; context.subscriptions.push(vscode.debug.onDidTerminateDebugSession((e) => { if (e.type !== "java") { return; } fetchUsageData().then((ret) => { if (Array.isArray(ret) && ret.length) { ret.forEach((entry) => { const commonProperties: any = {}; const measureProperties: any = {}; for (const key of Object.keys(entry)) { if (measureKeys.indexOf(key) >= 0) { measureProperties[key] = entry[key]; } else { commonProperties[key] = String(entry[key]); } } if (entry.scope === "exception") { logJavaException(commonProperties); } else { logJavaInfo(commonProperties, measureProperties); } }); } }); })); context.subscriptions.push(vscode.debug.onDidReceiveDebugSessionCustomEvent((customEvent) => { const t = customEvent.session ? customEvent.session.type : undefined; if (t !== JAVA_LANGID) { return; } if (customEvent.event === HCR_EVENT) { handleHotCodeReplaceCustomEvent(customEvent); } else if (customEvent.event === USER_NOTIFICATION_EVENT) { handleUserNotification(customEvent); } })); } function handleUserNotification(customEvent: vscode.DebugSessionCustomEvent) { if (customEvent.body.notificationType === "ERROR") { utility.showErrorMessageWithTroubleshooting({ message: customEvent.body.message, }); } else if (customEvent.body.notificationType === "WARNING") { utility.showWarningMessageWithTroubleshooting({ message: customEvent.body.message, }); } else { vscode.window.showInformationMessage(customEvent.body.message); } } function fetchUsageData() { return commands.executeJavaLanguageServerCommand(commands.JAVA_FETCH_USAGE_DATA); } function specifyProgramArguments(context: vscode.ExtensionContext): Thenable { const javaDebugProgramArgsKey = "JavaDebugProgramArgs"; const options: vscode.InputBoxOptions = { ignoreFocusOut: true, placeHolder: "Enter program arguments or leave empty to pass no args", }; const prevArgs = context.workspaceState.get(javaDebugProgramArgsKey, ""); if (prevArgs.length > 0) { options.value = prevArgs; } return vscode.window.showInputBox(options).then((text) => { // When user cancels the input box (by pressing Esc), the text value is undefined. if (text !== undefined) { context.workspaceState.update(javaDebugProgramArgsKey, text); } return text || " "; }); } async function applyHCR(hcrStatusBar: NotificationBar) { const debugSession: vscode.DebugSession | undefined = vscode.debug.activeDebugSession; if (!debugSession) { return; } if (debugSession.configuration.noDebug) { vscode.window.showWarningMessage("Failed to apply the changes because hot code replace is not supported by run mode, " + "would you like to restart the program?", YES_BUTTON, NO_BUTTON).then((res) => { if (res === YES_BUTTON) { vscode.commands.executeCommand("workbench.action.debug.restart"); } }); return; } const autobuildConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("java.autobuild"); if (!autobuildConfig.enabled) { const ans = await vscode.window.showWarningMessage( "The hot code replace feature requires you to enable the autobuild flag, do you want to enable it?", "Yes", "No"); if (ans === "Yes") { await autobuildConfig.update("enabled", true); // Force an incremental build to avoid auto build is not finishing during HCR. try { await commands.executeJavaExtensionCommand(commands.JAVA_BUILD_WORKSPACE, false); } catch (err) { // do nothing. } } } hcrStatusBar.show("$(sync~spin)Applying code changes..."); const start = new Date().getTime(); const response = await debugSession.customRequest("redefineClasses"); const elapsed = new Date().getTime() - start; const humanVisibleDelay = elapsed < 150 ? 150 : 0; if (humanVisibleDelay) { await new Promise((resolve) => { setTimeout(resolve, humanVisibleDelay); }); } if (response && response.errorMessage) { // The detailed error message is handled by hotCodeReplace#handleHotCodeReplaceCustomEvent hcrStatusBar.clear(); return; } if (!response || !response.changedClasses || !response.changedClasses.length) { hcrStatusBar.clear(); vscode.window.showWarningMessage("Cannot find any changed classes for hot replace!"); return; } const changed = response.changedClasses.length; hcrStatusBar.show("$(check)" + `${changed} changed class${changed > 1 ? "es are" : " is"} reloaded`, 5 * 1000); } async function runJavaFile(uri: vscode.Uri, noDebug: boolean) { const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug"); try { // Wait for Java Language Support extension being on Standard mode. const isOnStandardMode = await utility.waitForStandardMode(progressReporter); if (!isOnStandardMode) { 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("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) { 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) { progressReporter.done(); if (ex instanceof utility.OperationCancelledError) { return; } if (ex instanceof utility.JavaExtensionNotEnabledError) { utility.guideToInstallJavaExtension(); return; } vscode.window.showErrorMessage(String((ex && ex.message) || ex)); } } async function canDelegateToJavaTestRunner(uri: vscode.Uri): Promise { const fsPath: string = uri.fsPath; const isTestFile: boolean = /.*[\/\\]src[\/\\]test[\/\\]java[\/\\].*[Tt]ests?\.java/.test(fsPath); if (!isTestFile) { return false; } return (await vscode.commands.getCommands()).includes("java.test.editor.run"); } 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(""); } } 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)"); throw new utility.OperationCancelledError(""); } if (!mainClassPicker.isAutoPicked(mainMethods, autoPick)) { progressReporter.hide(true); } const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainMethods, placeHolder, autoPick); if (!pick) { throw new utility.OperationCancelledError(""); } progressReporter.report("Launching main class..."); startDebugging(pick.mainClass, pick.projectName || "", uri, noDebug, progressReporter); } async function runJavaProject(node: any, noDebug: boolean) { if (!node || !node.name || !node.uri) { vscode.window.showErrorMessage(`Failed to ${noDebug ? "run" : "debug"} the project because of invalid project node. ` + "This command only applies to Project Explorer view."); const error = new Error(`Failed to ${noDebug ? "run" : "debug"} the project because of invalid project node.`); setUserError(error); throw error; } const progressReporter = progressProvider.createProgressReporter(noDebug ? "Run" : "Debug"); try { progressReporter.report("Resolving main class..."); const mainClassesOptions: IMainClassOption[] = await utility.searchMainMethods(vscode.Uri.parse(node.uri)); if (progressReporter.isCancelled()) { throw new utility.OperationCancelledError(""); } 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(""); } 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.report("Launching main class..."); 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.done(); if (ex instanceof utility.OperationCancelledError) { return; } throw ex; } }