diff --git a/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala b/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala index 231960ec5116..035c1062a3e3 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala @@ -12,6 +12,7 @@ import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Names.* import dotty.tools.dotc.core.Symbols.* +import dotty.tools.pc.utils.InteractiveEnrichments.companion class CompilerSearchVisitor( visitSymbol: Symbol => Boolean @@ -91,11 +92,12 @@ class CompilerSearchVisitor( range: org.eclipse.lsp4j.Range ): Int = val gsym = SemanticdbSymbols.inverseSemanticdbSymbol(symbol).headOption - gsym - .filter(isAccessible) - .map(visitSymbol) - .map(_ => 1) - .getOrElse(0) + val matching = for + sym0 <- gsym.toList + sym <- if sym0.companion.is(Flags.Synthetic) then List(sym0, sym0.companion) else List(sym0) + if isAccessible(sym) + yield visitSymbol(sym) + matching.size def shouldVisitPackage(pkg: String): Boolean = isAccessible(requiredPackage(normalizePackage(pkg))) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala index c8cfbd178f32..e5c81e3c044e 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala @@ -937,3 +937,13 @@ class CompletionWorkspaceSuite extends BaseCompletionSuite: |""".stripMargin, "" ) + + @Test def `metals-i6593` = + check( + """|package a: + | class UniqueObject + |package b: + | val i = Uniq@@ + |""".stripMargin, + "UniqueObject(): UniqueObject - a" + ) diff --git a/presentation-compiler/test/dotty/tools/pc/utils/TestingWorkspaceSearch.scala b/presentation-compiler/test/dotty/tools/pc/utils/TestingWorkspaceSearch.scala index 0b49bdf8bca8..27b9a49f9555 100644 --- a/presentation-compiler/test/dotty/tools/pc/utils/TestingWorkspaceSearch.scala +++ b/presentation-compiler/test/dotty/tools/pc/utils/TestingWorkspaceSearch.scala @@ -1,25 +1,63 @@ package dotty.tools.pc.utils +import dotty.tools.dotc.ast.untpd.* +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.interactive.InteractiveDriver +import dotty.tools.pc.CompilerSearchVisitor +import dotty.tools.pc.utils.InteractiveEnrichments.decoded + import java.io.File import java.nio.file.Paths - import scala.collection.mutable -import scala.meta.internal.metals.{ - CompilerVirtualFileParams, - Fuzzy, - WorkspaceSymbolQuery -} -import scala.meta.pc.SymbolSearchVisitor import scala.language.unsafeNulls +import scala.meta.internal.metals.CompilerVirtualFileParams +import scala.meta.internal.metals.Fuzzy +import scala.meta.internal.metals.WorkspaceSymbolQuery +import scala.meta.pc.SymbolSearchVisitor -import dotty.tools.dotc.core.Contexts.Context -import dotty.tools.dotc.core.Symbols.* -import dotty.tools.dotc.interactive.InteractiveDriver -import dotty.tools.dotc.semanticdb.SemanticSymbolBuilder -import dotty.tools.pc.CompilerSearchVisitor +import TestingWorkspaceSearch.* object TestingWorkspaceSearch: def empty: TestingWorkspaceSearch = new TestingWorkspaceSearch(Nil) + class Disambiguator: + val nameMap = mutable.Map[String, Int]() + def methodPart(name: String) = + val i = nameMap.getOrElse(name, 0) + nameMap.put(name, i + 1) + if i == 0 then "()." + else s"(+$i)." + + case class ParentSymbol(symbol: SearchSymbol, fileName: String): + private val dis: Disambiguator = new Disambiguator + private def isPackage = symbol.lastOption.exists(_.suffix == "/") + private def isMethod = symbol.lastOption.exists(_.suffix.endsWith(").")) + private def isInit = symbol.lastOption.exists(_.name == "") + private def filePackage = SymbolPart(fileName, "$package.") + private def member(part: SymbolPart)= + if isPackage then Some(symbol :+ filePackage :+ part) + else if isMethod then + if isInit then Some(symbol.dropRight(1) :+ part) + else None + else Some(symbol :+ part) + def makeMethod(newPart: String) = member(SymbolPart(newPart, dis.methodPart(newPart))) + def makeVal(newPart: String) = + member(SymbolPart(newPart, ".")) + def makeTypeAlias(newPart: String) = member(SymbolPart(newPart, "#")) + def makeType(newPart: String) = symbol :+ SymbolPart(newPart, "#") + def makeTerm(newPart: String) = symbol :+ SymbolPart(newPart, ".") + def makePackage(parts: List[String], isPackageObject: Boolean = false) = + val suffix = if isPackageObject then "/package." else "/" + parts match + case "" :: Nil => List(SymbolPart("_empty_", suffix)) + case list if symbol.map(_.name) == List("_empty_") => list.map(SymbolPart(_, suffix)) + case list => symbol ++ list.map(SymbolPart(_, suffix)) + + object ParentSymbol: + def empty(fileName: String) = ParentSymbol(Nil, fileName) + + case class SymbolPart(name: String, suffix: String) + type SearchSymbol = List[SymbolPart] class TestingWorkspaceSearch(classpath: Seq[String]): val inputs: mutable.Map[String, String] = mutable.Map.empty[String, String] @@ -30,8 +68,41 @@ class TestingWorkspaceSearch(classpath: Seq[String]): defaultFlags ++ List("-classpath", classpath.mkString(File.pathSeparator)) + private class SymbolCollector extends UntypedTreeAccumulator[List[Tree]]: + override def apply(x: List[Tree], tree: Tree)(using Context): List[Tree] = tree :: x + + private def newSymbol(tree: Tree, parent: ParentSymbol)(using Context): Option[SearchSymbol] = + tree match + case PackageDef(name, _) => + Some(parent.makePackage(namesFromSelect(name).reverse)) + case m @ ModuleDef(name, _) if m.mods.is(Flags.Package) => + Some(parent.makePackage(List(name.decoded), isPackageObject = true)) + case ModuleDef(name, _) => + Some(parent.makeTerm(name.decoded)) + case ValDef(name, _, _) => + parent.makeVal(name.decoded) + case t @ TypeDef(name, _: Template) if !t.mods.is(Flags.Implicit) => + Some(parent.makeType(name.decoded)) + case TypeDef(name, _) => + parent.makeTypeAlias(name.decoded) + case DefDef(name, _, _, _) => + parent.makeMethod(name.decoded) + case _ => None + + def traverse(acc: List[SearchSymbol], tree: Tree, parent: ParentSymbol)(using Context): List[SearchSymbol] = + val symbol = newSymbol(tree, parent) + val res = symbol.filter(_.lastOption.exists(_.suffix != "/")).map(_ :: acc).getOrElse(acc) + val children = foldOver(Nil, tree).reverse + val newParent = symbol.map(ParentSymbol(_, parent.fileName)).getOrElse(parent) + children.foldLeft(res)((a, c) => traverse(a, c, newParent)) + val driver = new InteractiveDriver(settings) + private def namesFromSelect(select: Tree)(using Context): List[String] = + select match + case Select(qual, name) => name.decoded :: namesFromSelect(qual) + case Ident(name) => List(name.decoded) + def search( query: WorkspaceSymbolQuery, visitor: SymbolSearchVisitor, @@ -41,21 +112,17 @@ class TestingWorkspaceSearch(classpath: Seq[String]): visitor match case visitor: CompilerSearchVisitor => - inputs.map { (path, text) => - - val nioPath = Paths.get(path) - val uri = nioPath.toUri() - val symbols = DefSymbolCollector(driver, CompilerVirtualFileParams(uri, text)).namedDefSymbols - - // We have to map symbol from this Context, to one in PresentationCompiler - // To do it we are searching it with semanticdb symbol - val semanticSymbolBuilder = SemanticSymbolBuilder() - symbols - .filter((symbol, _) => filter(symbol)) - .filter((_, name) => Fuzzy.matches(query.query, name)) - .map(symbol => semanticSymbolBuilder.symbolName(symbol._1)) - .map( - visitor.visitWorkspaceSymbol(Paths.get(""), _, null, null) - ) - } + inputs.map: (path, text) => + val nio = Paths.get(path) + val uri = nio.toUri() + driver.run(uri, text) + val run = driver.currentCtx.run + val unit = run.units.head + val symbols = SymbolCollector().traverse(Nil, unit.untpdTree, ParentSymbol.empty(nio.getFileName().toString().stripSuffix(".scala"))) + symbols.foreach: sym => + val name = sym.last.name + if Fuzzy.matches(query.query, name) + then + val symbolsString = sym.map{ case SymbolPart(name, suffix) => name ++ suffix}.mkString + visitor.visitWorkspaceSymbol(Paths.get(""), symbolsString, null, null) case _ =>