// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import * as _ from "lodash";
import * as path from "path";
import * as vscode from "vscode";
import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper";
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";
const JAVA_DEBUG_CODELENS_COMMAND = "java.debug.debugCodeLens";
const JAVA_DEBUG_CONFIGURATION = "java.debug.settings";
const ENABLE_CODE_LENS_VARIABLE = "enableRunDebugCodeLens";
export function initializeCodeLensProvider(context: vscode.ExtensionContext): void {
// delay registering codelens provider until the Java extension is activated.
if (isActivatedByJavaFile() && isJavaExtEnabled()) {
getJavaExtensionAPI().then((api) => {
if (api && (api.serverMode === ServerMode.LIGHTWEIGHT || api.serverMode === ServerMode.HYBRID)) {
api.onDidServerModeChange((mode: string) => {
if (mode === ServerMode.STANDARD) {
context.subscriptions.push(new DebugCodeLensContainer());
}
});
} else {
context.subscriptions.push(new DebugCodeLensContainer());
}
});
} else {
context.subscriptions.push(new DebugCodeLensContainer());
}
}
function isActivatedByJavaFile(): boolean {
if (vscode.window.activeTextEditor) {
return vscode.window.activeTextEditor.document && vscode.window.activeTextEditor.document.languageId === "java";
}
return false;
}
class DebugCodeLensContainer implements vscode.Disposable {
private runCommand: vscode.Disposable;
private debugCommand: vscode.Disposable;
private lensProvider: vscode.Disposable | undefined;
private hoverProvider: vscode.Disposable | undefined;
private configurationEvent: vscode.Disposable;
constructor() {
this.runCommand = instrumentOperationAsVsCodeCommand(JAVA_RUN_CODELENS_COMMAND, runJavaProgram);
this.debugCommand = instrumentOperationAsVsCodeCommand(JAVA_DEBUG_CODELENS_COMMAND, debugJavaProgram);
const configuration = vscode.workspace.getConfiguration(JAVA_DEBUG_CONFIGURATION);
const isCodeLensEnabled = configuration.get(ENABLE_CODE_LENS_VARIABLE);
if (isCodeLensEnabled) {
this.lensProvider = vscode.languages.registerCodeLensProvider(JAVA_LANGID, new DebugCodeLensProvider());
} else {
this.hoverProvider = initializeHoverProvider();
}
this.configurationEvent = vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => {
if (event.affectsConfiguration(JAVA_DEBUG_CONFIGURATION)) {
const newConfiguration = vscode.workspace.getConfiguration(JAVA_DEBUG_CONFIGURATION);
const newEnabled = newConfiguration.get(ENABLE_CODE_LENS_VARIABLE);
if (newEnabled && this.lensProvider === undefined) {
this.lensProvider = vscode.languages.registerCodeLensProvider(JAVA_LANGID, new DebugCodeLensProvider());
} else if (!newEnabled && this.lensProvider !== undefined) {
this.lensProvider.dispose();
this.lensProvider = undefined;
}
if (newEnabled && this.hoverProvider) {
this.hoverProvider.dispose();
this.hoverProvider = undefined;
} else if (!newEnabled && !this.hoverProvider) {
this.hoverProvider = initializeHoverProvider();
}
}
}, this);
}
public dispose() {
if (this.lensProvider !== undefined) {
this.lensProvider.dispose();
}
if (this.hoverProvider) {
this.hoverProvider.dispose();
}
this.runCommand.dispose();
this.debugCommand.dispose();
this.configurationEvent.dispose();
}
}
class DebugCodeLensProvider implements vscode.CodeLensProvider {
public async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise {
try {
const mainMethods: IMainMethod[] = await resolveMainMethod(document.uri, token);
return _.flatten(mainMethods.map((method) => {
return [
new vscode.CodeLens(method.range, {
title: "Run",
command: JAVA_RUN_CODELENS_COMMAND,
tooltip: "Run Java Program",
arguments: [ method.mainClass, method.projectName, document.uri ],
}),
new vscode.CodeLens(method.range, {
title: "Debug",
command: JAVA_DEBUG_CODELENS_COMMAND,
tooltip: "Debug Java Program",
arguments: [ method.mainClass, method.projectName, document.uri ],
}),
];
}));
} catch (ex) {
// do nothing.
return [];
}
}
}
function runJavaProgram(mainClass: string, projectName: string, uri: vscode.Uri): Promise {
return startDebugging(mainClass, projectName, uri, true);
}
function debugJavaProgram(mainClass: string, projectName: string, uri: vscode.Uri): Promise {
return startDebugging(mainClass, projectName, uri, false);
}
async function constructDebugConfig(mainClass: string, projectName: string, workspace?: vscode.Uri): Promise {
const launchConfigurations: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("launch", workspace);
const rawConfigs: vscode.DebugConfiguration[] = launchConfigurations.configurations;
let debugConfig: vscode.DebugConfiguration | undefined = _.find(rawConfigs, (config) => {
return config.mainClass === mainClass && _.toString(config.projectName) === _.toString(projectName);
});
if (!debugConfig) {
debugConfig = _.find(rawConfigs, (config) => {
return config.mainClass === mainClass && !config.projectName;
});
}
if (!debugConfig) {
debugConfig = {
type: "java",
name: `${mainClass.substr(mainClass.lastIndexOf(".") + 1)}`,
request: "launch",
mainClass,
projectName,
};
// Persist the configuration into launch.json only if the launch.json file already exists in the workspace.
if ((rawConfigs && rawConfigs.length) || await launchJsonExists(workspace)) {
try {
// Insert the default debug configuration to the beginning of launch.json.
rawConfigs.splice(0, 0, debugConfig);
await launchConfigurations.update("configurations", rawConfigs, vscode.ConfigurationTarget.WorkspaceFolder);
} catch (error) {
// When launch.json has unsaved changes before invoking the update api, it will throw the error below:
// 'Unable to write into launch configuration file because the file is dirty. Please save it first and then try again.'
// It's safe to ignore it because the only impact is the configuration is not saved, but you can continue to start the debugger.
}
}
}
return _.cloneDeep(debugConfig);
}
async function launchJsonExists(workspace?: vscode.Uri): Promise {
if (!workspace) {
return false;
}
const workspaceFolder = vscode.workspace.getWorkspaceFolder(workspace);
// Excluding "**/node_modules/**" as a common cause of excessive CPU usage.
// https://github.com/microsoft/vscode/issues/75314#issuecomment-503195666
const results: vscode.Uri[] = await vscode.workspace.findFiles(".vscode/launch.json", "**/node_modules/**");
return !!results.find((launchJson) => vscode.workspace.getWorkspaceFolder(launchJson) === workspaceFolder);
}
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());
if (workspaceUri && onClasspath === false && !(await addToClasspath(uri))) {
return false;
}
const debugConfig: vscode.DebugConfiguration = await constructDebugConfig(mainClass, projectName, workspaceUri);
debugConfig.projectName = projectName;
debugConfig.noDebug = noDebug;
debugConfig.__progressId = progressReporter?.getId();
debugConfig.__origin = "internal";
return vscode.debug.startDebugging(workspaceFolder, debugConfig);
}
async function addToClasspath(uri: vscode.Uri): Promise {
const fileName = path.basename(uri.fsPath || "");
const parentFsPath = path.dirname(uri.fsPath || "");
if (!parentFsPath) {
return true;
}
const parentUri = vscode.Uri.file(parentFsPath);
let parentPath = vscode.workspace.asRelativePath(parentUri, true);
if (parentPath === parentUri.fsPath) {
parentPath = path.basename(parentFsPath);
}
sendInfo("", {operationName: "notOnClasspath"});
const ans = await vscode.window.showWarningMessage(`The file ${fileName} isn't on the classpath, the runtime may throw class not found error. `
+ `Do you want to add the parent folder "${parentPath}" to Java source path?`, "Add to Source Path", "Skip");
if (ans === "Skip") {
return true;
} else if (ans === "Add to Source Path") {
sendInfo("", {operationName: "addToSourcePath"});
vscode.commands.executeCommand("java.project.addToSourcePath.command", parentUri);
}
return false;
}