diff --git a/compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala b/compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala index 527c15414365..7933e007bfb0 100644 --- a/compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala +++ b/compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala @@ -89,7 +89,7 @@ object PickledQuotes { val pickled = pickler.assembleParts() if (quotePickling ne noPrinter) - new TastyPrinter(pickled).printContents() + println(new TastyPrinter(pickled).printContents()) pickled } @@ -98,7 +98,7 @@ object PickledQuotes { private def unpickle(bytes: Array[Byte], splices: Seq[Any], isType: Boolean)(implicit ctx: Context): Tree = { if (quotePickling ne noPrinter) { println(i"**** unpickling quote from TASTY") - new TastyPrinter(bytes).printContents() + println(new TastyPrinter(bytes).printContents()) } val mode = if (isType) UnpickleMode.TypeTree else UnpickleMode.Term diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala new file mode 100644 index 000000000000..4366ce29f76b --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala @@ -0,0 +1,16 @@ +package dotty.tools.dotc +package core +package tasty + +import Contexts._, Decorators._ +import Names.Name +import TastyUnpickler._ +import TastyBuffer.NameRef +import util.Positions.offsetToInt +import printing.Highlighting._ + +class TastyHTMLPrinter(bytes: Array[Byte])(implicit ctx: Context) extends TastyPrinter(bytes) { + override protected def nameColor(str: String): String = s"$str" + override protected def treeColor(str: String): String = s"$str" + override protected def lengthColor(str: String): String = s"$str" +} diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala index afaecd8c0ef8..22ec8bfd8c74 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala @@ -11,6 +11,8 @@ import printing.Highlighting._ class TastyPrinter(bytes: Array[Byte])(implicit ctx: Context) { + private[this] val sb: StringBuilder = new StringBuilder + val unpickler: TastyUnpickler = new TastyUnpickler(bytes) import unpickler.{nameAtRef, unpickle} @@ -21,41 +23,56 @@ class TastyPrinter(bytes: Array[Byte])(implicit ctx: Context) { def printNames(): Unit = for ((name, idx) <- nameAtRef.contents.zipWithIndex) { val index = nameColor("%4d".format(idx)) - println(index + ": " + nameToString(name)) + sb.append(index).append(": ").append(nameToString(name)).append("\n") } - def printContents(): Unit = { - println("Names:") + def printContents(): String = { + sb.append("Names:\n") printNames() - println() - println("Trees:") - unpickle(new TreeSectionUnpickler) - unpickle(new PositionSectionUnpickler) - unpickle(new CommentSectionUnpickler) + sb.append("\n") + sb.append("Trees:\n") + unpickle(new TreeSectionUnpickler) match { + case Some(s) => sb.append(s) + case _ => Unit + } + sb.append("\n\n") + unpickle(new PositionSectionUnpickler) match { + case Some(s) => sb.append(s) + case _ => Unit + } + sb.append("\n\n") + unpickle(new CommentSectionUnpickler) match { + case Some(s) => sb.append(s) + case _ => Unit + } + sb.result } - class TreeSectionUnpickler extends SectionUnpickler[Unit](TreePickler.sectionName) { + class TreeSectionUnpickler extends SectionUnpickler[String](TreePickler.sectionName) { import TastyFormat._ - def unpickle(reader: TastyReader, tastyName: NameTable): Unit = { + + private[this] val sb: StringBuilder = new StringBuilder + + def unpickle(reader: TastyReader, tastyName: NameTable): String = { import reader._ var indent = 0 def newLine() = { val length = treeColor("%5d".format(index(currentAddr) - index(startAddr))) - print(s"\n $length:" + " " * indent) + sb.append(s"\n $length:" + " " * indent) } - def printNat() = print(Yellow(" " + readNat()).show) + def printNat() = sb.append(treeColor(" " + readNat())) def printName() = { val idx = readNat() - print(nameColor(" " + idx + " [" + nameRefToString(NameRef(idx)) + "]")) + sb.append(nameColor(" " + idx + " [" + nameRefToString(NameRef(idx)) + "]")) } def printTree(): Unit = { newLine() val tag = readByte() - print(" ");print(astTagToString(tag)) + sb.append(" ").append(astTagToString(tag)) indent += 2 if (tag >= firstLengthTreeTag) { val len = readNat() - print(s"(${lengthColor(len.toString)})") + sb.append(s"(${lengthColor(len.toString)})") val end = currentAddr + len def printTrees() = until(end)(printTree()) tag match { @@ -76,7 +93,7 @@ class TastyPrinter(bytes: Array[Byte])(implicit ctx: Context) { printTrees() } if (currentAddr != end) { - println(s"incomplete read, current = $currentAddr, end = $end") + sb.append(s"incomplete read, current = $currentAddr, end = $end\n") goto(end) } } @@ -96,42 +113,51 @@ class TastyPrinter(bytes: Array[Byte])(implicit ctx: Context) { } indent -= 2 } - println(i"start = ${reader.startAddr}, base = $base, current = $currentAddr, end = $endAddr") - println(s"${endAddr.index - startAddr.index} bytes of AST, base = $currentAddr") + sb.append(i"start = ${reader.startAddr}, base = $base, current = $currentAddr, end = $endAddr\n") + sb.append(s"${endAddr.index - startAddr.index} bytes of AST, base = $currentAddr\n") while (!isAtEnd) { printTree() newLine() } + sb.result } } - class PositionSectionUnpickler extends SectionUnpickler[Unit]("Positions") { - def unpickle(reader: TastyReader, tastyName: NameTable): Unit = { - print(s" ${reader.endAddr.index - reader.currentAddr.index}") + class PositionSectionUnpickler extends SectionUnpickler[String]("Positions") { + + private[this] val sb: StringBuilder = new StringBuilder + + def unpickle(reader: TastyReader, tastyName: NameTable): String = { + sb.append(s" ${reader.endAddr.index - reader.currentAddr.index}") val positions = new PositionUnpickler(reader).positions - println(s" position bytes:") + sb.append(s" position bytes:\n") val sorted = positions.toSeq.sortBy(_._1.index) for ((addr, pos) <- sorted) { - print(treeColor("%10d".format(addr.index))) - println(s": ${offsetToInt(pos.start)} .. ${pos.end}") + sb.append(treeColor("%10d".format(addr.index))) + sb.append(s": ${offsetToInt(pos.start)} .. ${pos.end}\n") } + sb.result } } - class CommentSectionUnpickler extends SectionUnpickler[Unit]("Comments") { - def unpickle(reader: TastyReader, tastyName: NameTable): Unit = { - print(s" ${reader.endAddr.index - reader.currentAddr.index}") + class CommentSectionUnpickler extends SectionUnpickler[String]("Comments") { + + private[this] val sb: StringBuilder = new StringBuilder + + def unpickle(reader: TastyReader, tastyName: NameTable): String = { + sb.append(s" ${reader.endAddr.index - reader.currentAddr.index}") val comments = new CommentUnpickler(reader).comments - println(s" comment bytes:") + sb.append(s" comment bytes:\n") val sorted = comments.toSeq.sortBy(_._1.index) for ((addr, cmt) <- sorted) { - print(treeColor("%10d".format(addr.index))) - println(s": ${cmt.raw} (expanded = ${cmt.isExpanded})") + sb.append(treeColor("%10d".format(addr.index))) + sb.append(s": ${cmt.raw} (expanded = ${cmt.isExpanded})\n") } + sb.result } } - private def nameColor(str: String): String = Magenta(str).show - private def treeColor(str: String): String = Yellow(str).show - private def lengthColor(str: String): String = Cyan(str).show + protected def nameColor(str: String): String = Magenta(str).show + protected def treeColor(str: String): String = Yellow(str).show + protected def lengthColor(str: String): String = Cyan(str).show } diff --git a/compiler/src/dotty/tools/dotc/decompiler/DecompilationPrinter.scala b/compiler/src/dotty/tools/dotc/decompiler/DecompilationPrinter.scala index d3d4edae533a..34acbd255d76 100644 --- a/compiler/src/dotty/tools/dotc/decompiler/DecompilationPrinter.scala +++ b/compiler/src/dotty/tools/dotc/decompiler/DecompilationPrinter.scala @@ -39,7 +39,7 @@ class DecompilationPrinter extends Phase { private def printToOutput(out: PrintStream)(implicit ctx: Context): Unit = { val unit = ctx.compilationUnit if (ctx.settings.printTasty.value) { - new TastyPrinter(unit.pickled.head._2).printContents() + println(new TastyPrinter(unit.pickled.head._2).printContents()) } else { val unitFile = unit.source.toString.replace("\\", "/").replace(".class", ".tasty") out.println(s"/** Decompiled from $unitFile */") diff --git a/compiler/src/dotty/tools/dotc/decompiler/IDEDecompilerDriver.scala b/compiler/src/dotty/tools/dotc/decompiler/IDEDecompilerDriver.scala new file mode 100644 index 000000000000..ae4541865c80 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/decompiler/IDEDecompilerDriver.scala @@ -0,0 +1,44 @@ +package dotty.tools +package dotc +package decompiler + +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core._ +import dotty.tools.dotc.core.tasty.TastyHTMLPrinter +import dotty.tools.dotc.reporting._ +import dotty.tools.dotc.tastyreflect.ReflectionImpl + +/** + * Decompiler to be used with IDEs + */ +class IDEDecompilerDriver(val settings: List[String]) extends dotc.Driver { + + private val myInitCtx: Context = { + val rootCtx = initCtx.fresh.addMode(Mode.Interactive).addMode(Mode.ReadPositions).addMode(Mode.ReadComments) + rootCtx.setSetting(rootCtx.settings.YretainTrees, true) + rootCtx.setSetting(rootCtx.settings.fromTasty, true) + val ctx = setup(settings.toArray :+ "dummy.scala", rootCtx)._2 + ctx.initialize()(ctx) + ctx + } + + private val decompiler = new PartialTASTYDecompiler + + def run(className: String): (String, String) = { + val reporter = new StoreReporter(null) with HideNonSensicalMessages + + val run = decompiler.newRun(myInitCtx.fresh.setReporter(reporter)) + + implicit val ctx = run.runContext + + run.compile(List(className)) + run.printSummary() + val unit = ctx.run.units.head + + val decompiled = new ReflectionImpl(ctx).showSourceCode.showTree(unit.tpdTree) + val tree = new TastyHTMLPrinter(unit.pickled.head._2).printContents() + + reporter.removeBufferedMessages.foreach(message => System.err.println(message)) + (tree, decompiled) + } +} diff --git a/compiler/src/dotty/tools/dotc/decompiler/PartialTASTYDecompiler.scala b/compiler/src/dotty/tools/dotc/decompiler/PartialTASTYDecompiler.scala new file mode 100644 index 000000000000..62bf158d0ef6 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/decompiler/PartialTASTYDecompiler.scala @@ -0,0 +1,11 @@ +package dotty.tools.dotc.decompiler + +import dotty.tools.dotc.core.Phases.Phase + +/** Partial TASTYDecompiler that doesn't execute the backendPhases + * allowing to control decompiler output by manually running it + * on the CompilationUnits + */ +class PartialTASTYDecompiler extends TASTYDecompiler { + override protected def backendPhases: List[List[Phase]] = Nil +} diff --git a/language-server/src/dotty/tools/languageserver/DottyClient.scala b/language-server/src/dotty/tools/languageserver/DottyClient.scala new file mode 100644 index 000000000000..c0b7c3c155ed --- /dev/null +++ b/language-server/src/dotty/tools/languageserver/DottyClient.scala @@ -0,0 +1,6 @@ +package dotty.tools.languageserver + +/** + * A `LanguageClient` that regroups all language server features + */ +trait DottyClient extends worksheet.WorksheetClient \ No newline at end of file diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index c6abf045c0db..04c0140d541c 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -26,11 +26,13 @@ import reporting._, reporting.diagnostic.{Message, MessageContainer, messages} import typer.Typer import util.{Set => _, _} import interactive._, interactive.InteractiveDriver._ +import decompiler.IDEDecompilerDriver import Interactive.Include import config.Printers.interactiv import languageserver.config.ProjectConfig -import languageserver.worksheet.{Worksheet, WorksheetClient, WorksheetService} +import languageserver.worksheet.{Worksheet, WorksheetService} +import languageserver.decompiler.{TastyDecompilerService} import lsp4j.services._ @@ -43,7 +45,7 @@ import lsp4j.services._ * - This implementation is based on the LSP4J library: https://github.com/eclipse/lsp4j */ class DottyLanguageServer extends LanguageServer - with TextDocumentService with WorkspaceService with WorksheetService { thisServer => + with TextDocumentService with WorkspaceService with WorksheetService with TastyDecompilerService { thisServer => import ast.tpd._ import DottyLanguageServer._ @@ -56,8 +58,8 @@ class DottyLanguageServer extends LanguageServer private[this] var rootUri: String = _ - private[this] var myClient: WorksheetClient = _ - def client: WorksheetClient = myClient + private[this] var myClient: DottyClient = _ + def client: DottyClient = myClient private[this] var myDrivers: mutable.Map[ProjectConfig, InteractiveDriver] = _ @@ -128,6 +130,25 @@ class DottyLanguageServer extends LanguageServer drivers(configFor(uri)) } + /** The driver instance responsible for decompiling `uri` in `classPath` */ + def decompilerDriverFor(uri: URI, classPath: String): IDEDecompilerDriver = thisServer.synchronized { + val config = configFor(uri) + val defaultFlags = List("-color:never") + + implicit class updateDeco(ss: List[String]) { + def update(pathKind: String, pathInfo: String) = { + val idx = ss.indexOf(pathKind) + val ss1 = if (idx >= 0) ss.take(idx) ++ ss.drop(idx + 2) else ss + ss1 ++ List(pathKind, pathInfo) + } + } + val settings = + defaultFlags ++ + config.compilerArguments.toList + .update("-classpath", (classPath +: config.dependencyClasspath).mkString(File.pathSeparator)) + new IDEDecompilerDriver(settings) + } + /** A mapping from project `p` to the set of projects that transitively depend on `p`. */ def dependentProjects: Map[ProjectConfig, Set[ProjectConfig]] = thisServer.synchronized { if (myDependentProjects == null) { @@ -148,7 +169,7 @@ class DottyLanguageServer extends LanguageServer myDependentProjects } - def connect(client: WorksheetClient): Unit = { + def connect(client: DottyClient): Unit = { myClient = client } @@ -184,7 +205,8 @@ class DottyLanguageServer extends LanguageServer rootUri = params.getRootUri assert(rootUri != null) - class DottyServerCapabilities(val worksheetRunProvider: Boolean = true) extends lsp4j.ServerCapabilities + class DottyServerCapabilities(val worksheetRunProvider: Boolean = true, + val tastyDecompiler: Boolean = true) extends lsp4j.ServerCapabilities val c = new DottyServerCapabilities c.setTextDocumentSync(TextDocumentSyncKind.Full) diff --git a/language-server/src/dotty/tools/languageserver/Main.scala b/language-server/src/dotty/tools/languageserver/Main.scala index 6b509ce35160..67c076bd4ad2 100644 --- a/language-server/src/dotty/tools/languageserver/Main.scala +++ b/language-server/src/dotty/tools/languageserver/Main.scala @@ -67,9 +67,9 @@ object Main { println("Starting server") val launcher = - new Launcher.Builder[worksheet.WorksheetClient]() + new Launcher.Builder[DottyClient]() .setLocalService(server) - .setRemoteInterface(classOf[worksheet.WorksheetClient]) + .setRemoteInterface(classOf[DottyClient]) .setInput(in) .setOutput(out) // For debugging JSON messages: diff --git a/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerMessages.scala b/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerMessages.scala new file mode 100644 index 000000000000..48ce3c7c4fad --- /dev/null +++ b/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerMessages.scala @@ -0,0 +1,23 @@ +package dotty.tools.languageserver.decompiler + +import org.eclipse.lsp4j.TextDocumentIdentifier + +// All case classes in this file should have zero-parameters secondary +// constructors to allow Gson to reflectively create instances on +// deserialization without relying on sun.misc.Unsafe. + +/** The parameter for the `tasty/decompile` request. */ +case class TastyDecompileParams(textDocument: TextDocumentIdentifier) { + def this() = this(null) +} + +/** The response to a `tasty/decompile` request. */ +case class TastyDecompileResult(tastyTree: String = null, scala: String = null, error: Int = 0) { + def this() = this(null, null, 0) +} + +object TastyDecompileResult { + val ErrorTastyVersion = 1 + val ErrorClassNotFound = 2 + val ErrorOther = -1 +} \ No newline at end of file diff --git a/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerService.scala b/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerService.scala new file mode 100644 index 000000000000..8494b9630712 --- /dev/null +++ b/language-server/src/dotty/tools/languageserver/decompiler/TastyDecompilerService.scala @@ -0,0 +1,42 @@ +package dotty.tools +package languageserver +package decompiler + +import java.net.URI +import java.nio.file._ +import java.util.concurrent.CompletableFuture + +import dotc.core.tasty.TastyUnpickler.UnpickleException +import dotc.fromtasty.TastyFileUtil + +import org.eclipse.lsp4j.jsonrpc.services._ + + +@JsonSegment("tasty") +trait TastyDecompilerService { + thisServer: DottyLanguageServer => + + @JsonRequest + def decompile(params: TastyDecompileParams): CompletableFuture[TastyDecompileResult] = + computeAsync(synchronize = false, fun = { cancelChecker => + val uri = new URI(params.textDocument.getUri) + try { + TastyFileUtil.getClassName(Paths.get(uri)) match { + case Some((classPath, className)) => + val driver = thisServer.decompilerDriverFor(uri, classPath) + + val (tree, source) = driver.run(className) + + TastyDecompileResult(tree, source) + case _ => + TastyDecompileResult(error = TastyDecompileResult.ErrorClassNotFound) + } + } catch { + case _: UnpickleException => + TastyDecompileResult(error = TastyDecompileResult.ErrorTastyVersion) + case t: Throwable => + t.printStackTrace() + TastyDecompileResult(error = TastyDecompileResult.ErrorOther) + } + }) +} diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala b/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala index bba69c5e2b9a..bd7275f4a0c4 100644 --- a/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala +++ b/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala @@ -1,6 +1,8 @@ package dotty.tools.languageserver.util.server -import dotty.tools.languageserver.worksheet.{WorksheetRunOutput, WorksheetClient} +import dotty.tools.languageserver.worksheet.{WorksheetRunOutput} +import dotty.tools.languageserver.DottyClient + import java.util.concurrent.CompletableFuture @@ -9,7 +11,7 @@ import org.eclipse.lsp4j.services._ import scala.collection.mutable.Buffer -class TestClient extends WorksheetClient { +class TestClient extends DottyClient { class Log[T] { private[this] val log = Buffer.empty[T] diff --git a/vscode-dotty/package.json b/vscode-dotty/package.json index 6727ba0e6339..300ad23e4e42 100644 --- a/vscode-dotty/package.json +++ b/vscode-dotty/package.json @@ -14,7 +14,7 @@ "vscode": "^1.27.1" }, "categories": [ - "Languages" + "Programming Languages" ], "keywords": [ "scala", @@ -38,6 +38,15 @@ "aliases": [ "Scala" ] + }, + { + "id": "tasty", + "extensions": [ + ".tasty" + ], + "aliases": [ + "TASTy" + ] } ], "configuration": { diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index 38f2d7ed74cc..6768ec7b2bc9 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -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 @@ -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) diff --git a/vscode-dotty/src/features.ts b/vscode-dotty/src/features.ts index 24d02c169978..a4242e7e23a0 100644 --- a/vscode-dotty/src/features.ts +++ b/vscode-dotty/src/features.ts @@ -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. @@ -60,3 +61,45 @@ export class WorksheetRunFeature extends TextDocumentFeature { + 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) + } +} \ No newline at end of file diff --git a/vscode-dotty/src/protocol.ts b/vscode-dotty/src/protocol.ts index 764385fec5b2..976849203ff5 100644 --- a/vscode-dotty/src/protocol.ts +++ b/vscode-dotty/src/protocol.ts @@ -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' @@ -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 { @@ -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("worksheet/run") @@ -45,3 +64,8 @@ export namespace WorksheetRunRequest { export namespace WorksheetPublishOutputNotification { export const type = new NotificationType("worksheet/publishOutput") } + +/** The `tasty/decompile` request */ +export namespace TastyDecompileRequest { + export const type = new RequestType("tasty/decompile") +} diff --git a/vscode-dotty/src/tasty-decompiler.ts b/vscode-dotty/src/tasty-decompiler.ts new file mode 100644 index 000000000000..23b1583a5c16 --- /dev/null +++ b/vscode-dotty/src/tasty-decompiler.ts @@ -0,0 +1,211 @@ +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 { + const requestParams = asTastyDecompileParams(textDocument) + const canceller = new CancellationTokenSource() + const token = canceller.token + + return new Promise(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() + 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 ` + + + + + + Tasty Tree + + +
+${content}
+ + ` + } +}