Skip to content
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

Add LSP jar file system #4114

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
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"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Await.result(result, Duration("10min"))
Await.result(result, Duration("1min"))

I would say 10 minutes is a bit too much

}

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