Skip to content

Commit

Permalink
Add filesystem support for jars
Browse files Browse the repository at this point in the history
  • Loading branch information
Arthurm1 committed Mar 22, 2022
1 parent 4d583c7 commit 2cc3fb5
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,15 @@ object ClientCommands {
|""".stripMargin
)

val CreateLibraryFileSystem = new ParametrizedCommand[String](
"metals-create-library-filesystem",
"Create Library FS",
"""|Notifies the client that it should create an empty
|filesystem to navigate jar dependencies
|""".stripMargin,
arguments = """`string`, the URI root of the filesystem.""".stripMargin
)

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

def isLibraryFileSystemSupported(): Boolean =
// TODO - make this false before commit
initializationOptions.isLibraryFileSystemSupported.getOrElse(true)

def icons(): Icons =
initializationOptions.icons
.map(Icons.fromString)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package scala.meta.internal.metals

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.clients.language.MetalsLanguageClient
import scala.meta.internal.io.FileIO
import scala.meta.io.AbsolutePath
import scala.annotation.tailrec
import java.nio.file.Files
import java.util.stream.Collectors
import java.nio.charset.StandardCharsets

final case class FSReadDirectoryResponse(
name: String,
isFile: Boolean
)

final case class FSReadFileResponse(
name: String,
value: String,
error: String
)

final case class FSStatResponse(
name: String,
isFile: Boolean
)

final class FileSystemProvider(
languageClient: MetalsLanguageClient,
clientConfig: ClientConfiguration,
buildTargets: BuildTargets
)(implicit ec: ExecutionContext) {

private val jarFileSystemRoot = "metals_library_dependencies:/"

private case class JarInfo(path: AbsolutePath, isSource: Boolean)
private var jarNameToPath = Map.empty[String, JarInfo]

private def isTopLevel(uriAsStr: String) =
uriAsStr == jarFileSystemRoot

private def isJarLevel(uriAsStr: String) = {
val possibleJarName = uriAsStr.stripPrefix(jarFileSystemRoot)
possibleJarName.nonEmpty && !possibleJarName.contains('/')
}

private def getJarName(uriAsStr: String): Option[String] = {
val possibleJarName = uriAsStr.stripPrefix(jarFileSystemRoot)
if (possibleJarName.nonEmpty) {
Some(possibleJarName.takeWhile(_ != '/'))
} else None
}

private def getTopLevelJars: List[FSReadDirectoryResponse] = {
if (jarNameToPath.isEmpty) {
val workspaceJars = buildTargets.allWorkspaceJars.toList
val sourceAndSourcelessJars = workspaceJars.map(jar => {
val sourceJarName = jar.filename.replace(".jar", "-sources.jar")
buildTargets
.sourceJarFile(sourceJarName)
.map(sourceJar => JarInfo(sourceJar, isSource = true))
.getOrElse(JarInfo(jar, isSource = false))
})
jarNameToPath =
sourceAndSourcelessJars.map(f => f.path.filename -> f).toMap
}
jarNameToPath.keys.map(FSReadDirectoryResponse(_, false)).toList
}

private def getJarDirs(uriAsStr: String, jarName: String): List[String] = {
@tailrec
def getDirs(remainingUri: String, dirs: List[String]): List[String] = {
if (remainingUri.isEmpty)
dirs
else {
val (head, tail) = remainingUri.stripPrefix("/").span(_ != '/')
getDirs(tail, head :: dirs)
}
}
val remainingUri =
uriAsStr.stripPrefix(jarFileSystemRoot).stripPrefix(jarName)
getDirs(remainingUri, List.empty).reverse
}

private def accessJar[A](
uriAsStr: String,
convert: AbsolutePath => A
): Option[A] = {

@tailrec
def resolve(path: AbsolutePath, dirs: List[String]): AbsolutePath = {
if (dirs.isEmpty)
path
else
resolve(path.resolve(dirs.head), dirs.tail)
}

for {
jarName <- getJarName(uriAsStr)
jarInfo = jarNameToPath(jarName)
jarDirs = getJarDirs(uriAsStr, jarName)
} yield {
FileIO
.withJarFileSystem(
jarInfo.path,
create = false,
// TODO check if close is correct - copied from FindTextInDependencyJars
close = !jarInfo.isSource
) { root => convert(resolve(root, jarDirs)) }
}
}

private def getJarChildren(
uriAsStr: String
): List[FSReadDirectoryResponse] = {
accessJar(
uriAsStr,
uriPath =>
Files
.list(uriPath.toNIO)
.map(path =>
FSReadDirectoryResponse(
path.filename,
isFile = !Files.isDirectory(path)
)
)
.collect(Collectors.toList())
.asScala
.toList
).getOrElse(List.empty)
}

private def readFileContents(uriAsStr: String): FSReadFileResponse = {
accessJar(
uriAsStr,
path => {
val contents =
new String(Files.readAllBytes(path.toNIO), StandardCharsets.UTF_8)
FSReadFileResponse(path.filename, contents, null)
}
).getOrElse(FSReadFileResponse(uriAsStr, null, "Unknown error"))
}

private def getSystemStatInfo(uriAsStr: String): FSStatResponse = {
accessJar(
uriAsStr,
path =>
FSStatResponse(path.filename, isFile = !Files.isDirectory(path.toNIO))
)
// TODO introduce error class
.getOrElse(FSStatResponse("Fake", false))
}

def createLibraryFileSystem(): Unit =
if (clientConfig.isLibraryFileSystemSupported()) {
val params =
ClientCommands.CreateLibraryFileSystem.toExecuteCommandParams(
jarFileSystemRoot
)
languageClient.metalsExecuteClientCommand(params)
}

def readDirectory(
uriAsStr: String
): Future[Array[FSReadDirectoryResponse]] =
Future {
val contents = if (isTopLevel(uriAsStr)) {
getTopLevelJars
} else
getJarChildren(uriAsStr)
contents.toArray
}

def readFile(uriAsStr: String): Future[FSReadFileResponse] = Future {
readFileContents(uriAsStr)
}

def getSystemStat(uriAsStr: String): Future[FSStatResponse] =
Future {
if (isTopLevel(uriAsStr) || isJarLevel(uriAsStr))
FSStatResponse("/", isFile = false)
else
getSystemStatInfo(uriAsStr)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import ch.epfl.scala.{bsp4j => b}
final case class Indexer(
workspaceReload: () => WorkspaceReload,
doctor: () => Doctor,
fileSystemProvider: () => FileSystemProvider,
languageClient: DelegatingLanguageClient,
bspSession: () => Option[BspSession],
executionContext: ExecutionContextExecutorService,
Expand Down Expand Up @@ -266,6 +267,8 @@ final case class Indexer(
buildTool.importedBuild.dependencySources
)
}
// create library FS after jars have been indexed
fileSystemProvider().createLibraryFileSystem()
// Schedule removal of unused toplevel symbols from cache
if (usedJars.nonEmpty)
sh.schedule(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ final case class InitializationOptions(
isHttpEnabled: Option[Boolean],
commandInHtmlFormat: Option[CommandHTMLFormat],
isVirtualDocumentSupported: Option[Boolean],
isLibraryFileSystemSupported: Option[Boolean],
openFilesOnRenameProvider: Option[Boolean],
quickPickProvider: Option[Boolean],
renameFileThreshold: Option[Int],
Expand Down Expand Up @@ -109,6 +110,7 @@ object InitializationOptions {
None,
None,
None,
None,
None
)

Expand Down Expand Up @@ -152,6 +154,8 @@ object InitializationOptions {
.flatMap(CommandHTMLFormat.fromString),
isVirtualDocumentSupported =
jsonObj.getBooleanOption("isVirtualDocumentSupported"),
isLibraryFileSystemSupported =
jsonObj.getBooleanOption("isLibraryFileSystemSupported"),
openFilesOnRenameProvider =
jsonObj.getBooleanOption("openFilesOnRenameProvider"),
quickPickProvider = jsonObj.getBooleanOption("quickPickProvider"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class MetalsLanguageServer(
private var compilers: Compilers = _
private var scalafixProvider: ScalafixProvider = _
private var fileDecoderProvider: FileDecoderProvider = _
private var fileSystemProvider: FileSystemProvider = _
private var testProvider: TestSuitesProvider = _
private var workspaceReload: WorkspaceReload = _
private var buildToolSelector: BuildToolSelector = _
Expand Down Expand Up @@ -730,6 +731,11 @@ class MetalsLanguageServer(
clientConfig,
classFinder
)
fileSystemProvider = new FileSystemProvider(
languageClient,
clientConfig,
buildTargets
)
popupChoiceReset = new PopupChoiceReset(
workspace,
tables,
Expand Down Expand Up @@ -1713,6 +1719,12 @@ class MetalsLanguageServer(
params.kind == "class"
)
.asJavaObject
case ServerCommands.FileSystemStat(uriAsStr) =>
fileSystemProvider.getSystemStat(uriAsStr).asJavaObject
case ServerCommands.FileSystemReadFile(uriAsStr) =>
fileSystemProvider.readFile(uriAsStr).asJavaObject
case ServerCommands.FileSystemReadDirectory(uriAsStr) =>
fileSystemProvider.readDirectory(uriAsStr).asJavaObject
case ServerCommands.RunDoctor() =>
Future {
doctor.executeRunDoctor()
Expand Down Expand Up @@ -2314,6 +2326,7 @@ class MetalsLanguageServer(
private val indexer = Indexer(
() => workspaceReload,
() => doctor,
() => fileSystemProvider,
languageClient,
() => bspSession,
executionContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,42 @@ object ServerCommands {
|""".stripMargin
)

val FileSystemStat = new ParametrizedCommand[String](
"filesystem-stat",
"Read Directory",
"""|Return information about the uri. E.g. uri is a directory
|
|Virtual File Systems allow client to display filesystem data
|in the format that Metals dictates.
|e.g. jar library dependencies
|""".stripMargin,
"[uri], uri of the file or directory."
)

val FileSystemReadDirectory = new ParametrizedCommand[String](
"filesystem-read-directory",
"Read Directory",
"""|Return the contents of a virtual filesystem directory.
|
|Virtual File Systems allow client to display filesystem data
|in the format that Metals dictates.
|e.g. jar library dependencies
|""".stripMargin,
"[uri], uri of the directory."
)

val FileSystemReadFile = new ParametrizedCommand[String](
"filesystem-read-file",
"Read File",
"""|Return the contents of a virtual filesystem file.
|
|Virtual File Systems allow client to display filesystem data
|in the format that Metals dictates.
|e.g. jar library dependencies
|""".stripMargin,
"[uri], uri of the file."
)

val RunDoctor = new Command(
"doctor-run",
"Run doctor",
Expand Down

0 comments on commit 2cc3fb5

Please sign in to comment.