diff --git a/package-lock.json b/package-lock.json index b47f075..fccd806 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,9 +49,9 @@ "dev": true }, "@types/vscode": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.47.0.tgz", - "integrity": "sha512-nJA37ykkz9FYA0ZOQUSc3OZnhuzEW2vUhUEo4MiduUo82jGwwcLfyvmgd/Q7b0WrZAAceojGhZybg319L24bTA==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.49.0.tgz", + "integrity": "sha512-wfNQmLmm1VdMBr6iuNdprWmC1YdrgZ9dQzadv+l2eSjJlElOdJw8OTm4RU4oGTBcfvG6RZI2jOcppkdSS18mZw==", "dev": true }, "@webassemblyjs/ast": { diff --git a/package.json b/package.json index f64de63..d06e465 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "debugger" ], "engines": { - "vscode": "^1.47.3" + "vscode": "^1.49.0" }, "license": "SEE LICENSE IN LICENSE.txt", "repository": { @@ -745,7 +745,7 @@ "@types/lodash": "^4.14.137", "@types/mocha": "^5.2.7", "@types/node": "^8.10.51", - "@types/vscode": "1.47.0", + "@types/vscode": "1.49.0", "cross-env": "^5.2.0", "gulp": "^4.0.2", "gulp-tslint": "^8.1.4", diff --git a/src/commands.ts b/src/commands.ts index 5741ff3..f8dbeda 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -42,6 +42,8 @@ export const JAVA_FETCH_PLATFORM_SETTINGS = "vscode.java.fetchPlatformSettings"; export const JAVA_RESOLVE_CLASSFILTERS = "vscode.java.resolveClassFilters"; +export const JAVA_RESOLVE_SOURCE_URI = "vscode.java.resolveSourceUri"; + export function executeJavaLanguageServerCommand(...rest) { return executeJavaExtensionCommand(JAVA_EXECUTE_WORKSPACE_COMMAND, ...rest); } diff --git a/src/extension.ts b/src/extension.ts index 4f9df99..1525d5a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,7 @@ import { IMainClassOption, IMainMethod, resolveMainClass, resolveMainMethod } fr import { logger, Type } from "./logger"; import { mainClassPicker } from "./mainClassPicker"; import { pickJavaProcess } from "./processPicker"; +import { JavaTerminalLinkProvder } from "./terminalLinkProvider"; import { initializeThreadOperations } from "./threadOperations"; import * as utility from "./utility"; @@ -34,6 +35,7 @@ function initializeExtension(operationId: string, context: vscode.ExtensionConte registerDebugEventListener(context); context.subscriptions.push(logger); + 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 () => { diff --git a/src/languageServerPlugin.ts b/src/languageServerPlugin.ts index 3c542aa..65ce452 100644 --- a/src/languageServerPlugin.ts +++ b/src/languageServerPlugin.ts @@ -112,3 +112,7 @@ export function fetchPlatformSettings(): any { export async function resolveClassFilters(patterns: string[]): Promise { return await commands.executeJavaLanguageServerCommand(commands.JAVA_RESOLVE_CLASSFILTERS, ...patterns); } + +export async function resolveSourceUri(line: string): Promise { + return await commands.executeJavaLanguageServerCommand(commands.JAVA_RESOLVE_SOURCE_URI, line); +} diff --git a/src/terminalLinkProvider.ts b/src/terminalLinkProvider.ts new file mode 100644 index 0000000..3aae20b --- /dev/null +++ b/src/terminalLinkProvider.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { CancellationToken, commands, Position, ProviderResult, Range, TerminalLink, TerminalLinkContext, + TerminalLinkProvider, Uri, window } from "vscode"; +import { resolveSourceUri } from "./languageServerPlugin"; + +export class JavaTerminalLinkProvder implements TerminalLinkProvider { + /** + * Provide terminal links for the given context. Note that this can be called multiple times + * even before previous calls resolve, make sure to not share global objects (eg. `RegExp`) + * that could have problems when asynchronous usage may overlap. + * @param context Information about what links are being provided for. + * @param token A cancellation token. + * @return A list of terminal links for the given line. + */ + public provideTerminalLinks(context: TerminalLinkContext, token: CancellationToken): ProviderResult { + if (context.terminal.name !== "Java Debug Console" && context.terminal.name !== "Java Process Console") { + return []; + } + + const regex = new RegExp("(\\sat\\s+)([\\w$\\.]+\\/)?(([\\w$]+\\.)+[<\\w$>]+)\\(([\\w-$]+\\.java:\\d+)\\)"); + const result: RegExpExecArray = regex.exec(context.line); + if (result && result.length) { + const stackTrace = `${result[2] || ""}${result[3]}(${result[5]})`; + const sourceLineNumber = Number(result[5].split(":")[1]); + return [{ + startIndex: result.index + result[1].length, + length: stackTrace.length, + methodName: result[3], + stackTrace, + lineNumber: sourceLineNumber, + }]; + } + } + + /** + * Handle an activated terminal link. + */ + public async handleTerminalLink(link: IJavaTerminalLink): Promise { + const uri = await resolveSourceUri(link.stackTrace); + if (uri) { + const lineNumber = Math.max(link.lineNumber - 1, 0); + window.showTextDocument(Uri.parse(uri), { + preserveFocus: true, + selection: new Range(new Position(lineNumber, 0), new Position(lineNumber, 0)), + }); + } else { + // If no source is found, then open the searching symbols quickpick box. + const fullyQualifiedName = link.methodName.substring(0, link.methodName.lastIndexOf(".")); + const className = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf(".") + 1); + commands.executeCommand("workbench.action.quickOpen", "#" + className); + } + } +} + +interface IJavaTerminalLink extends TerminalLink { + methodName: string; + stackTrace: string; + lineNumber: number; +}