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}
+
+ `
+ }
+}