Skip to content

Commit

Permalink
Add LSP jar file system
Browse files Browse the repository at this point in the history
  • Loading branch information
Arthurm1 committed Jul 15, 2022
1 parent 2769719 commit 90d3236
Show file tree
Hide file tree
Showing 35 changed files with 1,233 additions and 261 deletions.
20 changes: 20 additions & 0 deletions docs/integrations/new-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ The currently available settings for `InitializationOptions` are listed below.
icons?: "vscode" | "octicons" | "atom" | "unicode";
inputBoxProvider?: boolean;
isVirtualDocumentSupported?: boolean;
isLibraryFileSystemSupported?: boolean;
isExitOnShutdown?: boolean;
isHttpEnabled?: boolean;
openFilesOnRenameProvider?: boolean;
Expand Down Expand Up @@ -327,6 +328,25 @@ Possible values:
Metals tries to fallback to `window/showMessageRequest` when possible.
- `on`: the `metals/inputBox` request is fully supported.

##### `isVirtualDocumentSupported`

Possible values:

- `off` (default): virtual documents are not supported. In this case, Metals
saves generated files to disk e.g. decompiled class files or source jar files.
- `on`: virtual documents are supported and Metals sends the content of the file
to the client rather than a URI reference to it.
It's up to the client to display that content as though it were a file.

##### `isLibraryFileSystemSupported`

Possible values:

- `off` (default): library file system is not supported.
- `on`: library file system is supported. Metals sends the
`metals-library-filesystem-ready` command when the libraries have been registered
and the library filesystem is ready to navigate.

##### `isExitOnShutdown`

Possible values:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class ServerInitializeBench {
def run(): Unit = {
val path = AbsolutePath(workspace)
val buffers = Buffers()
val client = new TestingClient(path, buffers)
val client = new TestingClient(path, () => null, buffers)
MetalsLogger.updateDefaultFormat()
val ec = ExecutionContext.fromExecutorService(ex)
val server = new MetalsLanguageServer(ec, sh = sh)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package scala.meta.internal.metals

import java.nio.file.Files
import java.nio.file.Path

import scala.collection.mutable.ListBuffer
Expand Down Expand Up @@ -154,7 +155,7 @@ class BuildTargetInfo(buildTargets: BuildTargets) {
val sourceJarName = jarName.replace(".jar", "-sources.jar")
buildTargets
.sourceJarFile(sourceJarName)
.exists(_.toFile.exists())
.exists(path => path.exists)
}

private def getSingleClassPathInfo(
Expand All @@ -164,7 +165,7 @@ class BuildTargetInfo(buildTargets: BuildTargets) {
): String = {
val filename = shortPath.toString()
val padding = " " * (maxFilenameSize - filename.size)
val status = if (path.toFile.exists) {
val status = if (Files.exists(path)) {
val blankWarning = " " * 9
if (path.toFile().isDirectory() || jarHasSource(filename))
blankWarning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ final class BuildTargets() {
def allJava: Iterator[JavaTarget] =
data.fromIterators(_.allJava)

def allJDKs: Iterator[String] =
data.fromIterators(_.allJDKs).distinct

def info(id: BuildTargetIdentifier): Option[BuildTarget] =
data.fromOptions(_.info(id))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@ object ClientCommands {
|""".stripMargin,
)

val LibraryFileSystemReady = new Command(
"metals-library-filesystem-ready",
"Library FS ready",
"""|Notifies the client that the library filesystem is ready to be navigated.
|""".stripMargin,
)

val RefreshModel = new Command(
"metals-model-refresh",
"Refresh model",
Expand Down Expand Up @@ -333,6 +340,7 @@ object ClientCommands {
FocusDiagnostics,
GotoLocation,
EchoCommand,
LibraryFileSystemReady,
RefreshModel,
ShowStacktrace,
CopyWorksheetOutput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ case class ClientConfiguration(initialConfig: MetalsServerConfig) {
def isVirtualDocumentSupported(): Boolean =
initializationOptions.isVirtualDocumentSupported.getOrElse(false)

def isLibraryFileSystemSupported(): Boolean =
initializationOptions.isLibraryFileSystemSupported.getOrElse(false)

def icons(): Icons =
initializationOptions.icons
.map(Icons.fromString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import java.nio.charset.StandardCharsets
import javax.annotation.Nullable

import scala.annotation.tailrec
import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.util.Failure
import scala.util.Properties
import scala.util.Success
Expand Down Expand Up @@ -73,6 +75,9 @@ object DecoderResponse {
def failed(uri: String, e: Throwable): DecoderResponse =
failed(uri.toString(), getAllMessages(e))

def failed(uri: URI, errorMsg: String, e: Throwable): DecoderResponse =
failed(uri, s"$errorMsg\n${getAllMessages(e)}")

def failed(uri: URI, e: Throwable): DecoderResponse =
failed(uri, getAllMessages(e))

Expand All @@ -84,6 +89,7 @@ final class FileDecoderProvider(
workspace: AbsolutePath,
compilers: Compilers,
buildTargets: BuildTargets,
uriMapper: URIMapper,
userConfig: () => UserConfiguration,
shellRunner: ShellRunner,
fileSystemSemanticdbs: FileSystemSemanticdbs,
Expand Down Expand Up @@ -126,6 +132,9 @@ final class FileDecoderProvider(
* metalsDecode:file:///somePath/someFile.scala.cfr
* metalsDecode:file:///somePath/someFile.class.cfr
*
* Auto-CFR:
* metalsfs:/metalslibraries/jar/someFile.jar/somePackage/someFile.class
*
* semanticdb:
* metalsDecode:file:///somePath/someFile.java.semanticdb-compact
* metalsDecode:file:///somePath/someFile.java.semanticdb-detailed
Expand All @@ -145,6 +154,7 @@ final class FileDecoderProvider(
*
* jar:
* metalsDecode:jar:file:///somePath/someFile-sources.jar!/somePackage/someFile.java
* jar:file:///somePath/someFile-sources.jar!/somePackage/someFile.java.semanticdb-compact
*
* build target:
* metalsDecode:file:///workspacePath/buildTargetName.metals-buildtarget
Expand All @@ -162,11 +172,13 @@ final class FileDecoderProvider(
case "file" => decodeMetalsFile(uri)
case "metalsDecode" =>
decodedFileContents(uri.getSchemeSpecificPart())
case "metalsfs" =>
decodedFileContents(uriMapper.convertToLocal(uriAsStr))
case _ =>
Future.successful(
DecoderResponse.failed(
uri,
s"Unexpected scheme ${uri.getScheme()}",
s"Unexpected scheme ${uri.getScheme()} in $uri",
)
)
}
Expand Down Expand Up @@ -246,7 +258,10 @@ final class FileDecoderProvider(
)
case _ =>
Future.successful(
DecoderResponse.failed(uri, "Unsupported extension")
DecoderResponse.failed(
uri,
s"Unsupported extension $additionalExtension in $uri",
)
)
}
}
Expand Down Expand Up @@ -304,7 +319,7 @@ final class FileDecoderProvider(
path: AbsolutePath,
format: Format,
): DecoderResponse = {
if (path.isScalaOrJava)
if (path.isScalaOrJava || path.isClassfile)
interactiveSemanticdbs
.textDocument(path)
.documentIncludingStale
Expand All @@ -316,7 +331,8 @@ final class FileDecoderProvider(
.fold(identity, identity)
)
else if (path.isSemanticdb) decodeFromSemanticDBFile(path, format)
else DecoderResponse.failed(path.toURI, "Unsupported extension")
else
DecoderResponse.failed(path.toURI, s"Unsupported extension ${path.toURI}")
}

private def isScala3(path: AbsolutePath): Boolean = {
Expand All @@ -340,7 +356,7 @@ final class FileDecoderProvider(
)
)
} else if (path.isTasty) {
findPathInfoForClassesPathFile(path) match {
findPathInfoForTastyPathFile(path) match {
case Some(pathInfo) => decodeFromTastyFile(pathInfo)
case None =>
Future.successful(
Expand Down Expand Up @@ -370,15 +386,25 @@ final class FileDecoderProvider(
PathInfo(metadata.targetId, metadata.classDir.resolve(relativePath))
})

private def findPathInfoForClassesPathFile(
private def findPathInfoForTastyPathFile(
path: AbsolutePath
): Option[PathInfo] = {
val pathInfos = for {
targetId <- buildTargets.allBuildTargetIds
classDir <- buildTargets.targetClassDirectories(targetId)
classPath = classDir.toAbsolutePath
if (path.isInside(classPath))
} yield PathInfo(targetId, path)
val pathInfos = if (path.isJarFileSystem) {
// should only ever exist in workspace jars
for {
targetId <- buildTargets.allBuildTargetIds
targetJars <- buildTargets.targetJarClasspath(targetId)
jarPath <- path.jarPath
if (targetJars.contains(jarPath))
} yield PathInfo(targetId, path)
} else {
for {
targetId <- buildTargets.allBuildTargetIds
classDir <- buildTargets.targetClassDirectories(targetId)
classPath = classDir.toAbsolutePath
if (path.isInside(classPath))
} yield PathInfo(targetId, path)
}
pathInfos.toList.headOption
}

Expand Down Expand Up @@ -511,17 +537,27 @@ final class FileDecoderProvider(
verbose: Boolean
)(path: AbsolutePath): Future[DecoderResponse] = {
try {
val defaultArgs = List("-private")
val args = if (verbose) "-verbose" :: defaultArgs else defaultArgs
val sbOut = new StringBuilder()
val sbErr = new StringBuilder()
val (classpath, parent, filename) = if (path.isJarFileSystem) {
val parent = workspace
val className = path.toString.stripPrefix("/").stripSuffix(".class")
(
path.jarPath.toList.flatMap(cp => List("-cp", cp.toString)),
parent,
className,
)
} else
(List.empty, path.parent, path.filename)
val defaultArgs = classpath ::: List("-private")
val args = if (verbose) "-verbose" :: defaultArgs else defaultArgs
shellRunner
.run(
"Decode using javap",
JavaBinary(userConfig().javaHome, "javap") :: args ::: List(
path.filename
filename
),
path.parent,
parent,
redirectErrorOutput = false,
Map.empty,
s => {
Expand All @@ -543,30 +579,45 @@ final class FileDecoderProvider(
})
} catch {
case NonFatal(e) =>
scribe.error(e.toString())
scribe.error(s"$e ${e.getStackTrace.mkString("\n at ")}")
Future.successful(DecoderResponse.failed(path.toURI, e))
}
}

private def decodeCFRFromClassFile(
def decodeCFRAndWait(path: AbsolutePath): DecoderResponse = {
val result = decodeCFRFromClassFile(path)
Await.result(result, Duration("10min"))
}

def decodeCFRFromClassFile(
path: AbsolutePath
): Future[DecoderResponse] = {
val cfrDependency = Dependency.of("org.benf", "cfr", "0.151")
val cfrMain = "org.benf.cfr.reader.Main"

// find the build target so we can use the full classpath - needed for a better decompile
// class file can be in classes output dir or could be in a jar on the build's classpath
val buildTarget = buildTargets
.inferBuildTarget(path)
.orElse(
buildTargets.allScala
.find(buildTarget =>
path.isInside(buildTarget.classDirectory.toAbsolutePath)
path.isInside(
buildTarget.classDirectory.toAbsolutePath
) || path.jarPath
.map(jar => buildTarget.fullClasspath.contains(jar.toNIO))
.getOrElse(false)
)
.map(_.id)
)
.orElse(
buildTargets.allJava
.find(buildTarget =>
path.isInside(buildTarget.classDirectory.toAbsolutePath)
path.isInside(
buildTarget.classDirectory.toAbsolutePath
) || path.jarPath
.map(jar => buildTarget.fullClasspath.contains(jar.toNIO))
.getOrElse(false)
)
.map(_.id)
)
Expand Down Expand Up @@ -597,9 +648,16 @@ final class FileDecoderProvider(
(classesPath, className)
})
.getOrElse({
val parent = path.parent
val className = path.filename
(parent, className)
if (path.isJarFileSystem) {
// if the file is in a jar then use the workspace as the working dir and the fully qualified name as the class
val parent = workspace
val className = path.toString.stripPrefix("/")
(parent, className)
} else {
val parent = path.parent
val className = path.filename
(parent, className)
}
})

val args = extraClassPath :::
Expand Down Expand Up @@ -637,15 +695,21 @@ final class FileDecoderProvider(
if (sbOut.isEmpty && sbErr.nonEmpty)
DecoderResponse.failed(
path.toURI,
s"$cfrDependency\n$cfrMain\n$parent\n$args\n${sbErr.toString}",
s"buildTarget:$buildTarget\nCFRJar:$cfrDependency\nCFRMain:$cfrMain\nParent:$parent\nArgs:$args\nError:${sbErr.toString}",
)
else
DecoderResponse.success(path.toURI, sbOut.toString)
})
} catch {
case NonFatal(e) =>
scribe.error(e.toString())
Future.successful(DecoderResponse.failed(path.toURI, e))
Future.successful(
DecoderResponse.failed(
path.toURI,
s"buildTarget:$buildTarget\nCFRJar:$cfrDependency\nCFRMain:$cfrMain\nParent:$parent\nArgs:$args\nError:${sbErr.toString}",
e,
)
)
}
}

Expand Down
Loading

0 comments on commit 90d3236

Please sign in to comment.