diff --git a/package.json b/package.json index 8aa1be0e55..a0dfaae19d 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,18 @@ ], "main": "./dist/extension", "contributes": { + "customEditors": [ + { + "viewType": "decompiled.javaClass", + "displayName": "Decompiled Java Class File", + "selector": [ + { + "scheme": "file", + "filenamePattern": "*.class" + } + ] + } + ], "javaBuildFilePatterns": [ "^pom\\.xml$", ".*\\.gradle(\\.kts)?$" diff --git a/src/commands.ts b/src/commands.ts index 371448fee9..a48eeb16f0 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -79,9 +79,9 @@ export namespace Commands { */ export const EXECUTE_WORKSPACE_COMMAND = 'java.execute.workspaceCommand'; - /** - * Execute Workspace build (compilation) - */ + /** + * Execute Workspace build (compilation) + */ export const COMPILE_WORKSPACE = 'java.workspace.compile'; /** @@ -119,10 +119,10 @@ export namespace Commands { */ export const OPEN_FORMATTER = 'java.open.formatter.settings'; - /** - * Open a file given the URI - */ - export const OPEN_FILE = 'java.open.file'; + /** + * Open a file given the URI + */ + export const OPEN_FILE = 'java.open.file'; /** * Clean the Java language server workspace @@ -130,7 +130,7 @@ export namespace Commands { export const CLEAN_WORKSPACE = 'java.clean.workspace'; /** * Update the source attachment for the selected class file - * client-side & server-side commands + * client-side & server-side commands */ export const UPDATE_SOURCE_ATTACHMENT_CMD = 'java.project.updateSourceAttachment.command'; export const UPDATE_SOURCE_ATTACHMENT = 'java.project.updateSourceAttachment'; @@ -140,25 +140,25 @@ export namespace Commands { export const RESOLVE_SOURCE_ATTACHMENT = 'java.project.resolveSourceAttachment'; /** * Mark the folder as the source root of the closest project. - * client-side & server-side commands + * client-side & server-side commands */ export const ADD_TO_SOURCEPATH_CMD = 'java.project.addToSourcePath.command'; export const ADD_TO_SOURCEPATH = 'java.project.addToSourcePath'; /** * Unmark the folder as the source root of the project. - * client-side & server-side commands + * client-side & server-side commands */ export const REMOVE_FROM_SOURCEPATH_CMD = 'java.project.removeFromSourcePath.command'; export const REMOVE_FROM_SOURCEPATH = 'java.project.removeFromSourcePath'; /** * List all recognized source roots in the workspace. - * client-side & server-side commands + * client-side & server-side commands */ export const LIST_SOURCEPATHS_CMD = 'java.project.listSourcePaths.command'; export const LIST_SOURCEPATHS = 'java.project.listSourcePaths'; /** * Import new projects - * client-side & server-side commands + * client-side & server-side commands */ export const IMPORT_PROJECTS_CMD = 'java.project.import.command'; export const IMPORT_PROJECTS = 'java.project.import'; @@ -170,7 +170,7 @@ export namespace Commands { * Generate hashCode() and equals(). */ export const HASHCODE_EQUALS_PROMPT = 'java.action.hashCodeEqualsPrompt'; - /** + /** * Open settings.json */ export const OPEN_JSON_SETTINGS = 'workbench.action.openSettingsJson'; @@ -182,10 +182,10 @@ export namespace Commands { * Organize imports silently. */ export const ORGANIZE_IMPORTS_SILENTLY = "java.edit.organizeImports"; - /** - * Handle a paste event. - */ - export const HANDLE_PASTE_EVENT = "java.edit.handlePasteEvent"; + /** + * Handle a paste event. + */ + export const HANDLE_PASTE_EVENT = "java.edit.handlePasteEvent"; /** * Custom paste action (triggers auto-import) */ @@ -320,4 +320,10 @@ export namespace Commands { * Clean everything in the shared index directory. */ export const CLEAN_SHARED_INDEXES = "java.clean.sharedIndexes"; + + /** + * Get the uri of the decompiled class file. + */ + export const GET_DECOMPILED_SOURCE = "java.decompile"; + } diff --git a/src/extension.ts b/src/extension.ts index 6c223c455f..5fbe033e47 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,6 +26,7 @@ import { runtimeStatusBarProvider } from './runtimeStatusBarProvider'; import { serverStatusBarProvider } from './serverStatusBarProvider'; import { ACTIVE_BUILD_TOOL_STATE, cleanWorkspaceFileName, getJavaServerMode, handleTextBlockClosing, onConfigurationChange, ServerMode } from './settings'; import { snippetCompletionProvider } from './snippetCompletionProvider'; +import { JavaClassEditorProvider } from './javaClassEditor'; import { StandardLanguageClient } from './standardLanguageClient'; import { SyntaxLanguageClient } from './syntaxLanguageClient'; import { convertToGlob, deleteDirectory, ensureExists, getBuildFilePatterns, getExclusionBlob, getInclusionPatternsFromNegatedExclusion, getJavaConfig, getJavaConfiguration, hasBuildToolConflicts } from './utils'; @@ -191,7 +192,7 @@ export function activate(context: ExtensionContext): Promise { range: client.code2ProtocolConverter.asRange(range), context: await client.code2ProtocolConverter.asCodeActionContext(context) }; - const showAt = getJavaConfiguration().get("quickfix.showAt"); + const showAt = getJavaConfiguration().get("quickfix.showAt"); if (showAt === 'line' && range.start.line === range.end.line && range.start.character === range.end.character) { const textLine = document.lineAt(params.range.start.line); if (textLine !== null) { @@ -362,6 +363,9 @@ export function activate(context: ExtensionContext): Promise { context.subscriptions.push(serverStatusBarProvider); context.subscriptions.push(runtimeStatusBarProvider); + const classEditorProviderRegistration = window.registerCustomEditorProvider(JavaClassEditorProvider.viewType, new JavaClassEditorProvider(context)); + context.subscriptions.push(classEditorProviderRegistration); + registerClientProviders(context, { contentProviderEvent: jdtEventEmitter.event }); apiManager.getApiInstance().onDidServerModeChange((event: ServerMode) => { @@ -448,7 +452,7 @@ async function workspaceContainsBuildFiles(): Promise { const inclusionPatterns: string[] = getBuildFilePatterns(); const inclusionPatternsFromNegatedExclusion: string[] = getInclusionPatternsFromNegatedExclusion(); if (inclusionPatterns.length > 0 && inclusionPatternsFromNegatedExclusion.length > 0 && - (await workspace.findFiles(convertToGlob(inclusionPatterns, inclusionPatternsFromNegatedExclusion), null, 1 /* maxResults */)).length > 0) { + (await workspace.findFiles(convertToGlob(inclusionPatterns, inclusionPatternsFromNegatedExclusion), null, 1 /* maxResults */)).length > 0) { return true; } @@ -485,7 +489,7 @@ async function ensureNoBuildToolConflicts(context: ExtensionContext, clientOptio clientOptions.initializationOptions.settings.java.import.maven.enabled = false; context.workspaceState.update(ACTIVE_BUILD_TOOL_STATE, "gradle"); } else { - throw new Error (`Unknown build tool: ${activeBuildTool}`); // unreachable + throw new Error(`Unknown build tool: ${activeBuildTool}`); // unreachable } } @@ -699,7 +703,7 @@ function openLogFile(logFile, openingFailureWarning: string, column: ViewColumn if (!doc) { return false; } - return window.showTextDocument(doc, {viewColumn: column, preview: false}) + return window.showTextDocument(doc, { viewColumn: column, preview: false }) .then(editor => !!editor); }, () => false) .then(didOpen => { @@ -937,7 +941,7 @@ async function cleanJavaWorkspaceStorage() { } } }); - } + } } function registerOutOfMemoryDetection(storagePath: string) { diff --git a/src/javaClassEditor.ts b/src/javaClassEditor.ts new file mode 100644 index 0000000000..053593c25f --- /dev/null +++ b/src/javaClassEditor.ts @@ -0,0 +1,67 @@ +import path = require('path'); +import * as vscode from 'vscode'; +import { Uri, window, ExtensionContext} from "vscode"; +import { getNonce } from "./webviewUtils"; + +class JavaClassDocument implements vscode.CustomDocument { + constructor(uri: Uri) { this.uri = uri; } + uri: Uri; + dispose(): void { } +} + +export class JavaClassEditorProvider implements vscode.CustomReadonlyEditorProvider { + + private context: ExtensionContext; + + openCustomDocument(uri: Uri, openContext: vscode.CustomDocumentOpenContext, token: vscode.CancellationToken): JavaClassDocument { + return new JavaClassDocument(uri); + } + + constructor (context: ExtensionContext) { + this.context = context; + } + + public static readonly viewType = 'decompiled.javaClass'; + + async resolveCustomEditor(document: vscode.CustomDocument, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken): Promise { + const nonce: string = getNonce(); + webviewPanel.webview.options = { + enableScripts: true, + localResourceRoots: [Uri.joinPath(Uri.parse(this.context.extensionPath), 'webview-resources')] + }; + const classUri = Uri.parse((document.uri.toString()).replace(/^file/, "class")); + const styleUri = Uri.file( + path.join(this.context.extensionPath, 'webview-resources', 'button.css') + ); + const style: string = ``; + webviewPanel.webview.html = ` + + + + ${style} + + +
+

This file is not displayed in the text editor because it is a Java class file. Click here to decompile and open.

+ +
+ + + + `; + webviewPanel.webview.onDidReceiveMessage(message => { + switch (message.command) { + case 'decompiled': + webviewPanel.dispose(); + window.showTextDocument(classUri, { preview: false }); + return; + } + }, undefined, this.context.subscriptions); + } +} \ No newline at end of file diff --git a/src/providerDispatcher.ts b/src/providerDispatcher.ts index 0b6b0e2cec..f5526ee077 100644 --- a/src/providerDispatcher.ts +++ b/src/providerDispatcher.ts @@ -28,10 +28,13 @@ export function registerClientProviders(context: ExtensionContext, options: Prov const jdtProvider = createJDTContentProvider(options); context.subscriptions.push(workspace.registerTextDocumentContentProvider('jdt', jdtProvider)); + const classProvider = createClassContentProvider(options); + context.subscriptions.push(workspace.registerTextDocumentContentProvider('class', classProvider)); + overwriteWorkspaceSymbolProvider(context); return { - handles: [hoverProvider, symbolProvider, jdtProvider] + handles: [hoverProvider, symbolProvider, jdtProvider, classProvider] }; } @@ -81,6 +84,27 @@ function createJDTContentProvider(options: ProviderOptions): TextDocumentContent }; } +function createClassContentProvider(options: ProviderOptions): TextDocumentContentProvider { + return { + onDidChange: options.contentProviderEvent, + provideTextDocumentContent: async (uri: Uri, token: CancellationToken): Promise => { + const languageClient: LanguageClient | undefined = await getActiveLanguageClient(); + + if (!languageClient) { + return ''; + } + const originalUri = uri.toString().replace(/^class/, "file"); + const decompiledContent: string = await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.GET_DECOMPILED_SOURCE, originalUri); + if (!decompiledContent) { + console.log(`Error while getting decompiled source : ${originalUri}`); + return "Error while getting decompiled source."; + } else { + return decompiledContent; + } + } + }; +} + function createDocumentSymbolProvider(): DocumentSymbolProvider { return { provideDocumentSymbols: async (document: TextDocument, token: CancellationToken): Promise => { @@ -109,7 +133,7 @@ function createDocumentSymbolProvider(): DocumentSymbolProvider { const START_OF_DOCUMENT = new Range(new Position(0, 0), new Position(0, 0)); -function createWorkspaceSymbolProvider(existingWorkspaceSymbolProvider: WorkspaceSymbolProvider): WorkspaceSymbolProvider { +function createWorkspaceSymbolProvider(existingWorkspaceSymbolProvider: WorkspaceSymbolProvider): WorkspaceSymbolProvider { return { provideWorkspaceSymbols: async (query: string, token: CancellationToken) => { // This is a workaround until vscode add support for qualified symbol search which is tracked by @@ -159,9 +183,9 @@ function createWorkspaceSymbolProvider(existingWorkspaceSymbolProvider: Workspac } function overwriteWorkspaceSymbolProvider(context: ExtensionContext): void { - const disposable = apiManager.getApiInstance().onDidServerModeChange( async (mode) => { + const disposable = apiManager.getApiInstance().onDidServerModeChange(async (mode) => { if (mode === ServerMode.standard) { - const feature = (await getActiveLanguageClient()).getFeature(WorkspaceSymbolRequest.method); + const feature = (await getActiveLanguageClient()).getFeature(WorkspaceSymbolRequest.method); const providers = feature.getProviders(); if (providers && providers.length > 0) { feature.dispose(); @@ -186,7 +210,7 @@ const REPLACE_JDT_LINKS_PATTERN: RegExp = /(\[(?:[^\]])+\]\()(jdt:\/\/(?:(?:(?:\ * @returns the hover with all jdt:// links replaced with a command:// link that opens the jdt URI */ function fixJdtSchemeHoverLinks(hover: Hover): Hover { - const newContents: (MarkedString|MarkdownString)[] = []; + const newContents: (MarkedString | MarkdownString)[] = []; for (const content of hover.contents) { if (content instanceof MarkdownString) { const newContent: string = (content).value.replace(REPLACE_JDT_LINKS_PATTERN, (_substring, group1, group2) => { diff --git a/webview-resources/button.css b/webview-resources/button.css new file mode 100644 index 0000000000..54e437988d --- /dev/null +++ b/webview-resources/button.css @@ -0,0 +1,34 @@ +/* https://css-tricks.com/overriding-default-button-styles/ */ + +button { + display: inline-block; + border: none; + padding: 12px 16px; + margin: 0; + text-decoration: none; + background: var(--vscode-button-background); + color: #ffffff; + font-family: sans-serif; + font-size: 14px; + cursor: pointer; + text-align: center; +} + +button:hover, +button:focus { + background: var(--vscode-button-hoverBackground); +} +button:focus { + outline: 1px solid var(--vscode-button-hoverBackground); + /* outline-offset: -4px; */ +} + +.center { + text-align: center; + margin: 0; + position: absolute; + top: 50%; + left: 50%; + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} \ No newline at end of file