Skip to content

Commit 1468162

Browse files
committed
Add filesystem support for jars
1 parent bb0620e commit 1468162

37 files changed

+1853
-462
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
scala.meta.internal.metals.filesystem.MetalsFileSystemProvider

metals/src/main/scala/scala/meta/internal/decorations/SyntheticsDecorationProvider.scala

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,16 @@ final class SyntheticsDecorationProvider(
282282
textDocument: Option[s.TextDocument],
283283
path: AbsolutePath
284284
): Option[TextDocument] = {
285-
for {
286-
doc <- textDocument
287-
source <- fingerprints.loadLastValid(path, doc.md5, charset)
288-
docWithText = doc.withText(source)
289-
_ = Document.set(docWithText)
290-
} yield docWithText
285+
// TODO what does `enrichWithText` do and should it be run on readOnly files? fingerprints causes slurping ;-(
286+
if (path.isReadOnly)
287+
textDocument
288+
else
289+
for {
290+
doc <- textDocument
291+
source <- fingerprints.loadLastValid(path, doc.md5, charset)
292+
docWithText = doc.withText(source)
293+
_ = Document.set(docWithText)
294+
} yield docWithText
291295
}
292296

293297
private def localSymbolName(symbol: String, textDoc: TextDocument): String = {

metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,16 @@ final class ImplementationProvider(
395395
def isDefinitionOccurrence(occ: SymbolOccurrence) =
396396
occ.role.isDefinition && occ.symbol == symbol
397397

398+
val input =
399+
if (source.isReadOnly)
400+
source.toInputFromBuffers(buffer)
401+
else
402+
source.toInput
398403
semanticDb.occurrences
399404
.find(isDefinitionOccurrence)
400405
.orElse(
401406
Mtags
402-
.allToplevels(source.toInput, scalaVersionSelector.getDialect(source))
407+
.allToplevels(input, scalaVersionSelector.getDialect(source))
403408
.occurrences
404409
.find(isDefinitionOccurrence)
405410
)

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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import scala.util.control.NonFatal
1313

1414
import scala.meta.internal.io.PathIO
1515
import scala.meta.internal.metals.MetalsEnrichments._
16+
import scala.meta.internal.metals.filesystem.MetalsPath
1617
import scala.meta.internal.mtags.Symbol
1718
import scala.meta.io.AbsolutePath
1819

@@ -93,6 +94,9 @@ final class BuildTargets() {
9394
def allJava: Iterator[JavaTarget] =
9495
data.fromIterators(_.allJava)
9596

97+
def allJDKs: Iterator[String] =
98+
data.fromIterators(_.allJDKs).distinct
99+
96100
def info(id: BuildTargetIdentifier): Option[BuildTarget] =
97101
data.fromOptions(_.info(id))
98102

@@ -267,7 +271,8 @@ final class BuildTargets() {
267271
} yield imports
268272

269273
/**
270-
* Tries to guess what build target this readonly file belongs to from the symbols it defines.
274+
* Tries to guess what build target this metalsfs or readonly file belongs
275+
* to from the symbols it defines.
271276
*
272277
* By default, we rely on carefully recording what build target produced what
273278
* files in the `.metals/readonly/` directory. This approach has the problem
@@ -276,51 +281,53 @@ final class BuildTargets() {
276281
* - a new metals feature forgot to record the build target
277282
* - a user removes `.metals/metals.h2.db`
278283
*
279-
* When encountering an unknown `readonly/` file we do the following steps to
284+
* When encountering an unknown `readonly/` or metalfs file we do the following steps to
280285
* infer what build target it belongs to:
281286
*
282287
* - check if file is in `.metals/readonly/dependencies/${source-jar-name}`
288+
* - check if the file is a jdk file
283289
* - find the build targets that have a sourceDependency with that name
284-
*
285-
* Otherwise if it's a jar file we find a build target it belongs to.
290+
* - find the build targets that have a workspace jar with that name as class files can be viewed
286291
*
287292
* This approach is not glamorous but it seems to work reasonably well.
288293
*/
289294
def inferBuildTarget(
290295
source: AbsolutePath
291296
): Option[BuildTargetIdentifier] = {
292-
if (source.isJarFileSystem) {
293-
for {
294-
jarName <- source.jarPath.map(_.filename)
295-
sourceJarFile <- sourceJarFile(jarName)
296-
buildTargetId <- inverseDependencySource(sourceJarFile).headOption
297-
} yield buildTargetId
298-
} else {
299-
val readonly = workspace.resolve(Directories.readonly)
300-
source.toRelativeInside(readonly) match {
301-
case Some(rel) =>
302-
val names = rel.toNIO.iterator().asScala.toList.map(_.filename)
303-
names match {
304-
case Directories.dependenciesName :: jarName :: _ =>
305-
// match build target by source jar name
306-
sourceJarFile(jarName)
307-
.flatMap(inverseDependencySource(_).headOption)
308-
case _ => None
309-
}
310-
case None =>
311-
// else it can be a source file inside a jar
312-
val fromJar = jarPath(source)
313-
.flatMap { jar =>
314-
allBuildTargetIdsInternal.find { case (_, id) =>
315-
targetJarClasspath(id).exists(_.contains(jar))
316-
}
317-
}
318-
fromJar.map { case (data0, id) =>
319-
data0.addSourceItem(source, id)
320-
id
321-
}
322-
}
297+
// source could be on metalFS or in readonly area
298+
val metalsPath = source.toNIO match {
299+
case metalsPath: MetalsPath => Some(metalsPath)
300+
case _ =>
301+
val readonly = workspace.resolve(Directories.readonly)
302+
source.toRelativeInside(readonly) match {
303+
case Some(rel) => Some(MetalsPath.fromReadOnly(rel))
304+
case None => None
305+
}
323306
}
307+
metalsPath.flatMap(metalsPath => {
308+
// check JDK - any build target is OK
309+
if (metalsPath.isJDK)
310+
allBuildTargetIdsInternal.headOption.map(_._2)
311+
else {
312+
// check source jars
313+
for {
314+
jarName <- metalsPath.jarName
315+
jarPath <- sourceJarFile(jarName)
316+
buildTargetId <- inverseDependencySource(jarPath).headOption
317+
} yield buildTargetId
318+
}.orElse({
319+
// check workspace jars
320+
// TODO speed this up - we're iterating over all build targets and all classpath entries - create inverseDependencySource equivalent
321+
val targetIds = for {
322+
targetId <- allBuildTargetIds
323+
classpathEntries <- targetJarClasspath(targetId).toList
324+
classpathEntry <- classpathEntries
325+
jarName <- metalsPath.jarName
326+
if classpathEntry.filename == jarName
327+
} yield targetId
328+
targetIds.headOption
329+
})
330+
})
324331
}
325332

326333
def findByDisplayName(name: String): Option[BuildTarget] = {
@@ -329,14 +336,6 @@ final class BuildTargets() {
329336
.find(_.getDisplayName() == name)
330337
}
331338

332-
private def jarPath(source: AbsolutePath): Option[AbsolutePath] = {
333-
source.jarPath.map { sourceJarPath =>
334-
sourceJarPath.parent.resolve(
335-
source.filename.replace("-sources.jar", ".jar")
336-
)
337-
}
338-
}
339-
340339
/**
341340
* Returns meta build target for `*.sbt` or `*.scala` files.
342341
* It selects build target by directory of its connection

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,15 @@ object ClientCommands {
241241
|""".stripMargin
242242
)
243243

244+
val CreateLibraryFileSystem = new ParametrizedCommand[String](
245+
"metals-create-library-filesystem",
246+
"Create Library FS",
247+
"""|Notifies the client that it should create an empty
248+
|filesystem to navigate jar dependencies
249+
|""".stripMargin,
250+
arguments = """`string`, the URI root of the filesystem.""".stripMargin
251+
)
252+
244253
val RefreshModel = new Command(
245254
"metals-model-refresh",
246255
"Refresh model",
@@ -325,6 +334,7 @@ object ClientCommands {
325334
FocusDiagnostics,
326335
GotoLocation,
327336
EchoCommand,
337+
CreateLibraryFileSystem,
328338
RefreshModel,
329339
ShowStacktrace,
330340
CopyWorksheetOutput,

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

Lines changed: 3 additions & 0 deletions
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 = true
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/DefinitionProvider.scala

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package scala.meta.internal.metals
22

3+
import java.nio.file.Paths
34
import java.util.Collections
45
import java.{util => ju}
56

@@ -8,6 +9,7 @@ import scala.concurrent.Future
89

910
import scala.meta.inputs.Input
1011
import scala.meta.internal.metals.MetalsEnrichments._
12+
import scala.meta.internal.metals.filesystem.MetalsFileSystem
1113
import scala.meta.internal.mtags.GlobalSymbolIndex
1214
import scala.meta.internal.mtags.Mtags
1315
import scala.meta.internal.mtags.Semanticdbs
@@ -16,9 +18,11 @@ import scala.meta.internal.mtags.SymbolDefinition
1618
import scala.meta.internal.parsing.TokenEditDistance
1719
import scala.meta.internal.parsing.Trees
1820
import scala.meta.internal.remotels.RemoteLanguageServer
21+
import scala.meta.internal.semanticdb.Language
1922
import scala.meta.internal.semanticdb.Scala._
2023
import scala.meta.internal.semanticdb.SymbolOccurrence
2124
import scala.meta.internal.semanticdb.TextDocument
25+
import scala.meta.internal.tvp.ClasspathSymbols
2226
import scala.meta.io.AbsolutePath
2327
import scala.meta.pc.CancelToken
2428

@@ -265,6 +269,7 @@ final class DefinitionProvider(
265269
}
266270

267271
private def fromMtags(source: AbsolutePath, dirtyPos: Position) = {
272+
// TODO why does a writable file use `source.toInput` instead of `source.toInputFromBuffers(buffers)`
268273
Mtags
269274
.allToplevels(source.toInput, scalaVersionSelector.getDialect(source))
270275
.occurrences
@@ -316,12 +321,28 @@ class DestinationProvider(
316321
private def bestTextDocument(
317322
symbolDefinition: SymbolDefinition
318323
): TextDocument = {
319-
val defnRevisedInput = symbolDefinition.path.toInput
324+
val decodedClassfile =
325+
if (symbolDefinition.path.isClassfile)
326+
MetalsFileSystem.metalsFS
327+
.decodeCFRFromClassFile(symbolDefinition.path.toNIO)
328+
.map(text =>
329+
Input.VirtualFile(symbolDefinition.path.toURI.toString(), text)
330+
)
331+
else None
332+
val defnRevisedInput = decodedClassfile.getOrElse(
333+
if (symbolDefinition.path.isReadOnly)
334+
symbolDefinition.path.toInputFromBuffers(buffers)
335+
else
336+
symbolDefinition.path.toInput
337+
)
320338
// Read text file from disk instead of editor buffers because the file
321339
// on disk is more likely to parse.
340+
val language =
341+
if (symbolDefinition.path.isClassfile) Language.JAVA
342+
else symbolDefinition.path.toLanguage
322343
lazy val parsed =
323344
mtags.index(
324-
symbolDefinition.path.toLanguage,
345+
language,
325346
defnRevisedInput,
326347
symbolDefinition.dialect
327348
)
@@ -346,23 +367,54 @@ class DestinationProvider(
346367
definition(symbol, targets)
347368
}
348369

370+
private val classpathSymbols = new ClasspathSymbols(
371+
isStatisticsEnabled = false
372+
)
373+
349374
def definition(
350375
symbol: String,
351376
allowedBuildTargets: Set[BuildTargetIdentifier]
352377
): Option[SymbolDefinition] = {
353-
val definitions = index.definitions(Symbol(symbol)).filter(_.path.exists)
378+
val querySymbol: Symbol = Symbol(symbol)
379+
val definitions = index.definitions(querySymbol).filter(_.path.exists)
380+
val extraDefinitions = if (definitions.isEmpty) {
381+
// search in all classpath, i.e. jars with no source
382+
buildTargets
383+
.inferBuildTarget(List(querySymbol.toplevel))
384+
.flatMap(buildTarget => {
385+
val metalsFSJar =
386+
MetalsFileSystem.metalsFS
387+
.getOrElseUpdateWorkspaceJar(buildTarget.jar)
388+
classpathSymbols
389+
.symbols(metalsFSJar, symbol)
390+
.filter(_.symbol == symbol)
391+
.flatMap(_.uri)
392+
.headOption
393+
.map(symbolFullURI => {
394+
val fullJarPath = AbsolutePath(Paths.get(symbolFullURI))
395+
SymbolDefinition(
396+
querySymbol,
397+
querySymbol,
398+
fullJarPath,
399+
scala.meta.dialects.Scala212Source3,
400+
None
401+
)
402+
})
403+
})
404+
.toList
405+
} else definitions
354406
if (allowedBuildTargets.isEmpty)
355-
definitions.headOption
407+
extraDefinitions.headOption
356408
else {
357-
val matched = definitions.find { defn =>
409+
val matched = extraDefinitions.find { defn =>
358410
sourceBuildTargets(defn.path).exists(id =>
359411
allowedBuildTargets.contains(id)
360412
)
361413
}
362414
// Fallback to any definition - it's needed for worksheets
363415
// They might have dynamic `import $dep` and these sources jars
364416
// aren't registered in buildTargets
365-
matched.orElse(definitions.headOption)
417+
matched.orElse(extraDefinitions.headOption)
366418
}
367419
}
368420

0 commit comments

Comments
 (0)