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/Indexer.scala b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala index cacdd666ec9..5350e44b58a 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala @@ -562,6 +562,7 @@ final case class Indexer( source, info.symbol, dialect, + info, ) } } 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 7b9246aa0d1..f5627e0c19e 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -786,9 +786,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..151795af3ca --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/tvp/IndexedSymbols.scala @@ -0,0 +1,192 @@ +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).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..49b0fc772b7 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala @@ -10,6 +10,7 @@ import scala.meta.Dialect import scala.meta.dialects import scala.meta.internal.io.{ListFiles => _} import scala.meta.internal.metals.ReportContext +import scala.meta.internal.semanticdb.SymbolInformation import scala.meta.io.AbsolutePath /** @@ -62,6 +63,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 @@ -122,9 +139,10 @@ final class OnDemandSymbolIndex( path: String, source: AbsolutePath, toplevel: String, - dialect: Dialect + dialect: Dialect, + info: SymbolInformation ): Unit = - getOrCreateBucket(dialect).addToplevelSymbol(path, source, toplevel) + getOrCreateBucket(dialect).addToplevelSymbol(path, source, toplevel, info) private def tryRun[A](path: AbsolutePath, fallback: => A, thunk: => A): A = try thunk 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 29968293341..2379a03a4de 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/ScalametaCommonEnrichments.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ScalametaCommonEnrichments.scala @@ -349,6 +349,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..48d9a72e3b2 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/SymbolIndexBucket.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/SymbolIndexBucket.scala @@ -37,7 +37,7 @@ final case class SymbolLocation( * while definitions contains only symbols generated by ScalaMtags. */ class SymbolIndexBucket( - toplevels: TrieMap[String, Set[AbsolutePath]], + toplevels: TrieMap[String, (s.SymbolInformation.Kind, Set[AbsolutePath])], definitions: TrieMap[String, Set[SymbolLocation]], sourceJars: OpenClassLoader, toIndexSource: AbsolutePath => AbsolutePath = identity, @@ -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 { @@ -95,8 +147,11 @@ class SymbolIndexBucket( else symbols patched.foreach { case (sym, path) => - val acc = toplevels.getOrElse(sym, Set.empty) - toplevels(sym) = acc + path + val (kind, acc: Set[AbsolutePath]) = toplevels.getOrElse( + sym, + (s.SymbolInformation.Kind.UNKNOWN_KIND, Set.empty) + ) + toplevels(sym) = (kind, acc + path) } } } @@ -106,8 +161,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 { @@ -117,20 +171,24 @@ class SymbolIndexBucket( } patched.foreach { symbol => - val acc = toplevels.getOrElse(symbol, Set.empty) - toplevels(symbol) = acc + source + val (kind, acc: Set[AbsolutePath]) = toplevels.getOrElse( + symbol, + (s.SymbolInformation.Kind.UNKNOWN_KIND, Set.empty) + ) + toplevels(symbol) = (kind, acc + source) } symbols } 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 @@ -151,11 +209,13 @@ class SymbolIndexBucket( def addToplevelSymbol( path: String, source: AbsolutePath, - toplevel: String + toplevel: String, + info: s.SymbolInformation ): Unit = { if (source.isAmmoniteScript || !isTrivialToplevelSymbol(path, toplevel)) { - val acc = toplevels.getOrElse(toplevel, Set.empty) - toplevels(toplevel) = acc + source + val (_, acc: Set[AbsolutePath]) = + toplevels.getOrElse(toplevel, (info.kind, Set.empty)) + toplevels(toplevel) = (info.kind, acc + source) } } @@ -186,7 +246,7 @@ class SymbolIndexBucket( val toplevel = symbol.toplevel val files = toplevels.get(toplevel.value) files match { - case Some(files) => + case Some((_, files)) => files.foreach(addMtagsSourceFile(_)) case _ => loadFromSourceJars(trivialPaths(toplevel)) @@ -200,9 +260,10 @@ class SymbolIndexBucket( for { companionClassFile <- toplevels .get(toplevelAlternative) + .map(_._2) .toSet .flatten - if (!files.exists(_.contains(companionClassFile))) + if (!files.exists(_._2.contains(companionClassFile))) } addMtagsSourceFile(companionClassFile) } } @@ -221,7 +282,9 @@ class SymbolIndexBucket( definitionSymbol = symbol, path = location.path, dialect = dialect, - range = location.range + range = location.range, + kind = None, + properties = 0 ) }.toList } @@ -235,17 +298,17 @@ class SymbolIndexBucket( */ private def removeOldEntries(symbol: Symbol): Unit = { val exists = - (toplevels.get(symbol.value).getOrElse(Set.empty) ++ definitions + (toplevels.get(symbol.value).map(_._2).getOrElse(Set.empty) ++ definitions .get(symbol.value) .map(_.map(_.path)) .getOrElse(Set.empty)).filter(_.exists) toplevels.get(symbol.value) match { case None => () - case Some(acc) => + case Some((kind, acc)) => val updated = acc.filter(exists(_)) if (updated.isEmpty) toplevels.remove(symbol.value) - else toplevels(symbol.value) = updated + else toplevels(symbol.value) = (kind, updated) } definitions.get(symbol.value) match { @@ -257,6 +320,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 +339,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 ebe1aa15b52..e05c439b894 100644 --- a/tests/unit/src/main/scala/tests/QuickBuild.scala +++ b/tests/unit/src/main/scala/tests/QuickBuild.scala @@ -120,7 +120,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 a4ae0d0ade5..defa348ebf0 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -1766,12 +1766,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( @@ -1821,8 +1821,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 2552a7b7645..100c5e7a6ed 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.folder - _ = 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_/ - @@ -170,19 +175,20 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { ) server.assertTreeViewChildren( s"libraries-$folder:${server.jar("scala-library")}!/scala/Some#", - """|value val + """|A type + |value val |get() method |""".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 +196,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 +239,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 +288,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 +397,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 +472,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