-
Notifications
You must be signed in to change notification settings - Fork 438
Add LSP jar file system support #8329
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
Open
Manykeys
wants to merge
3
commits into
scalameta:main
Choose a base branch
from
Manykeys:jar-file-system
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,059
−55
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
metals/src/main/scala/scala/meta/internal/metals/JarFileSystemCache.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package scala.meta.internal.metals | ||
|
|
||
| import java.net.URI | ||
| import java.nio.file.FileSystem | ||
| import java.nio.file.FileSystemAlreadyExistsException | ||
| import java.nio.file.FileSystemNotFoundException | ||
| import java.nio.file.FileSystems | ||
|
|
||
| import scala.collection.mutable | ||
| import scala.util.control.NonFatal | ||
|
|
||
| import scala.meta.io.AbsolutePath | ||
|
|
||
| final case class FileSystemInfo(fs: FileSystem, fileUri: String) | ||
|
|
||
| final class JarFileSystemCache { | ||
|
|
||
| private val openFileSystems = mutable.Map.empty[URI, FileSystem] | ||
|
|
||
| /** | ||
| * Synchronized to avoid a race where two threads both call newFileSystem | ||
| * for the same archive. | ||
| */ | ||
| def open(localPath: AbsolutePath): FileSystemInfo = synchronized { | ||
| val fileUri = localPath.toNIO.toUri.toString.stripSuffix("/") | ||
| val zipURI = JarFileSystemCache.jarUriFor(localPath) | ||
| val fs = openFileSystems.getOrElseUpdate(zipURI, openFileSystem(zipURI)) | ||
| FileSystemInfo(fs, fileUri) | ||
| } | ||
|
|
||
| private def openFileSystem(zipURI: URI): FileSystem = | ||
| try FileSystems.getFileSystem(zipURI) | ||
| catch { | ||
| case _: FileSystemNotFoundException => | ||
| try | ||
| FileSystems | ||
| .newFileSystem(zipURI, new java.util.HashMap[String, Any]) | ||
| catch { | ||
| case _: FileSystemAlreadyExistsException => | ||
| FileSystems.getFileSystem(zipURI) | ||
| } | ||
| } | ||
|
|
||
| def closeObsolete(knownPaths: Set[AbsolutePath]): Unit = synchronized { | ||
| val knownUris = knownPaths.map(JarFileSystemCache.jarUriFor) | ||
| val obsoleteUris = openFileSystems.keysIterator.filterNot(knownUris).toList | ||
| obsoleteUris.foreach(close) | ||
| } | ||
|
|
||
| def close(zipURI: URI): Unit = synchronized { | ||
| openFileSystems.remove(zipURI).foreach { fs => | ||
| try fs.close() | ||
| catch { | ||
| case NonFatal(e) => | ||
| scribe.warn(s"Failed to close jar file system $zipURI", e) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| object JarFileSystemCache { | ||
| def jarUriFor(localPath: AbsolutePath): URI = { | ||
| val fileUri = localPath.toNIO.toUri.toString.stripSuffix("/") | ||
| URI.create(s"jar:$fileUri") | ||
| } | ||
| } | ||
171 changes: 171 additions & 0 deletions
171
metals/src/main/scala/scala/meta/internal/metals/LSPFileSystemProvider.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| package scala.meta.internal.metals | ||
|
|
||
| import java.nio.charset.StandardCharsets | ||
| import java.nio.file.Files | ||
| import java.util.stream.Collectors | ||
|
|
||
| import scala.concurrent.ExecutionContext | ||
| import scala.concurrent.Future | ||
| import scala.util.Using | ||
|
|
||
| import scala.meta.internal.metals.MetalsEnrichments._ | ||
| import scala.meta.internal.metals.clients.language.MetalsLanguageClient | ||
| import scala.meta.internal.mtags.URIEncoderDecoder | ||
|
|
||
| /** Response indicating whether an entry is a file or directory. */ | ||
| case class FSReadDirectoryResponse(name: String, isFile: Boolean) | ||
|
|
||
| /** Response containing the contents of a directory listing. */ | ||
| case class FSReadDirectoriesResponse( | ||
| name: String, | ||
| directories: Array[FSReadDirectoryResponse], | ||
| error: String, | ||
| ) | ||
|
|
||
| /** Response containing the textual contents of a file. */ | ||
| case class FSReadFileResponse(name: String, value: String, error: String) | ||
|
|
||
| /** Response containing file metadata (existence and type). */ | ||
| case class FSStatResponse(name: String, isFile: Boolean, error: String) | ||
|
|
||
| /** | ||
| * Handles virtual file system requests from the LSP client. | ||
| * | ||
| * Translates `metalsfs://` URIs into JAR file system reads using Java NIO, | ||
| * delegating URI resolution to [[URIMapper]] and `.class` decompilation | ||
| * to [[FileDecoderProvider]]. | ||
| */ | ||
| class LSPFileSystemProvider( | ||
| languageClient: MetalsLanguageClient, | ||
| uriMapper: URIMapper, | ||
| fileDecoderProvider: FileDecoderProvider, | ||
| )(implicit ec: ExecutionContext) { | ||
|
|
||
| /** Callers must gate on [[ClientConfiguration.isLibraryFileSystemSupported]]. */ | ||
| def sendLibraryFileSystemReady(): Unit = { | ||
| val params = | ||
| ClientCommands.LibraryFileSystemReady.toExecuteCommandParams() | ||
| languageClient.metalsExecuteClientCommand(params) | ||
| } | ||
|
|
||
| /** | ||
| * Lists the contents of a virtual directory. | ||
| * | ||
| * For top-level URIs returns the three root categories (jdk, jar, source). | ||
| * For category URIs returns the available archives from [[BuildTargets]]. | ||
| * For paths within an archive opens the corresponding NIO file system | ||
| * and lists the directory entries. | ||
| */ | ||
| def readDirectory(uri: String): Future[FSReadDirectoriesResponse] = Future { | ||
| val entries = uri match { | ||
| case URIMapper.parentURI => | ||
| Array( | ||
| FSReadDirectoryResponse(URIMapper.jdkDir, isFile = false), | ||
| FSReadDirectoryResponse(URIMapper.workspaceJarDir, isFile = false), | ||
| FSReadDirectoryResponse(URIMapper.sourceJarDir, isFile = false), | ||
| ) | ||
| case URIMapper.jdkURI => | ||
| uriMapper.getJDKs | ||
| .map(name => FSReadDirectoryResponse(name, isFile = false)) | ||
| .toArray | ||
| case URIMapper.workspaceJarURI => | ||
| uriMapper.getWorkspaceJars | ||
| .map(name => FSReadDirectoryResponse(name, isFile = false)) | ||
| .toArray | ||
| case URIMapper.sourceJarURI => | ||
| uriMapper.getSourceJars | ||
| .map(name => FSReadDirectoryResponse(name, isFile = false)) | ||
| .toArray | ||
| case _ => | ||
| val (fs, innerPath) = resolveInnerPath(uri) | ||
| val path = fs.fs.getPath(innerPath.getOrElse("/")) | ||
| Using.resource(Files.list(path)) { stream => | ||
| stream | ||
| .collect(Collectors.toList()) | ||
| .asScala | ||
| .map(p => | ||
| FSReadDirectoryResponse( | ||
| p.getFileName.toString, | ||
| isFile = Files.isRegularFile(p), | ||
| ) | ||
| ) | ||
| .toArray | ||
| } | ||
| } | ||
| FSReadDirectoriesResponse(uri, entries, "") | ||
| } | ||
|
|
||
| /** | ||
| * Reads the textual contents of a file inside a JAR archive. | ||
| * | ||
| * For `.class` files the bytecode is decompiled via CFR through | ||
| * [[FileDecoderProvider]]. All other files are read as UTF-8 text. | ||
| */ | ||
| def readFile(uri: String): Future[FSReadFileResponse] = | ||
| Future(resolveInnerPath(uri)).flatMap { case (fs, innerPath) => | ||
| val path = fs.fs.getPath(innerPath.getOrElse("/")) | ||
| if (path.getFileName.toString.endsWith(".class")) | ||
| fileDecoderProvider.decodedFileContents(path.toUri.toString).map { | ||
| res => | ||
| val contents = Option(res.value).filter(_.nonEmpty).getOrElse("") | ||
| val error = Option(res.error).getOrElse("") | ||
| FSReadFileResponse(uri, contents, error) | ||
| } | ||
| else | ||
| Future { | ||
| val contents = | ||
| new String(Files.readAllBytes(path), StandardCharsets.UTF_8) | ||
| FSReadFileResponse(uri, contents, "") | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns metadata for the given URI, indicating whether it | ||
| * represents a file or a directory. | ||
| * | ||
| * Known root-level URIs are resolved statically; paths within | ||
| * an archive are checked via the NIO file system. | ||
| */ | ||
| def getSystemStat(uri: String): Future[FSStatResponse] = Future { | ||
| uri match { | ||
| case URIMapper.parentURI => | ||
| FSStatResponse(URIMapper.rootDir, isFile = false, "") | ||
| case URIMapper.jdkURI => | ||
| FSStatResponse(URIMapper.jdkDir, isFile = false, "") | ||
| case URIMapper.workspaceJarURI => | ||
| FSStatResponse(URIMapper.workspaceJarDir, isFile = false, "") | ||
| case URIMapper.sourceJarURI => | ||
| FSStatResponse(URIMapper.sourceJarDir, isFile = false, "") | ||
| case _ => | ||
| val (fs, innerPath) = resolveInnerPath(uri) | ||
| val path = fs.fs.getPath(innerPath.getOrElse("/")) | ||
| FSStatResponse(uri, isFile = Files.isRegularFile(path), "") | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Resolves a `metalsfs://` URI into a NIO [[FileSystemInfo]] and | ||
| * an optional inner path within the archive. | ||
| * | ||
| * Dispatches to the appropriate [[URIMapper]] method based on whether | ||
| * the URI falls under the jdk, workspace jar, or source jar category. | ||
| */ | ||
| private def resolveInnerPath( | ||
| uri: String | ||
| ): (FileSystemInfo, Option[String]) = | ||
| URIEncoderDecoder.decode(uri) match { | ||
| case jdk if jdk.startsWith(URIMapper.jdkURI) => | ||
| val (name, path) = URIMapper.getURIParts(jdk, URIMapper.jdkURI) | ||
| (uriMapper.getJDKFileSystem(name), path) | ||
| case jar if jar.startsWith(URIMapper.workspaceJarURI) => | ||
| val (name, path) = | ||
| URIMapper.getURIParts(jar, URIMapper.workspaceJarURI) | ||
| (uriMapper.getWorkspaceJarFileSystem(name), path) | ||
| case src if src.startsWith(URIMapper.sourceJarURI) => | ||
| val (name, path) = | ||
| URIMapper.getURIParts(src, URIMapper.sourceJarURI) | ||
| (uriMapper.getSourceJarFileSystem(name), path) | ||
| case other => | ||
| throw new IllegalArgumentException(s"Unknown metalsfs URI: $other") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.