Skip to content

Commit 15ffcc8

Browse files
author
Tobias Bordenca
committed
Add feature to decompile .tasty files to vscode extension
1 parent 2b64b6c commit 15ffcc8

File tree

5 files changed

+303
-4
lines changed

5 files changed

+303
-4
lines changed

vscode-dotty/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"vscode": "^1.27.1"
1515
},
1616
"categories": [
17-
"Languages"
17+
"Programming Languages"
1818
],
1919
"keywords": [
2020
"scala",
@@ -38,6 +38,15 @@
3838
"aliases": [
3939
"Scala"
4040
]
41+
},
42+
{
43+
"id": "tasty",
44+
"extensions": [
45+
".tasty"
46+
],
47+
"aliases": [
48+
"Tasty"
49+
]
4150
}
4251
],
4352
"configuration": {

vscode-dotty/src/extension.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import * as compareVersions from 'compare-versions'
66

77
import { ChildProcess } from "child_process"
88

9-
import { ExtensionContext } from 'vscode'
9+
import { ExtensionContext, Disposable } from 'vscode'
1010
import * as vscode from 'vscode'
1111
import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn,
1212
ServerOptions } from 'vscode-languageclient'
1313
import { enableOldServerWorkaround } from './compat'
1414
import * as features from './features'
15+
import { DecompiledDocumentProvider } from './tasty-decompiler'
1516

1617
export let client: LanguageClient
1718

@@ -333,8 +334,18 @@ function run(serverOptions: ServerOptions, isOldServer: boolean) {
333334
revealOutputChannelOn: RevealOutputChannelOn.Never
334335
}
335336

337+
// register DecompiledDocumentProvider for Tasty decompiler results
338+
const provider = new DecompiledDocumentProvider()
339+
340+
const providerRegistration = Disposable.from(
341+
vscode.workspace.registerTextDocumentContentProvider(DecompiledDocumentProvider.scheme, provider)
342+
);
343+
344+
extensionContext.subscriptions.push(providerRegistration, provider)
345+
336346
client = new LanguageClient(extensionName, "Dotty", serverOptions, clientOptions)
337347
client.registerFeature(new features.WorksheetRunFeature(client))
348+
client.registerFeature(new features.TastyDecompilerFeature(client, provider))
338349

339350
if (isOldServer)
340351
enableOldServerWorkaround(client)

vscode-dotty/src/features.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { generateUuid } from 'vscode-languageclient/lib/utils/uuid'
77
import { DocumentSelector } from 'vscode-languageserver-protocol'
88
import { Disposable } from 'vscode-jsonrpc'
99

10-
import { WorksheetRunRequest } from './protocol'
10+
import { WorksheetRunRequest, TastyDecompileRequest } from './protocol'
1111
import { WorksheetProvider } from './worksheet'
12+
import { TastyDecompilerProvider, DecompiledDocumentProvider } from './tasty-decompiler'
1213

1314
// Remove this if
1415
// https://github.com/Microsoft/vscode-languageserver-node/issues/423 is fixed.
@@ -60,3 +61,45 @@ export class WorksheetRunFeature extends TextDocumentFeature<TextDocumentRegistr
6061
return new WorksheetProvider(client, options.documentSelector!)
6162
}
6263
}
64+
65+
export interface TastyDecompilerServerCapabilities {
66+
/**
67+
* The server provides support for decompiling Tasty files.
68+
*/
69+
tastyDecompiler?: boolean
70+
}
71+
72+
export interface TastyDecompilerClientCapabilities {
73+
tasty?: {
74+
decompile?: {
75+
dynamicRegistration?: boolean
76+
}
77+
}
78+
}
79+
80+
export class TastyDecompilerFeature extends TextDocumentFeature<TextDocumentRegistrationOptions> {
81+
constructor(client: BaseLanguageClient, readonly provider: DecompiledDocumentProvider) {
82+
super(client, TastyDecompileRequest.type);
83+
}
84+
85+
fillClientCapabilities(capabilities: ClientCapabilities & TastyDecompilerClientCapabilities): void {
86+
ensure(ensure(capabilities, "tasty")!, "decompile")!.dynamicRegistration = true
87+
}
88+
89+
initialize(capabilities: ServerCapabilities & TastyDecompilerServerCapabilities, documentSelector: DocumentSelector): void {
90+
if (!capabilities.tastyDecompiler) {
91+
return
92+
}
93+
94+
const selector: DocumentSelector = [ { language: 'tasty' } ]
95+
this.register(this.messages, {
96+
id: generateUuid(),
97+
registerOptions: { documentSelector: selector }
98+
})
99+
}
100+
101+
protected registerLanguageProvider(options: TextDocumentRegistrationOptions): vscode.Disposable {
102+
let client = this._client
103+
return new TastyDecompilerProvider(client, options.documentSelector!, this.provider)
104+
}
105+
}

vscode-dotty/src/protocol.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode'
22
import { RequestType, NotificationType } from 'vscode-jsonrpc'
3-
import { VersionedTextDocumentIdentifier } from 'vscode-languageserver-protocol'
3+
import { VersionedTextDocumentIdentifier, TextDocumentIdentifier } from 'vscode-languageserver-protocol'
44

55
import { client } from './extension'
66

@@ -21,6 +21,18 @@ export interface WorksheetPublishOutputParams {
2121
content: string
2222
}
2323

24+
/** The parameters for the `tasty/decompile` request. */
25+
export interface TastyDecompileParams {
26+
textDocument: TextDocumentIdentifier
27+
}
28+
29+
/** The result of the `tasty/decompile` request */
30+
export interface TastyDecompileResult {
31+
tastyTree: string
32+
scala: string
33+
error: number
34+
}
35+
2436
// TODO: Can be removed once https://github.com/Microsoft/vscode-languageserver-node/pull/421
2537
// is merged.
2638
export function asVersionedTextDocumentIdentifier(textDocument: vscode.TextDocument): VersionedTextDocumentIdentifier {
@@ -36,6 +48,13 @@ export function asWorksheetRunParams(textDocument: vscode.TextDocument): Workshe
3648
}
3749
}
3850

51+
52+
export function asTastyDecompileParams(textDocument: vscode.TextDocument): TastyDecompileParams {
53+
return {
54+
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(textDocument)
55+
}
56+
}
57+
3958
/** The `worksheet/run` request */
4059
export namespace WorksheetRunRequest {
4160
export const type = new RequestType<WorksheetRunParams, WorksheetRunResult, void, void>("worksheet/run")
@@ -45,3 +64,8 @@ export namespace WorksheetRunRequest {
4564
export namespace WorksheetPublishOutputNotification {
4665
export const type = new NotificationType<WorksheetPublishOutputParams, void>("worksheet/publishOutput")
4766
}
67+
68+
/** The `tasty/decompile` request */
69+
export namespace TastyDecompileRequest {
70+
export const type = new RequestType<TastyDecompileParams, TastyDecompileResult, void, void>("tasty/decompile")
71+
}

vscode-dotty/src/tasty-decompiler.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import * as vscode from 'vscode'
2+
import * as path from 'path'
3+
import { CancellationTokenSource, ProgressLocation } from 'vscode'
4+
import { TastyDecompileRequest, TastyDecompileResult,
5+
asTastyDecompileParams, } from './protocol'
6+
import { BaseLanguageClient } from 'vscode-languageclient'
7+
import { Disposable } from 'vscode-jsonrpc'
8+
9+
const RESULT_OK = 0
10+
const ERROR_TASTY_VERSION = 1
11+
const ERROR_CLASS_NOT_FOUND = 2
12+
const ERROR_OTHER = -1
13+
14+
export class TastyDecompilerProvider implements Disposable {
15+
private disposables: Disposable[] = []
16+
17+
constructor(
18+
readonly client: BaseLanguageClient,
19+
readonly documentSelector: vscode.DocumentSelector,
20+
readonly provider: DecompiledDocumentProvider) {
21+
this.disposables.push(
22+
vscode.workspace.onDidOpenTextDocument( textDocument => {
23+
if (this.isTasty(textDocument)) {
24+
this.requestDecompile(textDocument).then( decompileResult => {
25+
switch(decompileResult.error) {
26+
case RESULT_OK:
27+
let scalaDocument = provider.makeScalaDocument(textDocument, decompileResult.scala)
28+
29+
vscode.workspace.openTextDocument(scalaDocument).then(doc => {
30+
vscode.window.showTextDocument(doc, 1)
31+
});
32+
33+
let fileName = textDocument.fileName.substring(textDocument.fileName.lastIndexOf(path.sep) + 1)
34+
35+
TastyTreeView.create(fileName, decompileResult.tastyTree)
36+
break;
37+
case ERROR_TASTY_VERSION:
38+
vscode.window.showErrorMessage("Tasty file has unexpected signature.")
39+
break;
40+
case ERROR_CLASS_NOT_FOUND:
41+
vscode.window.showErrorMessage("The class file related to this TASTy file could not be found.")
42+
break;
43+
case ERROR_OTHER:
44+
vscode.window.showErrorMessage("A decompilation error has occurred.")
45+
break;
46+
default:
47+
vscode.window.showErrorMessage("Unknown Error.")
48+
break;
49+
}
50+
})
51+
}
52+
})
53+
)
54+
}
55+
56+
dispose(): void {
57+
this.disposables.forEach(d => d.dispose())
58+
this.disposables = []
59+
}
60+
61+
/**
62+
* Request the TASTy in `textDocument` to be decompiled
63+
*/
64+
private requestDecompile(textDocument: vscode.TextDocument): Promise<TastyDecompileResult> {
65+
const requestParams = asTastyDecompileParams(textDocument)
66+
const canceller = new CancellationTokenSource()
67+
const token = canceller.token
68+
69+
return new Promise<TastyDecompileResult>(resolve => {
70+
resolve(vscode.window.withProgress({
71+
location: ProgressLocation.Notification,
72+
title: "Decompiling"
73+
}, () => this.client.sendRequest(TastyDecompileRequest.type, requestParams, token)
74+
))
75+
}).then( decompileResult => {
76+
canceller.dispose()
77+
return decompileResult
78+
});
79+
}
80+
81+
/** Is this document a tasty file? */
82+
private isTasty(document: vscode.TextDocument): boolean {
83+
return vscode.languages.match(this.documentSelector, document) > 0
84+
}
85+
86+
}
87+
88+
/**
89+
* Provider of virtual, read-only, scala documents
90+
*/
91+
export class DecompiledDocumentProvider implements vscode.TextDocumentContentProvider {
92+
static scheme = 'decompiled';
93+
94+
private _documents = new Map<string, string>();
95+
private _subscriptions: vscode.Disposable;
96+
97+
constructor() {
98+
// Don't keep closed documents in memory
99+
this._subscriptions = vscode.workspace.onDidCloseTextDocument(doc => this._documents.delete(doc.uri.toString()));
100+
}
101+
102+
dispose() {
103+
this._subscriptions.dispose();
104+
this._documents.clear();
105+
}
106+
107+
provideTextDocumentContent(uri: vscode.Uri): string {
108+
let document = this._documents.get(uri.toString());
109+
if (document) {
110+
return document;
111+
} else {
112+
return 'Failed to load result.';
113+
}
114+
}
115+
116+
/**
117+
* Creates a new virtual document ready to be provided and opened.
118+
*
119+
* @param textDocument The document containing the TASTy that was decompiled
120+
* @param content The source code provided by the language server
121+
*/
122+
makeScalaDocument(textDocument: vscode.TextDocument, content: string): vscode.Uri {
123+
let scalaDocument = textDocument.uri.with({
124+
scheme: DecompiledDocumentProvider.scheme,
125+
path: textDocument.uri.path.replace(".tasty", ".scala")
126+
})
127+
this._documents.set(scalaDocument.toString(), content);
128+
return scalaDocument;
129+
}
130+
}
131+
132+
/**
133+
* WebView used as container for preformatted TASTy trees
134+
*/
135+
class TastyTreeView {
136+
public static readonly viewType = 'tastyTree';
137+
138+
private readonly _panel: vscode.WebviewPanel;
139+
private _disposables: vscode.Disposable[] = [];
140+
141+
/**
142+
* Create new panel for a TASTy tree in a new column or column 2 if none is currently open
143+
*
144+
* @param title The panel's title
145+
* @param content The panel's preformatted content
146+
*/
147+
public static create(title: string, content: string) {
148+
const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
149+
150+
const panel = vscode.window.createWebviewPanel(TastyTreeView.viewType, "Tasty Tree", (column || vscode.ViewColumn.One) + 1, {});
151+
152+
new TastyTreeView(panel, title, content);
153+
}
154+
155+
private constructor(
156+
panel: vscode.WebviewPanel,
157+
title: string,
158+
content: string
159+
) {
160+
this._panel = panel;
161+
this.setContent(title, content)
162+
163+
// Listen for when the panel is disposed
164+
// This happens when the user closes the panel or when the panel is closed programmatically
165+
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
166+
}
167+
168+
public dispose() {
169+
this._panel.dispose();
170+
171+
while (this._disposables.length) {
172+
const x = this._disposables.pop();
173+
if (x) {
174+
x.dispose();
175+
}
176+
}
177+
}
178+
179+
private setContent(name: string, content: string) {
180+
this._panel.title = name;
181+
this._panel.webview.html = this._getHtmlForWebview(content);
182+
}
183+
184+
private _getHtmlForWebview(content: string) {
185+
return `<!DOCTYPE html>
186+
<html lang="en">
187+
<head>
188+
<style>
189+
pre {
190+
font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace
191+
}
192+
span.name {
193+
color:magenta;
194+
}
195+
span.tree {
196+
color:yellow;
197+
}
198+
span.length {
199+
color:cyan;
200+
}
201+
</style>
202+
<meta charset="UTF-8">
203+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
204+
<title>Tasty Tree</title>
205+
</head>
206+
<body>
207+
<pre>
208+
${content}</pre>
209+
</body>
210+
</html>`;
211+
}
212+
}

0 commit comments

Comments
 (0)