Skip to content

Commit 6dbd1bf

Browse files
JessicaJHeergrunber
authored andcommitted
Add support for .class files
Signed-off-by: Jessica He <[email protected]>
1 parent 5a16809 commit 6dbd1bf

File tree

6 files changed

+174
-27
lines changed

6 files changed

+174
-27
lines changed

package.json

+12
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@
5757
],
5858
"main": "./dist/extension",
5959
"contributes": {
60+
"customEditors": [
61+
{
62+
"viewType": "decompiled.javaClass",
63+
"displayName": "Decompiled Java Class File",
64+
"selector": [
65+
{
66+
"scheme": "file",
67+
"filenamePattern": "*.class"
68+
}
69+
]
70+
}
71+
],
6072
"javaBuildFilePatterns": [
6173
"^pom\\.xml$",
6274
".*\\.gradle(\\.kts)?$"

src/commands.ts

+23-17
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ export namespace Commands {
7979
*/
8080
export const EXECUTE_WORKSPACE_COMMAND = 'java.execute.workspaceCommand';
8181

82-
/**
83-
* Execute Workspace build (compilation)
84-
*/
82+
/**
83+
* Execute Workspace build (compilation)
84+
*/
8585
export const COMPILE_WORKSPACE = 'java.workspace.compile';
8686

8787
/**
@@ -119,18 +119,18 @@ export namespace Commands {
119119
*/
120120
export const OPEN_FORMATTER = 'java.open.formatter.settings';
121121

122-
/**
123-
* Open a file given the URI
124-
*/
125-
export const OPEN_FILE = 'java.open.file';
122+
/**
123+
* Open a file given the URI
124+
*/
125+
export const OPEN_FILE = 'java.open.file';
126126

127127
/**
128128
* Clean the Java language server workspace
129129
*/
130130
export const CLEAN_WORKSPACE = 'java.clean.workspace';
131131
/**
132132
* Update the source attachment for the selected class file
133-
* client-side & server-side commands
133+
* client-side & server-side commands
134134
*/
135135
export const UPDATE_SOURCE_ATTACHMENT_CMD = 'java.project.updateSourceAttachment.command';
136136
export const UPDATE_SOURCE_ATTACHMENT = 'java.project.updateSourceAttachment';
@@ -140,25 +140,25 @@ export namespace Commands {
140140
export const RESOLVE_SOURCE_ATTACHMENT = 'java.project.resolveSourceAttachment';
141141
/**
142142
* Mark the folder as the source root of the closest project.
143-
* client-side & server-side commands
143+
* client-side & server-side commands
144144
*/
145145
export const ADD_TO_SOURCEPATH_CMD = 'java.project.addToSourcePath.command';
146146
export const ADD_TO_SOURCEPATH = 'java.project.addToSourcePath';
147147
/**
148148
* Unmark the folder as the source root of the project.
149-
* client-side & server-side commands
149+
* client-side & server-side commands
150150
*/
151151
export const REMOVE_FROM_SOURCEPATH_CMD = 'java.project.removeFromSourcePath.command';
152152
export const REMOVE_FROM_SOURCEPATH = 'java.project.removeFromSourcePath';
153153
/**
154154
* List all recognized source roots in the workspace.
155-
* client-side & server-side commands
155+
* client-side & server-side commands
156156
*/
157157
export const LIST_SOURCEPATHS_CMD = 'java.project.listSourcePaths.command';
158158
export const LIST_SOURCEPATHS = 'java.project.listSourcePaths';
159159
/**
160160
* Import new projects
161-
* client-side & server-side commands
161+
* client-side & server-side commands
162162
*/
163163
export const IMPORT_PROJECTS_CMD = 'java.project.import.command';
164164
export const IMPORT_PROJECTS = 'java.project.import';
@@ -170,7 +170,7 @@ export namespace Commands {
170170
* Generate hashCode() and equals().
171171
*/
172172
export const HASHCODE_EQUALS_PROMPT = 'java.action.hashCodeEqualsPrompt';
173-
/**
173+
/**
174174
* Open settings.json
175175
*/
176176
export const OPEN_JSON_SETTINGS = 'workbench.action.openSettingsJson';
@@ -182,10 +182,10 @@ export namespace Commands {
182182
* Organize imports silently.
183183
*/
184184
export const ORGANIZE_IMPORTS_SILENTLY = "java.edit.organizeImports";
185-
/**
186-
* Handle a paste event.
187-
*/
188-
export const HANDLE_PASTE_EVENT = "java.edit.handlePasteEvent";
185+
/**
186+
* Handle a paste event.
187+
*/
188+
export const HANDLE_PASTE_EVENT = "java.edit.handlePasteEvent";
189189
/**
190190
* Custom paste action (triggers auto-import)
191191
*/
@@ -320,4 +320,10 @@ export namespace Commands {
320320
* Clean everything in the shared index directory.
321321
*/
322322
export const CLEAN_SHARED_INDEXES = "java.clean.sharedIndexes";
323+
324+
/**
325+
* Get the uri of the decompiled class file.
326+
*/
327+
export const GET_DECOMPILED_SOURCE = "java.decompile";
328+
323329
}

src/extension.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { runtimeStatusBarProvider } from './runtimeStatusBarProvider';
2626
import { serverStatusBarProvider } from './serverStatusBarProvider';
2727
import { ACTIVE_BUILD_TOOL_STATE, cleanWorkspaceFileName, getJavaServerMode, handleTextBlockClosing, onConfigurationChange, ServerMode } from './settings';
2828
import { snippetCompletionProvider } from './snippetCompletionProvider';
29+
import { JavaClassEditorProvider } from './javaClassEditor';
2930
import { StandardLanguageClient } from './standardLanguageClient';
3031
import { SyntaxLanguageClient } from './syntaxLanguageClient';
3132
import { convertToGlob, deleteDirectory, ensureExists, getBuildFilePatterns, getExclusionBlob, getInclusionPatternsFromNegatedExclusion, getJavaConfig, getJavaConfiguration, hasBuildToolConflicts } from './utils';
@@ -191,7 +192,7 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
191192
range: client.code2ProtocolConverter.asRange(range),
192193
context: await client.code2ProtocolConverter.asCodeActionContext(context)
193194
};
194-
const showAt = getJavaConfiguration().get<string>("quickfix.showAt");
195+
const showAt = getJavaConfiguration().get<string>("quickfix.showAt");
195196
if (showAt === 'line' && range.start.line === range.end.line && range.start.character === range.end.character) {
196197
const textLine = document.lineAt(params.range.start.line);
197198
if (textLine !== null) {
@@ -362,6 +363,9 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
362363
context.subscriptions.push(serverStatusBarProvider);
363364
context.subscriptions.push(runtimeStatusBarProvider);
364365

366+
const classEditorProviderRegistration = window.registerCustomEditorProvider(JavaClassEditorProvider.viewType, new JavaClassEditorProvider(context));
367+
context.subscriptions.push(classEditorProviderRegistration);
368+
365369
registerClientProviders(context, { contentProviderEvent: jdtEventEmitter.event });
366370

367371
apiManager.getApiInstance().onDidServerModeChange((event: ServerMode) => {
@@ -448,7 +452,7 @@ async function workspaceContainsBuildFiles(): Promise<boolean> {
448452
const inclusionPatterns: string[] = getBuildFilePatterns();
449453
const inclusionPatternsFromNegatedExclusion: string[] = getInclusionPatternsFromNegatedExclusion();
450454
if (inclusionPatterns.length > 0 && inclusionPatternsFromNegatedExclusion.length > 0 &&
451-
(await workspace.findFiles(convertToGlob(inclusionPatterns, inclusionPatternsFromNegatedExclusion), null, 1 /* maxResults */)).length > 0) {
455+
(await workspace.findFiles(convertToGlob(inclusionPatterns, inclusionPatternsFromNegatedExclusion), null, 1 /* maxResults */)).length > 0) {
452456
return true;
453457
}
454458

@@ -485,7 +489,7 @@ async function ensureNoBuildToolConflicts(context: ExtensionContext, clientOptio
485489
clientOptions.initializationOptions.settings.java.import.maven.enabled = false;
486490
context.workspaceState.update(ACTIVE_BUILD_TOOL_STATE, "gradle");
487491
} else {
488-
throw new Error (`Unknown build tool: ${activeBuildTool}`); // unreachable
492+
throw new Error(`Unknown build tool: ${activeBuildTool}`); // unreachable
489493
}
490494
}
491495

@@ -699,7 +703,7 @@ function openLogFile(logFile, openingFailureWarning: string, column: ViewColumn
699703
if (!doc) {
700704
return false;
701705
}
702-
return window.showTextDocument(doc, {viewColumn: column, preview: false})
706+
return window.showTextDocument(doc, { viewColumn: column, preview: false })
703707
.then(editor => !!editor);
704708
}, () => false)
705709
.then(didOpen => {
@@ -937,7 +941,7 @@ async function cleanJavaWorkspaceStorage() {
937941
}
938942
}
939943
});
940-
}
944+
}
941945
}
942946

943947
function registerOutOfMemoryDetection(storagePath: string) {

src/javaClassEditor.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import path = require('path');
2+
import * as vscode from 'vscode';
3+
import { Uri, window, ExtensionContext} from "vscode";
4+
import { getNonce } from "./webviewUtils";
5+
6+
class JavaClassDocument implements vscode.CustomDocument {
7+
constructor(uri: Uri) { this.uri = uri; }
8+
uri: Uri;
9+
dispose(): void { }
10+
}
11+
12+
export class JavaClassEditorProvider implements vscode.CustomReadonlyEditorProvider {
13+
14+
private context: ExtensionContext;
15+
16+
openCustomDocument(uri: Uri, openContext: vscode.CustomDocumentOpenContext, token: vscode.CancellationToken): JavaClassDocument {
17+
return new JavaClassDocument(uri);
18+
}
19+
20+
constructor (context: ExtensionContext) {
21+
this.context = context;
22+
}
23+
24+
public static readonly viewType = 'decompiled.javaClass';
25+
26+
async resolveCustomEditor(document: vscode.CustomDocument, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken): Promise<void> {
27+
const nonce: string = getNonce();
28+
webviewPanel.webview.options = {
29+
enableScripts: true,
30+
localResourceRoots: [Uri.joinPath(Uri.parse(this.context.extensionPath), 'webview-resources')]
31+
};
32+
const classUri = Uri.parse((document.uri.toString()).replace(/^file/, "class"));
33+
const styleUri = Uri.file(
34+
path.join(this.context.extensionPath, 'webview-resources', 'button.css')
35+
);
36+
const style: string = `<link rel="stylesheet" type="text/css" href="${webviewPanel.webview.asWebviewUri(styleUri).toString()}">`;
37+
webviewPanel.webview.html = `
38+
<html lang="en">
39+
<head>
40+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${webviewPanel.webview.cspSource};">
41+
${style}
42+
</head>
43+
<body>
44+
<div class="center">
45+
<p>This file is not displayed in the text editor because it is a Java class file. Click here to decompile and open.</p>
46+
<button id="btn"><center>Decompile Class File</center></button>
47+
<div>
48+
<script nonce="${nonce}">
49+
const vscode = acquireVsCodeApi();
50+
document.getElementById("btn").addEventListener("click", decompiled);
51+
function decompiled() {
52+
vscode.postMessage({ command: 'decompiled' });
53+
}
54+
</script>
55+
</body>
56+
</html>
57+
`;
58+
webviewPanel.webview.onDidReceiveMessage(message => {
59+
switch (message.command) {
60+
case 'decompiled':
61+
webviewPanel.dispose();
62+
window.showTextDocument(classUri, { preview: false });
63+
return;
64+
}
65+
}, undefined, this.context.subscriptions);
66+
}
67+
}

src/providerDispatcher.ts

+29-5
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ export function registerClientProviders(context: ExtensionContext, options: Prov
2828
const jdtProvider = createJDTContentProvider(options);
2929
context.subscriptions.push(workspace.registerTextDocumentContentProvider('jdt', jdtProvider));
3030

31+
const classProvider = createClassContentProvider(options);
32+
context.subscriptions.push(workspace.registerTextDocumentContentProvider('class', classProvider));
33+
3134
overwriteWorkspaceSymbolProvider(context);
3235

3336
return {
34-
handles: [hoverProvider, symbolProvider, jdtProvider]
37+
handles: [hoverProvider, symbolProvider, jdtProvider, classProvider]
3538
};
3639
}
3740

@@ -81,6 +84,27 @@ function createJDTContentProvider(options: ProviderOptions): TextDocumentContent
8184
};
8285
}
8386

87+
function createClassContentProvider(options: ProviderOptions): TextDocumentContentProvider {
88+
return <TextDocumentContentProvider>{
89+
onDidChange: options.contentProviderEvent,
90+
provideTextDocumentContent: async (uri: Uri, token: CancellationToken): Promise<string> => {
91+
const languageClient: LanguageClient | undefined = await getActiveLanguageClient();
92+
93+
if (!languageClient) {
94+
return '';
95+
}
96+
const originalUri = uri.toString().replace(/^class/, "file");
97+
const decompiledContent: string = await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.GET_DECOMPILED_SOURCE, originalUri);
98+
if (!decompiledContent) {
99+
console.log(`Error while getting decompiled source : ${originalUri}`);
100+
return "Error while getting decompiled source.";
101+
} else {
102+
return decompiledContent;
103+
}
104+
}
105+
};
106+
}
107+
84108
function createDocumentSymbolProvider(): DocumentSymbolProvider {
85109
return <DocumentSymbolProvider>{
86110
provideDocumentSymbols: async (document: TextDocument, token: CancellationToken): Promise<SymbolInformation[] | DocumentSymbol[]> => {
@@ -109,7 +133,7 @@ function createDocumentSymbolProvider(): DocumentSymbolProvider {
109133

110134
const START_OF_DOCUMENT = new Range(new Position(0, 0), new Position(0, 0));
111135

112-
function createWorkspaceSymbolProvider(existingWorkspaceSymbolProvider: WorkspaceSymbolProvider): WorkspaceSymbolProvider {
136+
function createWorkspaceSymbolProvider(existingWorkspaceSymbolProvider: WorkspaceSymbolProvider): WorkspaceSymbolProvider {
113137
return {
114138
provideWorkspaceSymbols: async (query: string, token: CancellationToken) => {
115139
// This is a workaround until vscode add support for qualified symbol search which is tracked by
@@ -159,9 +183,9 @@ function createWorkspaceSymbolProvider(existingWorkspaceSymbolProvider: Workspac
159183
}
160184

161185
function overwriteWorkspaceSymbolProvider(context: ExtensionContext): void {
162-
const disposable = apiManager.getApiInstance().onDidServerModeChange( async (mode) => {
186+
const disposable = apiManager.getApiInstance().onDidServerModeChange(async (mode) => {
163187
if (mode === ServerMode.standard) {
164-
const feature = (await getActiveLanguageClient()).getFeature(WorkspaceSymbolRequest.method);
188+
const feature = (await getActiveLanguageClient()).getFeature(WorkspaceSymbolRequest.method);
165189
const providers = feature.getProviders();
166190
if (providers && providers.length > 0) {
167191
feature.dispose();
@@ -186,7 +210,7 @@ const REPLACE_JDT_LINKS_PATTERN: RegExp = /(\[(?:[^\]])+\]\()(jdt:\/\/(?:(?:(?:\
186210
* @returns the hover with all jdt:// links replaced with a command:// link that opens the jdt URI
187211
*/
188212
function fixJdtSchemeHoverLinks(hover: Hover): Hover {
189-
const newContents: (MarkedString|MarkdownString)[] = [];
213+
const newContents: (MarkedString | MarkdownString)[] = [];
190214
for (const content of hover.contents) {
191215
if (content instanceof MarkdownString) {
192216
const newContent: string = (<MarkdownString>content).value.replace(REPLACE_JDT_LINKS_PATTERN, (_substring, group1, group2) => {

webview-resources/button.css

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* https://css-tricks.com/overriding-default-button-styles/ */
2+
3+
button {
4+
display: inline-block;
5+
border: none;
6+
padding: 12px 16px;
7+
margin: 0;
8+
text-decoration: none;
9+
background: var(--vscode-button-background);
10+
color: #ffffff;
11+
font-family: sans-serif;
12+
font-size: 14px;
13+
cursor: pointer;
14+
text-align: center;
15+
}
16+
17+
button:hover,
18+
button:focus {
19+
background: var(--vscode-button-hoverBackground);
20+
}
21+
button:focus {
22+
outline: 1px solid var(--vscode-button-hoverBackground);
23+
/* outline-offset: -4px; */
24+
}
25+
26+
.center {
27+
text-align: center;
28+
margin: 0;
29+
position: absolute;
30+
top: 50%;
31+
left: 50%;
32+
-ms-transform: translate(-50%, -50%);
33+
transform: translate(-50%, -50%);
34+
}

0 commit comments

Comments
 (0)