Skip to content

Decompile .tasty files with VS Code Extension #5513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 3, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion vscode-dotty/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"vscode": "^1.27.1"
},
"categories": [
"Languages"
"Programming Languages"
],
"keywords": [
"scala",
Expand All @@ -38,6 +38,15 @@
"aliases": [
"Scala"
]
},
{
"id": "tasty",
"extensions": [
".tasty"
],
"aliases": [
"Tasty"
]
}
],
"configuration": {
Expand Down
13 changes: 12 additions & 1 deletion vscode-dotty/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import * as compareVersions from 'compare-versions'

import { ChildProcess } from "child_process"

import { ExtensionContext } from 'vscode'
import { ExtensionContext, Disposable } from 'vscode'
import * as vscode from 'vscode'
import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn,
ServerOptions } from 'vscode-languageclient'
import { enableOldServerWorkaround } from './compat'
import * as features from './features'
import { DecompiledDocumentProvider } from './tasty-decompiler'

export let client: LanguageClient

Expand Down Expand Up @@ -333,8 +334,18 @@ function run(serverOptions: ServerOptions, isOldServer: boolean) {
revealOutputChannelOn: RevealOutputChannelOn.Never
}

// register DecompiledDocumentProvider for Tasty decompiler results
const provider = new DecompiledDocumentProvider()

const providerRegistration = Disposable.from(
vscode.workspace.registerTextDocumentContentProvider(DecompiledDocumentProvider.scheme, provider)
);

extensionContext.subscriptions.push(providerRegistration, provider)

client = new LanguageClient(extensionName, "Dotty", serverOptions, clientOptions)
client.registerFeature(new features.WorksheetRunFeature(client))
client.registerFeature(new features.TastyDecompilerFeature(client, provider))

if (isOldServer)
enableOldServerWorkaround(client)
Expand Down
45 changes: 44 additions & 1 deletion vscode-dotty/src/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { generateUuid } from 'vscode-languageclient/lib/utils/uuid'
import { DocumentSelector } from 'vscode-languageserver-protocol'
import { Disposable } from 'vscode-jsonrpc'

import { WorksheetRunRequest } from './protocol'
import { WorksheetRunRequest, TastyDecompileRequest } from './protocol'
import { WorksheetProvider } from './worksheet'
import { TastyDecompilerProvider, DecompiledDocumentProvider } from './tasty-decompiler'

// Remove this if
// https://github.com/Microsoft/vscode-languageserver-node/issues/423 is fixed.
Expand Down Expand Up @@ -60,3 +61,45 @@ export class WorksheetRunFeature extends TextDocumentFeature<TextDocumentRegistr
return new WorksheetProvider(client, options.documentSelector!)
}
}

export interface TastyDecompilerServerCapabilities {
/**
* The server provides support for decompiling Tasty files.
*/
tastyDecompiler?: boolean
}

export interface TastyDecompilerClientCapabilities {
tasty?: {
decompile?: {
dynamicRegistration?: boolean
}
}
}

export class TastyDecompilerFeature extends TextDocumentFeature<TextDocumentRegistrationOptions> {
constructor(client: BaseLanguageClient, readonly provider: DecompiledDocumentProvider) {
super(client, TastyDecompileRequest.type);
}

fillClientCapabilities(capabilities: ClientCapabilities & TastyDecompilerClientCapabilities): void {
ensure(ensure(capabilities, "tasty")!, "decompile")!.dynamicRegistration = true
}

initialize(capabilities: ServerCapabilities & TastyDecompilerServerCapabilities, documentSelector: DocumentSelector): void {
if (!capabilities.tastyDecompiler) {
return
}

const selector: DocumentSelector = [ { language: 'tasty' } ]
this.register(this.messages, {
id: generateUuid(),
registerOptions: { documentSelector: selector }
})
}

protected registerLanguageProvider(options: TextDocumentRegistrationOptions): vscode.Disposable {
let client = this._client
return new TastyDecompilerProvider(client, options.documentSelector!, this.provider)
}
}
26 changes: 25 additions & 1 deletion vscode-dotty/src/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from 'vscode'
import { RequestType, NotificationType } from 'vscode-jsonrpc'
import { VersionedTextDocumentIdentifier } from 'vscode-languageserver-protocol'
import { VersionedTextDocumentIdentifier, TextDocumentIdentifier } from 'vscode-languageserver-protocol'

import { client } from './extension'

Expand All @@ -21,6 +21,18 @@ export interface WorksheetPublishOutputParams {
content: string
}

/** The parameters for the `tasty/decompile` request. */
export interface TastyDecompileParams {
textDocument: TextDocumentIdentifier
}

/** The result of the `tasty/decompile` request */
export interface TastyDecompileResult {
tastyTree: string
scala: string
error: number
}

// TODO: Can be removed once https://github.com/Microsoft/vscode-languageserver-node/pull/421
// is merged.
export function asVersionedTextDocumentIdentifier(textDocument: vscode.TextDocument): VersionedTextDocumentIdentifier {
Expand All @@ -36,6 +48,13 @@ export function asWorksheetRunParams(textDocument: vscode.TextDocument): Workshe
}
}


export function asTastyDecompileParams(textDocument: vscode.TextDocument): TastyDecompileParams {
return {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(textDocument)
}
}

/** The `worksheet/run` request */
export namespace WorksheetRunRequest {
export const type = new RequestType<WorksheetRunParams, WorksheetRunResult, void, void>("worksheet/run")
Expand All @@ -45,3 +64,8 @@ export namespace WorksheetRunRequest {
export namespace WorksheetPublishOutputNotification {
export const type = new NotificationType<WorksheetPublishOutputParams, void>("worksheet/publishOutput")
}

/** The `tasty/decompile` request */
export namespace TastyDecompileRequest {
export const type = new RequestType<TastyDecompileParams, TastyDecompileResult, void, void>("tasty/decompile")
}
212 changes: 212 additions & 0 deletions vscode-dotty/src/tasty-decompiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import * as vscode from 'vscode'
import * as path from 'path'
import { CancellationTokenSource, ProgressLocation } from 'vscode'
import { TastyDecompileRequest, TastyDecompileResult,
asTastyDecompileParams, } from './protocol'
import { BaseLanguageClient } from 'vscode-languageclient'
import { Disposable } from 'vscode-jsonrpc'

const RESULT_OK = 0
const ERROR_TASTY_VERSION = 1
const ERROR_CLASS_NOT_FOUND = 2
const ERROR_OTHER = -1

export class TastyDecompilerProvider implements Disposable {
private disposables: Disposable[] = []

constructor(
readonly client: BaseLanguageClient,
readonly documentSelector: vscode.DocumentSelector,
readonly provider: DecompiledDocumentProvider) {
this.disposables.push(
vscode.workspace.onDidOpenTextDocument( textDocument => {
if (this.isTasty(textDocument)) {
this.requestDecompile(textDocument).then( decompileResult => {
switch(decompileResult.error) {
case RESULT_OK:
let scalaDocument = provider.makeScalaDocument(textDocument, decompileResult.scala)

vscode.workspace.openTextDocument(scalaDocument).then(doc => {
vscode.window.showTextDocument(doc, 1)
});

let fileName = textDocument.fileName.substring(textDocument.fileName.lastIndexOf(path.sep) + 1)

TastyTreeView.create(fileName, decompileResult.tastyTree)
break;
case ERROR_TASTY_VERSION:
vscode.window.showErrorMessage("Tasty file has unexpected signature.")
break;
case ERROR_CLASS_NOT_FOUND:
vscode.window.showErrorMessage("The class file related to this TASTy file could not be found.")
break;
case ERROR_OTHER:
vscode.window.showErrorMessage("A decompilation error has occurred.")
break;
default:
vscode.window.showErrorMessage("Unknown Error.")
break;
}
})
}
})
)
}

dispose(): void {
this.disposables.forEach(d => d.dispose())
this.disposables = []
}

/**
* Request the TASTy in `textDocument` to be decompiled
*/
private requestDecompile(textDocument: vscode.TextDocument): Promise<TastyDecompileResult> {
const requestParams = asTastyDecompileParams(textDocument)
const canceller = new CancellationTokenSource()
const token = canceller.token

return new Promise<TastyDecompileResult>(resolve => {
resolve(vscode.window.withProgress({
location: ProgressLocation.Notification,
title: "Decompiling"
}, () => this.client.sendRequest(TastyDecompileRequest.type, requestParams, token)
))
}).then( decompileResult => {
canceller.dispose()
return decompileResult
});
}

/** Is this document a tasty file? */
private isTasty(document: vscode.TextDocument): boolean {
return vscode.languages.match(this.documentSelector, document) > 0
}

}

/**
* Provider of virtual, read-only, scala documents
*/
export class DecompiledDocumentProvider implements vscode.TextDocumentContentProvider {
static scheme = 'decompiled';

private _documents = new Map<string, string>();
private _subscriptions: vscode.Disposable;

constructor() {
// Don't keep closed documents in memory
this._subscriptions = vscode.workspace.onDidCloseTextDocument(doc => this._documents.delete(doc.uri.toString()));
}

dispose() {
this._subscriptions.dispose();
this._documents.clear();
}

provideTextDocumentContent(uri: vscode.Uri): string {
let document = this._documents.get(uri.toString());
if (document) {
return document;
} else {
return 'Failed to load result.';
}
}

/**
* Creates a new virtual document ready to be provided and opened.
*
* @param textDocument The document containing the TASTy that was decompiled
* @param content The source code provided by the language server
*/
makeScalaDocument(textDocument: vscode.TextDocument, content: string): vscode.Uri {
let scalaDocument = textDocument.uri.with({
scheme: DecompiledDocumentProvider.scheme,
path: textDocument.uri.path.replace(".tasty", ".scala")
})
this._documents.set(scalaDocument.toString(), content);
return scalaDocument;
}
}

/**
* WebView used as container for preformatted TASTy trees
*/
class TastyTreeView {
public static readonly viewType = 'tastyTree';

private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];

/**
* Create new panel for a TASTy tree in a new column or column 2 if none is currently open
*
* @param title The panel's title
* @param content The panel's preformatted content
*/
public static create(title: string, content: string) {
const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;

const panel = vscode.window.createWebviewPanel(TastyTreeView.viewType, "Tasty Tree", (column || vscode.ViewColumn.One) + 1, {});

new TastyTreeView(panel, title, content);
}

private constructor(
panel: vscode.WebviewPanel,
title: string,
content: string
) {
this._panel = panel;
this.setContent(title, content)

// Listen for when the panel is disposed
// This happens when the user closes the panel or when the panel is closed programmatically
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
}

public dispose() {
this._panel.dispose();

while (this._disposables.length) {
const x = this._disposables.pop();
if (x) {
x.dispose();
}
}
}

private setContent(name: string, content: string) {
this._panel.title = name;
this._panel.webview.html = this._getHtmlForWebview(content);
}

private _getHtmlForWebview(content: string) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<style>
pre {
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
}
span.name {
color:magenta;
}
span.tree {
color:yellow;
}
span.length {
color:cyan;
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tasty Tree</title>
</head>
<body>
<pre>
${content}</pre>
</body>
</html>`;
}
}