Skip to content

Commit 7158836

Browse files
committed
Add LSP jar file system
1 parent 4209432 commit 7158836

35 files changed

+1224
-262
lines changed

docs/integrations/new-editor.md

+20
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ The currently available settings for `InitializationOptions` are listed below.
122122
icons?: "vscode" | "octicons" | "atom" | "unicode";
123123
inputBoxProvider?: boolean;
124124
isVirtualDocumentSupported?: boolean;
125+
isLibraryFileSystemSupported?: boolean;
125126
isExitOnShutdown?: boolean;
126127
isHttpEnabled?: boolean;
127128
openFilesOnRenameProvider?: boolean;
@@ -327,6 +328,25 @@ Possible values:
327328
Metals tries to fallback to `window/showMessageRequest` when possible.
328329
- `on`: the `metals/inputBox` request is fully supported.
329330

331+
##### `isVirtualDocumentSupported`
332+
333+
Possible values:
334+
335+
- `off` (default): virtual documents are not supported. In this case, Metals
336+
saves generated files to disk e.g. decompiled class files or source jar files.
337+
- `on`: virtual documents are supported and Metals sends the content of the file
338+
to the client rather than a URI reference to it.
339+
It's up to the client to display that content as though it were a file.
340+
341+
##### `isLibraryFileSystemSupported`
342+
343+
Possible values:
344+
345+
- `off` (default): library file system is not supported.
346+
- `on`: library file system is supported. Metals sends the
347+
`metals-library-filesystem-ready` command when the libraries have been registered
348+
and the library filesystem is ready to navigate.
349+
330350
##### `isExitOnShutdown`
331351

332352
Possible values:

metals-bench/src/main/scala/bench/ServerInitializeBench.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class ServerInitializeBench {
5252
def run(): Unit = {
5353
val path = AbsolutePath(workspace)
5454
val buffers = Buffers()
55-
val client = new TestingClient(path, buffers)
55+
val client = new TestingClient(path, () => null, buffers)
5656
MetalsLogger.updateDefaultFormat()
5757
val ec = ExecutionContext.fromExecutorService(ex)
5858
val server = new MetalsLanguageServer(ec, sh = sh)

metals/src/main/scala/scala/meta/internal/metals/BuildTargetInfo.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package scala.meta.internal.metals
22

3+
import java.nio.file.Files
34
import java.nio.file.Path
45

56
import scala.collection.mutable.ListBuffer
@@ -154,7 +155,7 @@ class BuildTargetInfo(buildTargets: BuildTargets) {
154155
val sourceJarName = jarName.replace(".jar", "-sources.jar")
155156
buildTargets
156157
.sourceJarFile(sourceJarName)
157-
.exists(_.toFile.exists())
158+
.exists(path => path.exists)
158159
}
159160

160161
private def getSingleClassPathInfo(
@@ -164,7 +165,7 @@ class BuildTargetInfo(buildTargets: BuildTargets) {
164165
): String = {
165166
val filename = shortPath.toString()
166167
val padding = " " * (maxFilenameSize - filename.size)
167-
val status = if (path.toFile.exists) {
168+
val status = if (Files.exists(path)) {
168169
val blankWarning = " " * 9
169170
if (path.toFile().isDirectory() || jarHasSource(filename))
170171
blankWarning

metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ final class BuildTargets() {
9393
def allJava: Iterator[JavaTarget] =
9494
data.fromIterators(_.allJava)
9595

96+
def allJDKs: Iterator[String] =
97+
data.fromIterators(_.allJDKs).distinct
98+
9699
def info(id: BuildTargetIdentifier): Option[BuildTarget] =
97100
data.fromOptions(_.info(id))
98101

metals/src/main/scala/scala/meta/internal/metals/ClientCommands.scala

+8
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,13 @@ object ClientCommands {
249249
|""".stripMargin
250250
)
251251

252+
val LibraryFileSystemReady = new Command(
253+
"metals-library-filesystem-ready",
254+
"Library FS ready",
255+
"""|Notifies the client that the library filesystem is ready to be navigated.
256+
|""".stripMargin
257+
)
258+
252259
val RefreshModel = new Command(
253260
"metals-model-refresh",
254261
"Refresh model",
@@ -333,6 +340,7 @@ object ClientCommands {
333340
FocusDiagnostics,
334341
GotoLocation,
335342
EchoCommand,
343+
LibraryFileSystemReady,
336344
RefreshModel,
337345
ShowStacktrace,
338346
CopyWorksheetOutput,

metals/src/main/scala/scala/meta/internal/metals/ClientConfiguration.scala

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ case class ClientConfiguration(initialConfig: MetalsServerConfig) {
5757
def isVirtualDocumentSupported(): Boolean =
5858
initializationOptions.isVirtualDocumentSupported.getOrElse(false)
5959

60+
def isLibraryFileSystemSupported(): Boolean =
61+
initializationOptions.isLibraryFileSystemSupported.getOrElse(false)
62+
6063
def icons(): Icons =
6164
initializationOptions.icons
6265
.map(Icons.fromString)

metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala

+89-25
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import java.nio.charset.StandardCharsets
99
import javax.annotation.Nullable
1010

1111
import scala.annotation.tailrec
12+
import scala.concurrent.Await
1213
import scala.concurrent.ExecutionContext
1314
import scala.concurrent.Future
15+
import scala.concurrent.duration.Duration
1416
import scala.util.Failure
1517
import scala.util.Properties
1618
import scala.util.Success
@@ -73,6 +75,9 @@ object DecoderResponse {
7375
def failed(uri: String, e: Throwable): DecoderResponse =
7476
failed(uri.toString(), getAllMessages(e))
7577

78+
def failed(uri: URI, errorMsg: String, e: Throwable): DecoderResponse =
79+
failed(uri, s"$errorMsg\n${getAllMessages(e)}")
80+
7681
def failed(uri: URI, e: Throwable): DecoderResponse =
7782
failed(uri, getAllMessages(e))
7883

@@ -84,6 +89,7 @@ final class FileDecoderProvider(
8489
workspace: AbsolutePath,
8590
compilers: Compilers,
8691
buildTargets: BuildTargets,
92+
uriMapper: URIMapper,
8793
userConfig: () => UserConfiguration,
8894
shellRunner: ShellRunner,
8995
fileSystemSemanticdbs: FileSystemSemanticdbs,
@@ -126,6 +132,9 @@ final class FileDecoderProvider(
126132
* metalsDecode:file:///somePath/someFile.scala.cfr
127133
* metalsDecode:file:///somePath/someFile.class.cfr
128134
*
135+
* Auto-CFR:
136+
* metalsfs:/metalslibraries/jar/someFile.jar/somePackage/someFile.class
137+
*
129138
* semanticdb:
130139
* metalsDecode:file:///somePath/someFile.java.semanticdb-compact
131140
* metalsDecode:file:///somePath/someFile.java.semanticdb-detailed
@@ -145,6 +154,7 @@ final class FileDecoderProvider(
145154
*
146155
* jar:
147156
* metalsDecode:jar:file:///somePath/someFile-sources.jar!/somePackage/someFile.java
157+
* jar:file:///somePath/someFile-sources.jar!/somePackage/someFile.java.semanticdb-compact
148158
*
149159
* build target:
150160
* metalsDecode:file:///workspacePath/buildTargetName.metals-buildtarget
@@ -162,11 +172,13 @@ final class FileDecoderProvider(
162172
case "file" => decodeMetalsFile(uri)
163173
case "metalsDecode" =>
164174
decodedFileContents(uri.getSchemeSpecificPart())
175+
case "metalsfs" =>
176+
decodedFileContents(uriMapper.convertToLocal(uriAsStr))
165177
case _ =>
166178
Future.successful(
167179
DecoderResponse.failed(
168180
uri,
169-
s"Unexpected scheme ${uri.getScheme()}"
181+
s"Unexpected scheme ${uri.getScheme()} in $uri"
170182
)
171183
)
172184
}
@@ -246,7 +258,10 @@ final class FileDecoderProvider(
246258
)
247259
case _ =>
248260
Future.successful(
249-
DecoderResponse.failed(uri, "Unsupported extension")
261+
DecoderResponse.failed(
262+
uri,
263+
s"Unsupported extension $additionalExtension in $uri"
264+
)
250265
)
251266
}
252267
}
@@ -304,7 +319,7 @@ final class FileDecoderProvider(
304319
path: AbsolutePath,
305320
format: Format
306321
): DecoderResponse = {
307-
if (path.isScalaOrJava)
322+
if (path.isScalaOrJava || path.isClassfile)
308323
interactiveSemanticdbs
309324
.textDocument(path)
310325
.documentIncludingStale
@@ -316,7 +331,8 @@ final class FileDecoderProvider(
316331
.fold(identity, identity)
317332
)
318333
else if (path.isSemanticdb) decodeFromSemanticDBFile(path, format)
319-
else DecoderResponse.failed(path.toURI, "Unsupported extension")
334+
else
335+
DecoderResponse.failed(path.toURI, s"Unsupported extension ${path.toURI}")
320336
}
321337

322338
private def isScala3(path: AbsolutePath): Boolean = {
@@ -340,7 +356,7 @@ final class FileDecoderProvider(
340356
)
341357
)
342358
} else if (path.isTasty) {
343-
findPathInfoForClassesPathFile(path) match {
359+
findPathInfoForTastyPathFile(path) match {
344360
case Some(pathInfo) => decodeFromTastyFile(pathInfo)
345361
case None =>
346362
Future.successful(
@@ -370,15 +386,25 @@ final class FileDecoderProvider(
370386
PathInfo(metadata.targetId, metadata.classDir.resolve(relativePath))
371387
})
372388

373-
private def findPathInfoForClassesPathFile(
389+
private def findPathInfoForTastyPathFile(
374390
path: AbsolutePath
375391
): Option[PathInfo] = {
376-
val pathInfos = for {
377-
targetId <- buildTargets.allBuildTargetIds
378-
classDir <- buildTargets.targetClassDirectories(targetId)
379-
classPath = classDir.toAbsolutePath
380-
if (path.isInside(classPath))
381-
} yield PathInfo(targetId, path)
392+
val pathInfos = if (path.isJarFileSystem) {
393+
// should only ever exist in workspace jars
394+
for {
395+
targetId <- buildTargets.allBuildTargetIds
396+
targetJars <- buildTargets.targetJarClasspath(targetId)
397+
jarPath <- path.jarPath
398+
if (targetJars.contains(jarPath))
399+
} yield PathInfo(targetId, path)
400+
} else {
401+
for {
402+
targetId <- buildTargets.allBuildTargetIds
403+
classDir <- buildTargets.targetClassDirectories(targetId)
404+
classPath = classDir.toAbsolutePath
405+
if (path.isInside(classPath))
406+
} yield PathInfo(targetId, path)
407+
}
382408
pathInfos.toList.headOption
383409
}
384410

@@ -511,17 +537,27 @@ final class FileDecoderProvider(
511537
verbose: Boolean
512538
)(path: AbsolutePath): Future[DecoderResponse] = {
513539
try {
514-
val defaultArgs = List("-private")
515-
val args = if (verbose) "-verbose" :: defaultArgs else defaultArgs
516540
val sbOut = new StringBuilder()
517541
val sbErr = new StringBuilder()
542+
val (classpath, parent, filename) = if (path.isJarFileSystem) {
543+
val parent = workspace
544+
val className = path.toString.stripPrefix("/").stripSuffix(".class")
545+
(
546+
path.jarPath.toList.flatMap(cp => List("-cp", cp.toString)),
547+
parent,
548+
className
549+
)
550+
} else
551+
(List.empty, path.parent, path.filename)
552+
val defaultArgs = classpath ::: List("-private")
553+
val args = if (verbose) "-verbose" :: defaultArgs else defaultArgs
518554
shellRunner
519555
.run(
520556
"Decode using javap",
521557
JavaBinary(userConfig().javaHome, "javap") :: args ::: List(
522-
path.filename
558+
filename
523559
),
524-
path.parent,
560+
parent,
525561
redirectErrorOutput = false,
526562
Map.empty,
527563
s => {
@@ -543,30 +579,45 @@ final class FileDecoderProvider(
543579
})
544580
} catch {
545581
case NonFatal(e) =>
546-
scribe.error(e.toString())
582+
scribe.error(s"$e ${e.getStackTrace.mkString("\n at ")}")
547583
Future.successful(DecoderResponse.failed(path.toURI, e))
548584
}
549585
}
550586

551-
private def decodeCFRFromClassFile(
587+
def decodeCFRAndWait(path: AbsolutePath): DecoderResponse = {
588+
val result = decodeCFRFromClassFile(path)
589+
Await.result(result, Duration("10min"))
590+
}
591+
592+
def decodeCFRFromClassFile(
552593
path: AbsolutePath
553594
): Future[DecoderResponse] = {
554595
val cfrDependency = Dependency.of("org.benf", "cfr", "0.151")
555596
val cfrMain = "org.benf.cfr.reader.Main"
556597

598+
// find the build target so we can use the full classpath - needed for a better decompile
599+
// class file can be in classes output dir or could be in a jar on the build's classpath
557600
val buildTarget = buildTargets
558601
.inferBuildTarget(path)
559602
.orElse(
560603
buildTargets.allScala
561604
.find(buildTarget =>
562-
path.isInside(buildTarget.classDirectory.toAbsolutePath)
605+
path.isInside(
606+
buildTarget.classDirectory.toAbsolutePath
607+
) || path.jarPath
608+
.map(jar => buildTarget.fullClasspath.contains(jar.toNIO))
609+
.getOrElse(false)
563610
)
564611
.map(_.id)
565612
)
566613
.orElse(
567614
buildTargets.allJava
568615
.find(buildTarget =>
569-
path.isInside(buildTarget.classDirectory.toAbsolutePath)
616+
path.isInside(
617+
buildTarget.classDirectory.toAbsolutePath
618+
) || path.jarPath
619+
.map(jar => buildTarget.fullClasspath.contains(jar.toNIO))
620+
.getOrElse(false)
570621
)
571622
.map(_.id)
572623
)
@@ -597,9 +648,16 @@ final class FileDecoderProvider(
597648
(classesPath, className)
598649
})
599650
.getOrElse({
600-
val parent = path.parent
601-
val className = path.filename
602-
(parent, className)
651+
if (path.isJarFileSystem) {
652+
// if the file is in a jar then use the workspace as the working dir and the fully qualified name as the class
653+
val parent = workspace
654+
val className = path.toString.stripPrefix("/")
655+
(parent, className)
656+
} else {
657+
val parent = path.parent
658+
val className = path.filename
659+
(parent, className)
660+
}
603661
})
604662

605663
val args = extraClassPath :::
@@ -637,15 +695,21 @@ final class FileDecoderProvider(
637695
if (sbOut.isEmpty && sbErr.nonEmpty)
638696
DecoderResponse.failed(
639697
path.toURI,
640-
s"$cfrDependency\n$cfrMain\n$parent\n$args\n${sbErr.toString}"
698+
s"buildTarget:$buildTarget\nCFRJar:$cfrDependency\nCFRMain:$cfrMain\nParent:$parent\nArgs:$args\nError:${sbErr.toString}"
641699
)
642700
else
643701
DecoderResponse.success(path.toURI, sbOut.toString)
644702
})
645703
} catch {
646704
case NonFatal(e) =>
647705
scribe.error(e.toString())
648-
Future.successful(DecoderResponse.failed(path.toURI, e))
706+
Future.successful(
707+
DecoderResponse.failed(
708+
path.toURI,
709+
s"buildTarget:$buildTarget\nCFRJar:$cfrDependency\nCFRMain:$cfrMain\nParent:$parent\nArgs:$args\nError:${sbErr.toString}",
710+
e
711+
)
712+
)
649713
}
650714
}
651715

0 commit comments

Comments
 (0)