From 596885f88b2e2540b6743ca23f74da4fb61edab7 Mon Sep 17 00:00:00 2001 From: tgodzik Date: Sun, 13 Aug 2023 17:29:54 +0200 Subject: [PATCH] improvement: Base Metals view on indexing information MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we would read the classpath to create the metals view, which worked well for Scala 2, but needed a reimplementation for Scala 3. So now, instead of doing this reimplementation, which would probably need to use tasty and might be quite complex, I decided to base it on the same approach we use for inexing. This makes the feature more reliable since, we now used the same well tested functionaties as things like go to definition. Unfortunately, we might index the particular files the second time, because the basic indexing caches are reversed and finding all symbols in a file from those maps was much slower than just doing it the second time. We later cache that information the same way we did in the previous tree view implementation. So we won't use more memory and running the benchmarks show that the new approach is similarily fast: ``` [info] Benchmark Mode Cnt Score Error Units [info] ClasspathSymbolsBench.run ss 10 542.090 ± 61.953 ms/op ``` as compared to the previous: ``` [info] Benchmark Mode Cnt Score Error Units [info] ClasspathSymbolsBench.run ss 10 477.283 ± 93.933 ms/op ``` Also it seems we index much more information, since mtags index all symbols, which would also explain the slight difference in timings as I think the difference was 1300 symbols previously and 17000 after this change (number of symbols detected in the benchmarks) Fixes https://github.com/scalameta/metals/issues/2859 --- .../scala/bench/ClasspathSymbolsBench.scala | 18 +- .../metals/JavaInteractiveSemanticdb.scala | 4 +- .../internal/metals/MetalsLspService.scala | 4 +- .../internal/metals/MetalsSymbolSearch.scala | 2 +- .../metals/ScalaVersionSelector.scala | 15 +- .../metals/StandaloneSymbolSearch.scala | 2 +- .../meta/internal/tvp/ClasspathSymbols.scala | 231 ------------------ .../meta/internal/tvp/ClasspathTreeView.scala | 8 +- .../meta/internal/tvp/IndexedSymbols.scala | 199 +++++++++++++++ .../internal/tvp/MetalsTreeViewProvider.scala | 69 ++++-- .../internal/mtags/GlobalSymbolIndex.scala | 14 +- .../scala/meta/internal/mtags/JavaMtags.scala | 17 +- .../scala/meta/internal/mtags/Mtags.scala | 32 ++- .../internal/mtags/OnDemandSymbolIndex.scala | 16 ++ .../mtags/ScalametaCommonEnrichments.scala | 3 + .../scala/meta/internal/mtags/Symbol.scala | 15 +- .../internal/mtags/SymbolIndexBucket.scala | 86 +++++-- .../scala/tests/TestingSymbolSearch.scala | 2 +- tests/unit/src/main/scala/tests/Library.scala | 11 +- .../src/main/scala/tests/QuickBuild.scala | 2 +- .../src/main/scala/tests/TestingServer.scala | 9 +- .../test/scala/tests/JavaToplevelSuite.scala | 2 +- .../test/scala/tests/ScalaToplevelSuite.scala | 2 +- .../scala/tests/ToplevelLibrarySuite.scala | 8 +- .../src/test/scala/tests/ToplevelSuite.scala | 2 +- .../test/scala/tests/TreeViewLspSuite.scala | 130 +++++----- 26 files changed, 528 insertions(+), 375 deletions(-) delete mode 100644 metals/src/main/scala/scala/meta/internal/tvp/ClasspathSymbols.scala create mode 100644 metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala diff --git a/metals-bench/src/main/scala/bench/ClasspathSymbolsBench.scala b/metals-bench/src/main/scala/bench/ClasspathSymbolsBench.scala index 4dd9048bcf5..c22d32d4add 100644 --- a/metals-bench/src/main/scala/bench/ClasspathSymbolsBench.scala +++ b/metals-bench/src/main/scala/bench/ClasspathSymbolsBench.scala @@ -2,7 +2,11 @@ package bench import java.util.concurrent.TimeUnit -import scala.meta.internal.tvp.ClasspathSymbols +import scala.meta.dialects +import scala.meta.internal.metals.EmptyReportContext +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.mtags.OnDemandSymbolIndex +import scala.meta.internal.tvp.IndexedSymbols import scala.meta.io.AbsolutePath import org.openjdk.jmh.annotations.Benchmark @@ -21,7 +25,7 @@ class ClasspathSymbolsBench { @Setup def setup(): Unit = { - classpath = Library.cats + classpath = Library.catsSources.filter(_.filename.contains("sources")) } @TearDown @@ -31,8 +35,14 @@ class ClasspathSymbolsBench { @BenchmarkMode(Array(Mode.SingleShotTime)) @OutputTimeUnit(TimeUnit.MILLISECONDS) def run(): Unit = { - val jars = new ClasspathSymbols() - classpath.foreach { jar => jars.symbols(jar, "cats/") } + implicit val reporting = EmptyReportContext + val jars = new IndexedSymbols( + OnDemandSymbolIndex.empty(), + isStatisticsEnabled = false, + ) + classpath.foreach { jar => + jars.jarSymbols(jar, "cats/", dialects.Scala213) + } } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/JavaInteractiveSemanticdb.scala b/metals/src/main/scala/scala/meta/internal/metals/JavaInteractiveSemanticdb.scala index 5e71b272dc7..bc1d4f50896 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/JavaInteractiveSemanticdb.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/JavaInteractiveSemanticdb.scala @@ -225,6 +225,9 @@ object JdkVersion { } def fromReleaseFile(javaHome: AbsolutePath): Option[JdkVersion] = + fromReleaseFileString(javaHome).flatMap(f => parse(f)) + + def fromReleaseFileString(javaHome: AbsolutePath): Option[String] = Seq(javaHome.resolve("release"), javaHome.parent.resolve("release")) .filter(_.exists) .flatMap { releaseFile => @@ -233,7 +236,6 @@ object JdkVersion { props.asScala .get("JAVA_VERSION") .map(_.stripPrefix("\"").stripSuffix("\"")) - .flatMap(jv => JdkVersion.parse(jv)) } .headOption diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index f6dd6e39cf0..7c66d0cf5e8 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -806,9 +806,9 @@ class MetalsLspService( buildTargets, () => buildClient.ongoingCompilations(), definitionIndex, - id => compilations.compileTarget(id), - () => bspSession.map(_.mainConnectionIsBloop).getOrElse(false), clientConfig.initialConfig.statistics, + optJavaHome, + scalaVersionSelector, ) private val popupChoiceReset: PopupChoiceReset = new PopupChoiceReset( diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala index d0b32541299..b6ac451cc56 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsSymbolSearch.scala @@ -82,7 +82,7 @@ class MetalsSymbolSearch( } else { dependencySourceCache.getOrElseUpdate( path, - Mtags.toplevels(input).asJava, + Mtags.topLevelSymbols(input).asJava, ) } }) diff --git a/metals/src/main/scala/scala/meta/internal/metals/ScalaVersionSelector.scala b/metals/src/main/scala/scala/meta/internal/metals/ScalaVersionSelector.scala index 4e92e333e49..22666a220f3 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ScalaVersionSelector.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ScalaVersionSelector.scala @@ -57,22 +57,21 @@ class ScalaVersionSelector( ) } - def getDialect(path: AbsolutePath): Dialect = { - - def dialectFromBuildTarget = buildTargets - .inverseSources(path) - .flatMap(id => buildTargets.scalaTarget(id)) - .map(_.dialect(path)) + def dialectFromBuildTarget(path: AbsolutePath): Option[Dialect] = buildTargets + .inverseSources(path) + .flatMap(id => buildTargets.scalaTarget(id)) + .map(_.dialect(path)) + def getDialect(path: AbsolutePath): Dialect = { Option(path.extension) match { case Some("scala") => - dialectFromBuildTarget.getOrElse( + dialectFromBuildTarget(path).getOrElse( fallbackDialect(isAmmonite = false) ) case Some("sbt") => dialects.Sbt case Some("sc") => // worksheets support Scala 3, but ammonite scripts do not - val dialect = dialectFromBuildTarget.getOrElse( + val dialect = dialectFromBuildTarget(path).getOrElse( fallbackDialect(isAmmonite = path.isAmmoniteScript) ) dialect diff --git a/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala b/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala index e9400741330..0f3f41fbdce 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala @@ -90,7 +90,7 @@ class StandaloneSymbolSearch( val input = symDef.path.toInput dependencySourceCache.getOrElseUpdate( symDef.path, - mtags.toplevels(input).asJava, + mtags.topLevelSymbols(input).asJava, ) } .orElse(workspaceFallback.map(_.definitionSourceToplevels(sym, source))) diff --git a/metals/src/main/scala/scala/meta/internal/tvp/ClasspathSymbols.scala b/metals/src/main/scala/scala/meta/internal/tvp/ClasspathSymbols.scala deleted file mode 100644 index fea522441c6..00000000000 --- a/metals/src/main/scala/scala/meta/internal/tvp/ClasspathSymbols.scala +++ /dev/null @@ -1,231 +0,0 @@ -package scala.meta.internal.tvp - -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.SimpleFileVisitor -import java.nio.file.attribute.BasicFileAttributes - -import scala.collection.concurrent.TrieMap -import scala.collection.mutable -import scala.tools.asm.tree.ClassNode -import scala.tools.scalap.scalax.rules.scalasig.SymbolInfoSymbol -import scala.util.control.NonFatal - -import scala.meta.internal.classpath.ClasspathIndex -import scala.meta.internal.io._ -import scala.meta.internal.javacp.Javacp -import scala.meta.internal.metacp._ -import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.Time -import scala.meta.internal.metals.Timer -import scala.meta.internal.mtags.Symbol -import scala.meta.internal.scalacp.SymlinkChildren -import scala.meta.internal.scalacp.Synthetics -import scala.meta.internal.semanticdb.Scala._ -import scala.meta.internal.semanticdb.Scala.{Descriptor => d} -import scala.meta.internal.semanticdb.SymbolInformation -import scala.meta.internal.semanticdb.SymbolInformation.{Kind => k} -import scala.meta.internal.semanticdb._ -import scala.meta.io.AbsolutePath -import scala.meta.io.Classpath - -/** - * Extracts SemanticDB symbols from `*.class` files in jars. - */ -class ClasspathSymbols(isStatisticsEnabled: Boolean = false) { - private val cache = TrieMap.empty[ - AbsolutePath, - TrieMap[String, Array[TreeViewSymbolInformation]], - ] - - def clearCache(path: AbsolutePath): Unit = { - cache.remove(path) - } - - def reset(): Unit = { - cache.clear() - } - - def symbols( - in: AbsolutePath, - symbol: String, - ): Iterator[TreeViewSymbolInformation] = { - val symbols = cache.getOrElseUpdate(in, TrieMap.empty) - symbols - .getOrElseUpdate( - symbol.asSymbol.enclosingPackage.value, { - val timer = new Timer(Time.system) - val result = loadSymbols(in, symbol) - if (isStatisticsEnabled) { - scribe.info(s"$timer - $in/!$symbol") - } - result - }, - ) - .iterator - } - - // Names of methods that are autogenerated by the Scala compiler. - private val isSyntheticMethodName = Set( - "productElement", "productElementName", "equals", "canEqual", "hashCode", - "productIterator", "toString", "productPrefix", "unapply", "apply", "copy", - "productArity", "readResolve", - ) - - private def isRelevant(info: SymbolInformation): Boolean = - !info.isPrivate && - !info.isPrivateThis && { - info.kind match { - case k.METHOD => - val isVarSetter = info.isVar && info.displayName.endsWith("_=") - !isVarSetter && - !isSyntheticMethodName(info.displayName) && - !info.displayName.contains("$default$") - case k.CLASS | k.INTERFACE | k.OBJECT | k.PACKAGE_OBJECT | k.TRAIT | - k.MACRO | k.FIELD => - true - case _ => false - } - } - - private def loadSymbols( - in: AbsolutePath, - symbol: String, - ): Array[TreeViewSymbolInformation] = { - val index = ClasspathIndex(Classpath(in), false) - val buf = Array.newBuilder[TreeViewSymbolInformation] - def list(root: AbsolutePath): Unit = { - val dir = - if ( - symbol == Scala.Symbols.RootPackage || symbol == Scala.Symbols.EmptyPackage - ) root - else root.resolve(Symbol(symbol).enclosingPackage.value) - - dir.list.foreach { - case path if path.isDirectory => - buf ++= dummyClassfiles(root.toNIO, path.toNIO) - - case path if isClassfile(path.toNIO) => - try { - val node = path.toClassNode - classfileSymbols( - node, - index, - { i => - if (isRelevant(i)) { - buf += TreeViewSymbolInformation( - i.symbol, - i.kind, - i.properties, - ) - } - }, - ) - } catch { - case NonFatal(ex) => - scribe.warn(s"error: can't convert $path in $in", ex) - } - - case _ => - } - - } - if (in.extension == "jar") { - FileIO.withJarFileSystem(in, create = false, close = true)(list) - } else { - list(in) - } - buf.result() - } - - /** - * Recursively look for classfiles in this directory to determine if this - * path maps to a non-empty package symbol. - */ - private def dummyClassfiles( - root: Path, - path: Path, - ): collection.Seq[TreeViewSymbolInformation] = { - val result = mutable.ListBuffer.empty[TreeViewSymbolInformation] - def isDone = result.lengthCompare(1) > 0 - Files.walkFileTree( - path, - new SimpleFileVisitor[Path] { - override def preVisitDirectory( - dir: Path, - attrs: BasicFileAttributes, - ): FileVisitResult = - if (isDone) FileVisitResult.SKIP_SIBLINGS - else FileVisitResult.CONTINUE - override def visitFile( - child: Path, - attrs: BasicFileAttributes, - ): FileVisitResult = { - if (isDone) FileVisitResult.CONTINUE - else { - if (isClassfile(child) && !child.filename.contains("$")) { - val relpath = root.relativize(child).iterator().asScala - val dummySymbol = relpath.foldLeft(Symbols.RootPackage) { - case (owner, path) => - Symbols.Global( - owner, - d.Package(path.filename.stripSuffix("/")), - ) - } - result += TreeViewSymbolInformation(dummySymbol, k.CLASS, 0) - } - FileVisitResult.CONTINUE - } - - } - }, - ) - result - } - - private def isClassfile(path: Path): Boolean = { - PathIO.extension(path) == "class" && Files.size(path) > 0 - } - - private def classfileSymbols( - node: ClassNode, - index: ClasspathIndex, - fn: SymbolInformation => Unit, - ): Unit = { - node.scalaSig match { - case Some(scalaSig) => - import ScalacpCopyPaste._ - def infos(sym: SymbolInfoSymbol): List[SymbolInformation] = { - if (sym.isSemanticdbLocal) return Nil - if (sym.isUseless) return Nil - val ssym = sym.ssym - if (ssym.contains("$extension")) return Nil - val sinfo = sym.toSymbolInformation(SymlinkChildren) - if (sym.isUsefulField && sym.isMutable) { - List(sinfo) ++ Synthetics.setterInfos(sinfo, SymlinkChildren) - } else { - List(sinfo) - } - } - scalaSig.scalaSig.symbols.foreach { - case sym: SymbolInfoSymbol if !sym.isSynthetic => - infos(sym).foreach(fn) - case _ => - } - case None => - val attrs = - if (node.attrs != null) node.attrs.asScala else Nil - if (attrs.exists(_.`type` == "Scala")) { - None - } else { - val innerClassNode = - node.innerClasses.asScala.find(_.name == node.name) - if (innerClassNode.isEmpty && node.name != "module-info") { - Javacp.parse(node, index).infos.foreach(fn) - } - } - } - } - -} diff --git a/metals/src/main/scala/scala/meta/internal/tvp/ClasspathTreeView.scala b/metals/src/main/scala/scala/meta/internal/tvp/ClasspathTreeView.scala index e12d98ec5e9..eaea2c7ba9d 100644 --- a/metals/src/main/scala/scala/meta/internal/tvp/ClasspathTreeView.scala +++ b/metals/src/main/scala/scala/meta/internal/tvp/ClasspathTreeView.scala @@ -78,7 +78,9 @@ class ClasspathTreeView[Value, Key]( val label = if (child.kind.isPackage) { displayName + "/" - } else if (child.kind.isMethod && !child.isVal && !child.isVar) { + } else if ( + child.kind.isConstructor || (child.kind.isMethod && !child.isVal && !child.isVar) + ) { displayName + "()" } else { displayName @@ -87,6 +89,7 @@ class ClasspathTreeView[Value, Key]( // Get the children of this child to determine its collapse state. val grandChildren = transitiveChildren.filter(_.symbol.owner == child.symbol) + val collapseState = if (!childHasSiblings && grandChildren.nonEmpty) MetalsTreeItemCollapseState.expanded @@ -106,6 +109,7 @@ class ClasspathTreeView[Value, Key]( case k.TRAIT => "trait" case k.CLASS => "class" case k.INTERFACE => "interface" + case k.CONSTRUCTOR => "method" case k.METHOD | k.MACRO => if (child.properties.isVal) "val" else if (child.properties.isVar) "var" @@ -113,6 +117,8 @@ class ClasspathTreeView[Value, Key]( case k.FIELD => if (child.properties.isEnum) "enum" else "field" + case k.TYPE_PARAMETER => "type" + case k.TYPE => "type" case _ => null } diff --git a/metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala b/metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala new file mode 100644 index 00000000000..db72d4b9ae6 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala @@ -0,0 +1,199 @@ +package scala.meta.internal.tvp + +import scala.collection.concurrent.TrieMap + +import scala.meta.Dialect +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.Time +import scala.meta.internal.metals.Timer +import scala.meta.internal.mtags.GlobalSymbolIndex +import scala.meta.internal.mtags.Symbol +import scala.meta.internal.mtags.SymbolDefinition +import scala.meta.internal.semanticdb.SymbolInformation +import scala.meta.io.AbsolutePath + +class IndexedSymbols(index: GlobalSymbolIndex, isStatisticsEnabled: Boolean) { + + // Used for workspace, is eager + private val workspaceCache = TrieMap.empty[ + AbsolutePath, + Array[TreeViewSymbolInformation], + ] + + type TopLevel = SymbolDefinition + type AllSymbols = Array[TreeViewSymbolInformation] + // Used for dependencies, is lazy, TopLevel is changed to AllSymbols when needed + private val jarCache = TrieMap.empty[ + AbsolutePath, + TrieMap[String, Either[TopLevel, AllSymbols]], + ] + + def clearCache(path: AbsolutePath): Unit = { + jarCache.remove(path) + workspaceCache.remove(path) + } + + def reset(): Unit = { + jarCache.clear() + workspaceCache.clear() + } + + def withTimer[T](label: String)(f: => T): T = { + val timer = new Timer(Time.system) + val result = f + if (isStatisticsEnabled) { + scribe.info(s"$timer - $label") + } + result + } + + /** + * We load all symbols for workspace eagerly + * + * @param in the input file to index + * @param dialect dialect to parse the file with + * @return list of tree view symbols within the file + */ + def workspaceSymbols( + in: AbsolutePath, + symbol: String, + dialect: Dialect, + ): Iterator[TreeViewSymbolInformation] = withTimer(s"$in/!$symbol") { + val syms = workspaceCache + .getOrElseUpdate( + in, + members(in, dialect).map(toTreeView), + ) + if (Symbol(symbol).isRootPackage) syms.iterator + else + syms.collect { + case defn if defn.symbol.startsWith(symbol) => defn + }.iterator + } + + /** + * Lazily calculate symbols in a jar. + * + * @param in the input jar + * @param symbol symbol we want to calculate members for + * @param dialect dialect to use for the jar + * @return all topelevels for root and up to grandchildren for other symbols + */ + def jarSymbols( + in: AbsolutePath, + symbol: String, + dialect: Dialect, + ): Iterator[TreeViewSymbolInformation] = withTimer(s"$in/!$symbol") { + lazy val potentialSourceJar = + in.parent.resolve(in.filename.replace(".jar", "-sources.jar")) + if (!in.isSourcesJar && !potentialSourceJar.exists) { + Iterator.empty[TreeViewSymbolInformation] + } else { + val realIn = if (!in.isSourcesJar) potentialSourceJar else in + val jarSymbols = jarCache.getOrElseUpdate( + realIn, { + val toplevels = index + .toplevelsAt(in, dialect) + .map(defn => defn.definitionSymbol.value -> Left(defn)) + + TrieMap.empty[ + String, + Either[TopLevel, AllSymbols], + ] ++ toplevels + }, + ) + + def toplevelOwner(symbol: Symbol): Symbol = { + if (symbol.isPackage) symbol + else if (jarSymbols.contains(symbol.value)) symbol + else if (symbol.owner.isPackage) symbol + else toplevelOwner(symbol.owner) + } + + val parsedSymbol = Symbol(symbol) + // if it's a package we'll collect all the children + if (parsedSymbol.isPackage) { + jarSymbols.values + .collect { + // root package doesn't need to calculate any members, they will be calculated lazily + case Left(defn) if parsedSymbol.isRootPackage => + Array(toTreeView(defn)) + case Right(list) if parsedSymbol.isRootPackage => list + case cached => + symbolsForPackage(cached, dialect, jarSymbols, parsedSymbol) + } + .flatten + .iterator + } else { + jarSymbols.get(toplevelOwner(Symbol(symbol)).value) match { + case Some(Left(toplevelOnly)) => + val allSymbols = members(toplevelOnly.path, dialect).map(toTreeView) + jarSymbols.put(symbol, Right(allSymbols)) + allSymbols.iterator + case Some(Right(calculated)) => + calculated.iterator + case _ => Iterator.empty[TreeViewSymbolInformation] + } + } + } + } + + private def symbolsForPackage( + cached: Either[TopLevel, AllSymbols], + dialect: Dialect, + jarSymbols: TrieMap[String, Either[TopLevel, AllSymbols]], + symbol: Symbol, + ): Array[TreeViewSymbolInformation] = + cached match { + case Left(toplevel) + if toplevel.definitionSymbol.value.startsWith(symbol.value) => + // we need to check if we have grandchildren and the nodes are exapandable + if (toplevel.definitionSymbol.owner == symbol) { + val children = + members(toplevel.path, dialect).map(toTreeView) + jarSymbols.put( + toplevel.definitionSymbol.value, + Right(children), + ) + children + } else { + Array(toTreeView(toplevel)) + } + case Right(allSymbols) => allSymbols + case _ => Array.empty[TreeViewSymbolInformation] + } + + private def members( + path: AbsolutePath, + dialect: Dialect, + ): Array[SymbolDefinition] = { + index + .symbolsAt(path, dialect) + .filter(defn => + defn.kind.isEmpty || !defn.kind.exists(kind => + kind.isParameter || kind.isTypeParameter + ) + ) + .toArray + } + + private def toTreeView( + symDef: SymbolDefinition + ): TreeViewSymbolInformation = { + val kind = symDef.kind match { + case Some(SymbolInformation.Kind.UNKNOWN_KIND) | None => + if (symDef.definitionSymbol.isMethod) SymbolInformation.Kind.METHOD + else if (symDef.definitionSymbol.isType) SymbolInformation.Kind.CLASS + else if (symDef.definitionSymbol.isTypeParameter) + SymbolInformation.Kind.TYPE_PARAMETER + else SymbolInformation.Kind.OBJECT + case Some(knownKind) => knownKind + } + TreeViewSymbolInformation( + symDef.definitionSymbol.value, + kind, + symDef.properties, + ) + } + +} diff --git a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala index 0036ea32cd6..996a30bc2de 100644 --- a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala @@ -7,6 +7,7 @@ import java.util.concurrent.atomic.AtomicBoolean import scala.collection.concurrent.TrieMap +import scala.meta.Dialect import scala.meta.dialects import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals._ @@ -128,13 +129,27 @@ class MetalsTreeViewProvider( path: AbsolutePath, pos: l.Position, ): Option[TreeViewNodeRevealResult] = { + def dialectFromWorkspace = + getFolderTreeViewProviders().iterator + .map(_.dialectOf(path)) + .collectFirst { case Some(dialect) => + dialect + } + .getOrElse(dialects.Scala213) + val dialect = + if (path.isJarFileSystem) { + path.jarPath + .map { p => + ScalaVersions.dialectForDependencyJar(p.filename) + } + .getOrElse(dialects.Scala3) + } else dialectFromWorkspace val input = path.toInput val occurrences = Mtags .allToplevels( input, - // TreeViewProvider doesn't work with Scala 3 - see #2859 - dialects.Scala213, + dialect, ) .occurrences .filterNot(_.symbol.isPackage) @@ -238,12 +253,19 @@ class FolderTreeViewProvider( buildTargets: BuildTargets, compilations: () => TreeViewCompilations, definitionIndex: GlobalSymbolIndex, - doCompile: BuildTargetIdentifier => Unit, - isBloop: () => Boolean, statistics: StatisticsConfig, + userJavaHome: Option[AbsolutePath], + scalaVersionSelector: ScalaVersionSelector, ) { - private val classpath = new ClasspathSymbols( - isStatisticsEnabled = statistics.isTreeView + def dialectOf(path: AbsolutePath): Option[Dialect] = + scalaVersionSelector.dialectFromBuildTarget(path) + private val maybeUsedJdkVersion = + userJavaHome.flatMap { path => + JdkVersion.fromReleaseFileString(path) + } + private val classpath = new IndexedSymbols( + definitionIndex, + isStatisticsEnabled = statistics.isTreeView, ) private val isVisible = TrieMap.empty[String, Boolean].withDefaultValue(false) private val isCollapsed = TrieMap.empty[BuildTargetIdentifier, Boolean] @@ -258,10 +280,20 @@ class FolderTreeViewProvider( identity, _.toURI.toString(), _.toAbsolutePath, - _.filename, + path => { + if (path.filename == JdkSources.zipFileName) { + maybeUsedJdkVersion + .map(ver => s"jdk-${ver}-sources") + .getOrElse("jdk-sources") + } else + path.filename + }, _.toString, - () => buildTargets.allWorkspaceJars, - (path, symbol) => classpath.symbols(path, symbol), + () => buildTargets.allSourceJars, + (path, symbol) => { + val dialect = ScalaVersions.dialectForDependencyJar(path.filename) + classpath.jarSymbols(path, symbol, dialect) + }, ) val projects = new ClasspathTreeView[BuildTarget, BuildTargetIdentifier]( @@ -281,11 +313,12 @@ class FolderTreeViewProvider( ) }, { (id, symbol) => - if (isBloop()) doCompile(id) - buildTargets - .targetClassDirectories(id) - .flatMap(cd => classpath.symbols(cd.toAbsolutePath, symbol)) - .iterator + val tops = for { + scalaTarget <- buildTargets.scalaTarget(id).iterator + source <- buildTargets.buildTargetSources(id) + dialect = scalaTarget.dialect(source) + } yield classpath.workspaceSymbols(source, symbol, dialect) + tops.flatten }, ) @@ -381,18 +414,22 @@ class FolderTreeViewProvider( def revealResult( path: AbsolutePath, closestSymbol: SymbolOccurrence, - ): Option[List[String]] = + ): Option[List[String]] = { if (path.isDependencySource(folder.path) || path.isJarFileSystem) { buildTargets .inferBuildTarget(List(Symbol(closestSymbol.symbol).toplevel)) .map { inferred => - libraries.toUri(inferred.jar, inferred.symbol).parentChain + val sourceJar = inferred.jar.parent.resolve( + inferred.jar.filename.replace(".jar", "-sources.jar") + ) + libraries.toUri(sourceJar, inferred.symbol).parentChain } } else { buildTargets .inverseSources(path) .map(id => projects.toUri(id, closestSymbol.symbol).parentChain) } + } private def ongoingCompilations: Array[TreeViewNode] = { compilations().buildTargets.flatMap(ongoingCompileNode).toArray diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/GlobalSymbolIndex.scala b/mtags/src/main/scala/scala/meta/internal/mtags/GlobalSymbolIndex.scala index cd94563fcf7..83ed12bb2e7 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/GlobalSymbolIndex.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/GlobalSymbolIndex.scala @@ -37,6 +37,16 @@ trait GlobalSymbolIndex { def definitions(symbol: mtags.Symbol): List[SymbolDefinition] + def toplevelsAt( + path: AbsolutePath, + dialect: Dialect + ): List[SymbolDefinition] = Nil + + def symbolsAt( + path: AbsolutePath, + dialect: Dialect + ): List[SymbolDefinition] = Nil + /** * Add an individual Java or Scala source file to the index. * @@ -105,7 +115,9 @@ case class SymbolDefinition( definitionSymbol: Symbol, path: AbsolutePath, dialect: Dialect, - range: Option[s.Range] + range: Option[s.Range], + kind: Option[s.SymbolInformation.Kind], + properties: Int ) { def isExact: Boolean = querySymbol == definitionSymbol diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala index 89626be9e8b..fde48e1369c 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala @@ -182,15 +182,18 @@ class JavaMtags(virtualFile: Input.VirtualFile, includeMembers: Boolean) case _ => 0 } val pos = toRangePosition(line, name) - val kind: Kind = m match { - case _: JavaMethod => Kind.METHOD - case _: JavaField => Kind.FIELD + val (kind: Kind, properties: Int) = m match { + case _: JavaMethod => (Kind.METHOD, 0) + case field: JavaField if field.isEnumConstant() => + (Kind.FIELD, Property.ENUM.value) + case _: JavaField => + (Kind.FIELD, 0) case c: JavaClass => - if (c.isInterface) Kind.INTERFACE - else Kind.CLASS - case _ => Kind.UNKNOWN_KIND + if (c.isInterface) (Kind.INTERFACE, 0) + else (Kind.CLASS, 0) + case _ => (Kind.UNKNOWN_KIND, 0) } - term(name, pos, kind, 0) + term(name, pos, kind, properties) } implicit class XtensionJavaModel(m: JavaModel) { diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala index af9233fbbb5..5d932734958 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala @@ -17,7 +17,7 @@ final class Mtags(implicit rc: ReportContext) { def toplevels( input: Input.VirtualFile, dialect: Dialect = dialects.Scala213 - ): List[String] = { + ): TextDocument = { val language = input.toLanguage if (language.isJava || language.isScala) { @@ -32,18 +32,22 @@ final class Mtags(implicit rc: ReportContext) { dialect ) addLines(language, input.text) - mtags - .index() - .occurrences - .iterator - .filterNot(_.symbol.isPackage) - .map(_.symbol) - .toList + mtags.index() } else { - Nil + TextDocument() } } + def topLevelSymbols( + input: Input.VirtualFile, + dialect: Dialect = dialects.Scala213 + ): List[String] = { + toplevels(input, dialect).occurrences.iterator + .filterNot(_.symbol.isPackage) + .map(_.symbol) + .toList + } + def index( language: Language, input: Input.VirtualFile, @@ -107,11 +111,19 @@ object Mtags { TextDocument() } } + def toplevels( input: Input.VirtualFile, dialect: Dialect = dialects.Scala213 - )(implicit rc: ReportContext = EmptyReportContext): List[String] = { + )(implicit rc: ReportContext = EmptyReportContext): TextDocument = { new Mtags().toplevels(input, dialect) } + def topLevelSymbols( + input: Input.VirtualFile, + dialect: Dialect = dialects.Scala213 + )(implicit rc: ReportContext = EmptyReportContext): List[String] = { + new Mtags().topLevelSymbols(input, dialect) + } + } diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala b/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala index 8f5beeca2c2..74785de6536 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala @@ -62,6 +62,22 @@ final class OnDemandSymbolIndex( List.empty } + override def toplevelsAt( + path: AbsolutePath, + dialect: Dialect + ): List[SymbolDefinition] = { + val bucket = getOrCreateBucket(dialect) + bucket.toplevelsAt(path) + } + + override def symbolsAt( + path: AbsolutePath, + dialect: Dialect + ): List[SymbolDefinition] = { + val bucket = getOrCreateBucket(dialect) + bucket.symbolsAt(path) + } + override def addSourceDirectory( dir: AbsolutePath, dialect: Dialect diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/ScalametaCommonEnrichments.scala b/mtags/src/main/scala/scala/meta/internal/mtags/ScalametaCommonEnrichments.scala index 42c53dd498f..1d29c485126 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/ScalametaCommonEnrichments.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ScalametaCommonEnrichments.scala @@ -354,6 +354,9 @@ trait ScalametaCommonEnrichments extends CommonMtagsEnrichments { def isScalaScript: Boolean = { filename.endsWith(".sc") } + def isSourcesJar: Boolean = { + filename.endsWith("-sources.jar") + } def isMill: Boolean = isScalaScript && filename == "build.sc" def isAmmoniteScript: Boolean = isScalaScript && !isWorksheet && !isMill diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala index d0282b6f60a..2e5097e080a 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala @@ -29,7 +29,7 @@ final class Symbol private (val value: String) { def isPackage: Boolean = desc.isPackage def isParameter: Boolean = desc.isParameter def isTypeParameter: Boolean = desc.isTypeParameter - private def desc: Descriptor = value.desc + private lazy val desc: Descriptor = value.desc def owner: Symbol = Symbol(value.owner) def displayName: String = desc.name.value @@ -41,13 +41,14 @@ final class Symbol private (val value: String) { } loop(this) } - def enclosingPackageChain: String = { - def loop(s: Symbol): List[String] = { - if (s.isPackage) Nil - else s.displayName :: loop(s.owner) + def enclosingPackageChain: List[Symbol] = { + def loop(s: Symbol): List[Symbol] = { + if (s.isRootPackage) Nil + else if (s.isPackage) s :: loop(s.owner) + else loop(s.owner) } - if (isPackage || isNone) displayName - else loop(this).reverse.mkString(".") + if (!owner.isPackage || isNone) Nil + else loop(this).reverse } def toplevel: Symbol = { if (value.isNone) this diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/SymbolIndexBucket.scala b/mtags/src/main/scala/scala/meta/internal/mtags/SymbolIndexBucket.scala index ce0431b38a6..101dbea1079 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/SymbolIndexBucket.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/SymbolIndexBucket.scala @@ -51,6 +51,58 @@ class SymbolIndexBucket( def close(): Unit = sourceJars.close() + def toplevelsAt(path: AbsolutePath): List[SymbolDefinition] = { + + def indexJar(jar: AbsolutePath) = { + FileIO.withJarFileSystem(jar, create = false) { root => + try { + root.listRecursive.toList.collect { + case source if source.isFile => + (source, mtags.toplevels(source.toInput, dialect).symbols) + } + } catch { + // this happens in broken jars since file from FileWalker should exists + case _: UncheckedIOException => Nil + } + } + } + + val pathSymbolInfos = if (path.isSourcesJar) { + indexJar(path) + } else { + List((path, mtags.toplevels(path.toInput, dialect).symbols)) + } + pathSymbolInfos.collect { case (path, infos) => + infos.map { info => + SymbolDefinition( + Symbol("_empty_"), + Symbol(info.symbol), + path, + dialect, + None, + Some(info.kind), + info.properties + ) + } + }.flatten + } + + def symbolsAt(path: AbsolutePath): List[SymbolDefinition] = { + val document = allSymbols(path) + document.symbols.map { info => + SymbolDefinition( + Symbol("_empty_"), + Symbol(info.symbol), + path, + dialect, + None, + Some(info.kind), + info.properties + ) + }.toList + + } + def addSourceDirectory(dir: AbsolutePath): List[(String, AbsolutePath)] = { if (sourceJars.addEntry(dir.toNIO)) { dir.listRecursive.toList.flatMap { @@ -106,8 +158,7 @@ class SymbolIndexBucket( sourceDirectory: Option[AbsolutePath], fromSourceJar: Option[AbsolutePath] = None ): List[String] = { - val uri = source.toIdeallyRelativeURI(sourceDirectory) - val symbols = indexSource(source, uri, dialect) + val symbols = indexSource(source, dialect, sourceDirectory) val patched = fromSourceJar match { @@ -125,12 +176,13 @@ class SymbolIndexBucket( private def indexSource( source: AbsolutePath, - uri: String, - dialect: Dialect + dialect: Dialect, + sourceDirectory: Option[AbsolutePath] ): List[String] = { + val uri = source.toIdeallyRelativeURI(sourceDirectory) val text = FileIO.slurp(source, StandardCharsets.UTF_8) val input = Input.VirtualFile(uri, text) - val sourceToplevels = mtags.toplevels(input, dialect) + val sourceToplevels = mtags.topLevelSymbols(input, dialect) if (source.isAmmoniteScript) sourceToplevels else @@ -221,7 +273,9 @@ class SymbolIndexBucket( definitionSymbol = symbol, path = location.path, dialect = dialect, - range = location.range + range = location.range, + kind = None, + properties = 0 ) }.toList } @@ -257,6 +311,17 @@ class SymbolIndexBucket( } } + private def allSymbols(path: AbsolutePath): s.TextDocument = { + val language = path.toLanguage + val toIndexSource0 = toIndexSource(path) + val input = toIndexSource0.toInput + + stdLibPatches.patchDocument( + path, + mtags.index(language, input, dialect) + ) + } + // similar as addSourceFile except indexes all global symbols instead of // only non-trivial toplevel symbols. private def addMtagsSourceFile( @@ -265,14 +330,7 @@ class SymbolIndexBucket( ): Unit = try { val docs: s.TextDocuments = PathIO.extension(file.toNIO) match { case "scala" | "java" | "sc" => - val language = file.toLanguage - val toIndexSource0 = toIndexSource(file) - val input = toIndexSource0.toInput - val document = - stdLibPatches.patchDocument( - file, - mtags.index(language, input, dialect) - ) + val document = allSymbols(file) s.TextDocuments(List(document)) case _ => s.TextDocuments(Nil) diff --git a/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala b/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala index 9619e0aa06a..0312f561cef 100644 --- a/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala +++ b/tests/mtest/src/main/scala/tests/TestingSymbolSearch.scala @@ -77,7 +77,7 @@ class TestingSymbolSearch( filename, content, ) - Mtags.toplevels(input).asJava + Mtags.topLevelSymbols(input).asJava } } diff --git a/tests/unit/src/main/scala/tests/Library.scala b/tests/unit/src/main/scala/tests/Library.scala index a284ec4ba6d..8e66808ee66 100644 --- a/tests/unit/src/main/scala/tests/Library.scala +++ b/tests/unit/src/main/scala/tests/Library.scala @@ -25,8 +25,8 @@ object Library { Classpath(PackageIndex.bootClasspath.map(AbsolutePath.apply)), Classpath(JdkSources().right.get :: Nil), ) - def cats: Seq[AbsolutePath] = - fetch("org.typelevel", "cats-core_2.12", "2.0.0-M4") + def catsSources: Seq[AbsolutePath] = + fetchSources("org.typelevel", "cats-core_2.12", "2.0.0-M4") def scala3: Library = { val binaryVersion = @@ -102,12 +102,17 @@ object Library { ) } - def fetch(org: String, artifact: String, version: String): Seq[AbsolutePath] = + def fetchSources( + org: String, + artifact: String, + version: String, + ): Seq[AbsolutePath] = Fetch .create() .withDependencies( Dependency.of(org, artifact, version).withTransitive(false) ) + .withClassifiers(Set("sources").asJava) .fetch() .asScala .toSeq diff --git a/tests/unit/src/main/scala/tests/QuickBuild.scala b/tests/unit/src/main/scala/tests/QuickBuild.scala index e7b455d53a2..36e90b26feb 100644 --- a/tests/unit/src/main/scala/tests/QuickBuild.scala +++ b/tests/unit/src/main/scala/tests/QuickBuild.scala @@ -122,7 +122,7 @@ case class QuickBuild( val scalaDependencies = if (ScalaVersions.isScala3Version(scalaVersion)) { Array( - s"org.scala-lang:scala-library:2.13.1", + s"org.scala-lang:scala-library:2.13.11", s"org.scala-lang:scala3-library_$binaryVersion:$scalaVersion", ) } else { diff --git a/tests/unit/src/main/scala/tests/TestingServer.scala b/tests/unit/src/main/scala/tests/TestingServer.scala index f9d60750e9b..1533452c8e6 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -1779,12 +1779,12 @@ final case class TestingServer( } def jar(filename: String): String = { - server.buildTargets.allWorkspaceJars + server.buildTargets.allSourceJars .find(_.filename.contains(filename)) .map(_.toURI.toString()) .getOrElse { val alternatives = - server.buildTargets.allWorkspaceJars + server.buildTargets.allSourceJars .map(_.filename) .mkString(" ") throw new NoSuchElementException( @@ -1834,8 +1834,11 @@ final case class TestingServer( val tree = parents .zip(reveal.uriChain :+ "root") .foldLeft(PrettyPrintTree.empty) { case (child, (parent, uri)) => + val realUri = + if (uri.contains("-sources.jar")) uri + else uri.replace(".jar", "-sources.jar") PrettyPrintTree( - label(uri.toLowerCase), + label.getOrElse(realUri.toLowerCase, realUri), parent.nodes .map(n => PrettyPrintTree(label(n.nodeUri.toLowerCase))) .filterNot(t => isIgnored(t.value)) diff --git a/tests/unit/src/test/scala/tests/JavaToplevelSuite.scala b/tests/unit/src/test/scala/tests/JavaToplevelSuite.scala index aae8277da5b..71fc3c49e31 100644 --- a/tests/unit/src/test/scala/tests/JavaToplevelSuite.scala +++ b/tests/unit/src/test/scala/tests/JavaToplevelSuite.scala @@ -82,7 +82,7 @@ class JavaToplevelSuite extends BaseSuite { test(name) { val input = Input.VirtualFile("Test.java", code) val obtained = - Mtags.toplevels(input, dialects.Scala213) + Mtags.topLevelSymbols(input, dialects.Scala213) assertNoDiff( obtained.sorted.mkString("\n"), diff --git a/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala b/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala index 312fa346954..e4d545c41fb 100644 --- a/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala +++ b/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala @@ -615,7 +615,7 @@ class ScalaToplevelSuite extends BaseSuite { .occurrences .map(_.symbol) .toList - case Toplevel => Mtags.toplevels(input, dialect) + case Toplevel => Mtags.topLevelSymbols(input, dialect) } assertNoDiff( obtained.sorted.mkString("\n"), diff --git a/tests/unit/src/test/scala/tests/ToplevelLibrarySuite.scala b/tests/unit/src/test/scala/tests/ToplevelLibrarySuite.scala index f5c0786c22f..ccff918dfda 100644 --- a/tests/unit/src/test/scala/tests/ToplevelLibrarySuite.scala +++ b/tests/unit/src/test/scala/tests/ToplevelLibrarySuite.scala @@ -30,7 +30,7 @@ class ToplevelLibrarySuite extends BaseSuite { forAllScalaFilesInJar(entry) { file => val input = file.toInput val scalaMtags = Mtags.toplevels(Mtags.index(input, dialects.Scala213)) - val scalaToplevelMtags = Mtags.toplevels(input) + val scalaToplevelMtags = Mtags.topLevelSymbols(input) assertTopLevels(scalaToplevelMtags, scalaMtags, input) @@ -38,7 +38,7 @@ class ToplevelLibrarySuite extends BaseSuite { // to scala2 parser if (!scala3ExclusionList(file.toString)) { val scala3Toplevels = - Mtags.toplevels(input, dialect = dialects.Scala3) + Mtags.topLevelSymbols(input, dialect = dialects.Scala3) assertTopLevels(scala3Toplevels, scalaMtags, input) } } @@ -51,7 +51,7 @@ class ToplevelLibrarySuite extends BaseSuite { if (!scala3ExclusionList.contains(file.toString)) { val input = file.toInput val scalaMtags = Mtags.toplevels(Mtags.index(input, dialects.Scala3)) - val scalaToplevelMtags = Mtags.toplevels(input, dialects.Scala3) + val scalaToplevelMtags = Mtags.topLevelSymbols(input, dialects.Scala3) assertTopLevels(scalaToplevelMtags, scalaMtags, input) } } @@ -63,7 +63,7 @@ class ToplevelLibrarySuite extends BaseSuite { forAllJavaFilesInJar(entry) { file => val input = file.toInput val javaMtags = Mtags.toplevels(Mtags.index(input, dialects.Scala3)) - val javaToplevelMtags = Mtags.toplevels(input, dialects.Scala3) + val javaToplevelMtags = Mtags.topLevelSymbols(input, dialects.Scala3) assertTopLevels(javaToplevelMtags, javaMtags, input) } } diff --git a/tests/unit/src/test/scala/tests/ToplevelSuite.scala b/tests/unit/src/test/scala/tests/ToplevelSuite.scala index ef9c7fe8568..d68b697b00c 100644 --- a/tests/unit/src/test/scala/tests/ToplevelSuite.scala +++ b/tests/unit/src/test/scala/tests/ToplevelSuite.scala @@ -30,7 +30,7 @@ abstract class ToplevelSuite( FileIO.slurp(ls.root.resolve(relpath), StandardCharsets.UTF_8) val input = Input.VirtualFile(relpath.toURI(false).toString, text) val reluri = relpath.toURI(isDirectory = false).toString - Mtags.toplevels(input, dialect).foreach { toplevel => + Mtags.topLevelSymbols(input, dialect).foreach { toplevel => // do not check symtab for Scala 3 since it's not possible currently if (symtab.info(toplevel).isEmpty && dialect != Scala3) { missingSymbols += toplevel diff --git a/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala b/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala index 806d0c94b50..00b6ce1b5bb 100644 --- a/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala +++ b/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala @@ -1,9 +1,14 @@ package tests +import java.nio.file.Paths + import scala.collection.SortedSet +import scala.util.Properties import scala.meta.internal.metals.InitializationOptions +import scala.meta.internal.metals.JdkVersion import scala.meta.internal.tvp.TreeViewProvider +import scala.meta.io.AbsolutePath /** * @note This suite will fail on openjdk8 < 262) @@ -11,41 +16,47 @@ import scala.meta.internal.tvp.TreeViewProvider */ class TreeViewLspSuite extends BaseLspSuite("tree-view") { + private val javaVersion = + JdkVersion + .fromReleaseFileString(AbsolutePath(Paths.get(Properties.javaHome))) + .getOrElse("") + private val jdkSourcesName = s"jdk-$javaVersion-sources" override protected def initializationOptions: Option[InitializationOptions] = Some(TestingServer.TestDefault) /** * The libraries we expect to find for tests in this file. - * - * @note this value changes depending on the JVM version in use as some JAR - * files have moved to become modules on JVM > 8. */ - val expectedLibraries: SortedSet[String] = { - lazy val jdk8Libraries = SortedSet( - "charsets", "jce", "jsse", "resources", "rt", - ) - - val otherLibraries = SortedSet( - "cats-core_2.13", "cats-kernel_2.13", "checker-qual", "circe-core_2.13", - "circe-numbers_2.13", "error_prone_annotations", "failureaccess", "gson", - "guava", "j2objc-annotations", "jsr305", "listenablefuture", - "org.eclipse.lsp4j", "org.eclipse.lsp4j.generator", - "org.eclipse.lsp4j.jsonrpc", "org.eclipse.xtend.lib", - "org.eclipse.xtend.lib.macro", "org.eclipse.xtext.xbase.lib", - "scala-library", "scala-reflect", "semanticdb-javac", - "simulacrum-scalafix-annotations_2.13", "sourcecode_2.13", - ) - - if (scala.util.Properties.isJavaAtLeast(9.toString)) { - otherLibraries - } else { - otherLibraries ++ jdk8Libraries + "jfr" - } - } + val expectedLibraries: SortedSet[String] = SortedSet( + "cats-core_2.13", + "cats-kernel_2.13", + "checker-qual", + "circe-core_2.13", + "circe-numbers_2.13", + "error_prone_annotations", + "failureaccess", + "gson", + "guava", + "j2objc-annotations", + jdkSourcesName, + "jsr305", + "org.eclipse.lsp4j", + "org.eclipse.lsp4j.generator", + "org.eclipse.lsp4j.jsonrpc", + "org.eclipse.xtend.lib", + "org.eclipse.xtend.lib.macro", + "org.eclipse.xtext.xbase.lib", + "scala-library", + "scala-reflect", + "simulacrum-scalafix-annotations_2.13", + "sourcecode_2.13", + ) lazy val expectedLibrariesString: String = - this.expectedLibraries.toVector - .map((s: String) => s"${s}.jar -") + (this.expectedLibraries.toVector + .map { (s: String) => + if (s != jdkSourcesName) s"${s}.jar -" else s"$jdkSourcesName -" + }) .mkString("\n") lazy val expectedLibrariesCount: Int = @@ -96,12 +107,6 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { |""".stripMargin, ) folder = server.server.path - _ = server.assertTreeViewChildren( - s"projects-$folder:${server.buildTarget("a")}", - "", - ) - _ <- server.didOpen("a/src/main/scala/a/First.scala") - _ <- server.didOpen("b/src/main/scala/b/Third.scala") _ = server.assertTreeViewChildren( s"projects-$folder:${server.buildTarget("a")}", """|_empty_/ - @@ -175,14 +180,14 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { |""".stripMargin, ) server.assertTreeViewChildren( - s"libraries-$folder:${server.jar("lsp4j")}!/org/eclipse/lsp4j/FileChangeType#", - """|Created enum + s"libraries-$folder:${server.jar("lsp4j-")}!/org/eclipse/lsp4j/FileChangeType#", + """|getValue() method + |forValue() method + |() method + |Created enum |Changed enum |Deleted enum - |values() method - |valueOf() method - |getValue() method - |forValue() method + |value field |""".stripMargin, ) server.assertTreeViewChildren( @@ -190,6 +195,19 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { """|io/ + |""".stripMargin, ) + server.assertTreeViewChildren( + s"libraries-$folder:${server.jar("cats-core")}!/_root_/", + """|cats/ + + |""".stripMargin, + ) + server.assertTreeViewChildren( + s"libraries-$folder:${server.jar("cats-core")}!/cats/compat/", + """|FoldableCompat object - + |Seq object - + |SortedSet object - + |Vector object - + |""".stripMargin, + ) server.assertTreeViewChildren( s"libraries-$folder:${server.jar("cats-core")}!/cats/instances/symbol/", """|package object @@ -220,10 +238,11 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { ), s"""|root | Projects (0) - | Libraries (${expectedLibrariesCount}) - | Libraries (${expectedLibrariesCount}) - | sourcecode_2.13-0.1.7.jar - | sourcecode_2.13-0.1.7.jar + | Libraries (22) + | Libraries (22) + | $jdkSourcesName + | sourcecode_2.13-0.1.7-sources.jar + | sourcecode_2.13-0.1.7-sources.jar | sourcecode/ | sourcecode/ | Args class @@ -268,17 +287,16 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { "registerCapability", isIgnored = { label => label.endsWith(".jar") && - !label.contains("lsp4j") + !label.contains("lsp4j-0") }, ), s"""|root | Projects (0) | Libraries (${expectedLibrariesCount}) | Libraries (${expectedLibrariesCount}) - | org.eclipse.lsp4j-0.5.0.jar - | org.eclipse.lsp4j.generator-0.5.0.jar - | org.eclipse.lsp4j.jsonrpc-0.5.0.jar - | org.eclipse.lsp4j-0.5.0.jar + | $jdkSourcesName + | org.eclipse.lsp4j-0.5.0-sources.jar + | org.eclipse.lsp4j-0.5.0-sources.jar | org/ | org/ | eclipse/ @@ -378,7 +396,7 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { | HoverCapabilities class | ImplementationCapabilities class | InitializeError class - | InitializeErrorCode class + | InitializeErrorCode interface | InitializeParams class | InitializeResult class | InitializedParams class @@ -453,13 +471,13 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { | WorkspaceServerCapabilities class | WorkspaceSymbolParams class | services/ - | LanguageClient class - | LanguageClientAware class - | LanguageClientExtensions class - | LanguageServer class - | TextDocumentService class - | WorkspaceService class - | LanguageClient class + | LanguageClient interface + | LanguageClientAware interface + | LanguageClientExtensions interface + | LanguageServer interface + | TextDocumentService interface + | WorkspaceService interface + | LanguageClient interface | applyEdit() method | registerCapability() method | unregisterCapability() method