diff --git a/src/commands.ts b/src/commands.ts index 68358c52..e1a93e3f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -30,6 +30,8 @@ export const JAVA_INFER_LAUNCH_COMMAND_LENGTH = "vscode.java.inferLaunchCommandL export const JAVA_CHECK_PROJECT_SETTINGS = "vscode.java.checkProjectSettings"; +export const JAVA_RESOLVE_ELEMENT_AT_SELECTION = "vscode.java.resolveElementAtSelection"; + export function executeJavaLanguageServerCommand(...rest) { // TODO: need to handle error and trace telemetry if (!utility.isJavaExtEnabled()) { diff --git a/src/debugCodeLensProvider.ts b/src/debugCodeLensProvider.ts index 1bae3e40..140200aa 100644 --- a/src/debugCodeLensProvider.ts +++ b/src/debugCodeLensProvider.ts @@ -6,6 +6,7 @@ import * as vscode from "vscode"; import { instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper"; import { JAVA_LANGID } from "./constants"; +import { initializeHoverProvider } from "./hoverProvider"; import { IMainMethod, resolveMainMethod } from "./languageServerPlugin"; import { getJavaExtensionAPI, isJavaExtEnabled } from "./utility"; @@ -37,6 +38,7 @@ 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() { @@ -48,6 +50,8 @@ class DebugCodeLensContainer implements vscode.Disposable { if (isCodeLensEnabled) { this.lensProvider = vscode.languages.registerCodeLensProvider(JAVA_LANGID, new DebugCodeLensProvider()); + } else { + this.hoverProvider = initializeHoverProvider(); } this.configurationEvent = vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { @@ -60,6 +64,13 @@ class DebugCodeLensContainer implements vscode.Disposable { 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); } @@ -68,6 +79,9 @@ class DebugCodeLensContainer implements vscode.Disposable { if (this.lensProvider !== undefined) { this.lensProvider.dispose(); } + if (this.hoverProvider) { + this.hoverProvider.dispose(); + } this.runCommand.dispose(); this.debugCommand.dispose(); this.configurationEvent.dispose(); diff --git a/src/hoverProvider.ts b/src/hoverProvider.ts new file mode 100644 index 00000000..b72ca881 --- /dev/null +++ b/src/hoverProvider.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { CancellationToken, Command, Disposable, Hover, HoverProvider, languages, MarkdownString, Position, ProviderResult, TextDocument, + Uri, window } from "vscode"; +import { instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper"; +import { JAVA_LANGID } from "./constants"; +import { startDebugging } from "./debugCodeLensProvider"; +import { resolveElementAtSelection } from "./languageServerPlugin"; + +const JAVA_HOVER_RUN_COMMAND = "java.debug.runHover"; +const MAIN_METHOD_REGEX = /^(public|static|final|synchronized|\s+){4,}void\s+main\s*\(\s*String\s*\[\s*\]\s*\w+\s*\)\s*($|\{)/; + +export function initializeHoverProvider(): Disposable { + return new DebugHoverProvider(); +} + +class DebugHoverProvider implements Disposable { + private runHoverCommand: Disposable; + private hoverProvider: Disposable | undefined; + + constructor() { + this.runHoverCommand = instrumentOperationAsVsCodeCommand(JAVA_HOVER_RUN_COMMAND, async (noDebug: boolean, uri: string, position: any) => { + const element = await resolveElementAtSelection(uri, position.line, position.character); + if (element && element.hasMainMethod) { + startDebugging(element.declaringType, element.projectName, Uri.parse(uri), noDebug); + } else { + window.showErrorMessage("The hovered element is not a main method."); + } + }); + this.hoverProvider = languages.registerHoverProvider(JAVA_LANGID, new InternalDebugHoverProvider()); + } + + public dispose() { + if (this.runHoverCommand) { + this.runHoverCommand.dispose(); + } + + if (this.hoverProvider) { + this.hoverProvider.dispose(); + } + } +} + +class InternalDebugHoverProvider implements HoverProvider { + public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + const range = document.getWordRangeAtPosition(position, /\w+/); + if (!range || document.getText(range) !== "main") { + return; + } + + const line = document.lineAt(position); + if (MAIN_METHOD_REGEX.test(line.text.trim()) && this.isMainMethod(line.text.trim())) { + const commands: Command[] = [ + { + title: "Run", + command: JAVA_HOVER_RUN_COMMAND, + tooltip: "Run Java Program", + arguments: [ true, document.uri.toString(), { line: position.line, character: position.character }], + }, + { + title: "Debug", + command: JAVA_HOVER_RUN_COMMAND, + tooltip: "Debug Java Program", + arguments: [ false, document.uri.toString(), { line: position.line, character: position.character }], + }, + ]; + const contributed = new MarkdownString(commands.map((command) => this.convertCommandToMarkdown(command)).join(" | ")); + contributed.isTrusted = true; + return new Hover(contributed); + } + } + + private isMainMethod(line: string): boolean { + const modifier: string = line.substring(0, line.indexOf("main")); + const modifiers: string[] = modifier.split(/\s+/); + return modifiers.indexOf("public") >= 0 && modifiers.indexOf("static") >= 0; + } + + private convertCommandToMarkdown(command: Command): string { + return `[${command.title}](command:${command.command}?` + + `${encodeURIComponent(JSON.stringify(command.arguments || []))} "${command.tooltip || command.command}")`; + } +} diff --git a/src/languageServerPlugin.ts b/src/languageServerPlugin.ts index 23e5dfa2..9d67f0a3 100644 --- a/src/languageServerPlugin.ts +++ b/src/languageServerPlugin.ts @@ -4,8 +4,6 @@ import * as vscode from "vscode"; import * as commands from "./commands"; -import { logger, Type } from "./logger"; -import * as utility from "./utility"; export interface IMainClassOption { readonly mainClass: string; @@ -75,3 +73,7 @@ export async function detectPreviewFlag(className: string, projectName: string): }; return checkProjectSettings(className, projectName, true, expectedOptions); } + +export function resolveElementAtSelection(uri: string, line: number, character: number): Promise { + return >commands.executeJavaLanguageServerCommand(commands.JAVA_RESOLVE_ELEMENT_AT_SELECTION, uri, line, character); +}