From b806a7c0e9f648c44696ff07e58d07f9f9c69788 Mon Sep 17 00:00:00 2001 From: Alec Theriault Date: Tue, 11 Mar 2025 07:31:52 -0400 Subject: [PATCH 1/2] fix: generate auto-imports for more pattern completions This tweaks the case-completion logic to always take auto-imports into account. The auto-import logic was present for known direct subclasses, but not for some other cases. Now, they all get handled uniformly. The only hair in the soup is that extra work needs to go in to def myList: List[Int] myList match { case N@@ // want `case Nil =>`, not `case immutable.Nil =>` } Reason here is that the `Nil` variant is completing with `scala.collection.immutable.Nil`, while only `scala.Nil` is in scope. The `scala.*` package re-exports are narrowly worked around since they probably would occur frequently. --- .../scala/meta/internal/pc/MetalsGlobal.scala | 18 ++++++- .../pc/completions/MatchCaseCompletions.scala | 32 ++++++----- .../scala/tests/pc/CompletionCaseSuite.scala | 54 ++++++++++++++++++- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala index 8e5f5b662de..016e688fbd9 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala @@ -963,10 +963,26 @@ class MetalsGlobal( } else { other.dealiased == sym.dealiased || other.companion == sym.dealiased || - semanticdbSymbol(other.dealiased) == semanticdbSymbol(sym.dealiased) + semanticdbSymbol(other.dealiased) == semanticdbSymbol(sym.dealiased) || + isScalaPackageObjectReexport(other) } } + /** + * Check if the symbol is a `scala.*` `val` re-export of `other` + * + * @param other + * @return + */ + private def isScalaPackageObjectReexport(other: Symbol): Boolean = + sym.tpe match { + case NullaryMethodType(resultType: SingleType) => + sym.isDefinedInPackage && sym.isStable && + sym.effectiveOwner.name.toTermName == termNames.scala_ && + resultType =:= other.tpe + case _ => false + } + def snippetCursor: String = sym.paramss match { case Nil => diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/completions/MatchCaseCompletions.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/completions/MatchCaseCompletions.scala index 967d063653e..8f06d24841c 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/completions/MatchCaseCompletions.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/completions/MatchCaseCompletions.scala @@ -10,7 +10,6 @@ import scala.meta.internal.jdk.CollectionConverters._ import scala.meta.internal.metals.PcQueryContext import scala.meta.internal.mtags.MtagsEnrichments._ import scala.meta.internal.pc.CompletionFuzzy -import scala.meta.internal.pc.Identifier import scala.meta.internal.pc.MetalsGlobal import org.eclipse.{lsp4j => l} @@ -83,6 +82,7 @@ trait MatchCaseCompletions { this: MetalsGlobal => override def contribute: List[Member] = { val selectorSym = parents.selector.typeSymbol + val autoImport = autoImportPosition(pos, text) // Special handle case when selector is a tuple or `FunctionN`. if (definitions.isTupleType(parents.selector)) { @@ -110,11 +110,7 @@ trait MatchCaseCompletions { this: MetalsGlobal => ) val result = ListBuffer.empty[(Symbol, TextEditMember)] val isVisited = mutable.Set.empty[Symbol] - def visit( - sym: Symbol, - name: String, - autoImports: List[l.TextEdit] - ): Unit = { + def visit(sym: Symbol): Unit = { val fsym = sym.dealiasedSingleType def recordVisit(s: Symbol): Unit = { if (s != NoSymbol && !isVisited(s)) { @@ -127,6 +123,16 @@ trait MatchCaseCompletions { this: MetalsGlobal => if (!isVisited(sym) && !isVisited(fsym)) { recordVisit(sym) recordVisit(fsym) + + val (name, autoImports) = autoImport match { + case Some(value) => + val (shortName, edits) = + ShortenedNames.synthesize(sym, pos, context, value) + (shortName, edits) + case None => + (sym.fullNameSyntax, Nil) + } + if (fuzzyMatches(name)) result += (( sym, @@ -150,7 +156,7 @@ trait MatchCaseCompletions { this: MetalsGlobal => !(selectorSym.isSealed && (selectorSym.isAbstract || selectorSym.isTrait)) ) - visit(selectorSym, Identifier(selectorSym.name), Nil) + visit(selectorSym) } // Step 1: walk through scope members. @@ -163,24 +169,16 @@ trait MatchCaseCompletions { this: MetalsGlobal => fsym.hasModuleFlag || fsym.isInstanceOf[TypeSymbol]) && parents.isSubClass(fsym, includeReverse = false) - if (isValid) visit(sym, Identifier(m.sym.name), Nil) + if (isValid) visit(sym) } // Step 2: walk through known direct subclasses of sealed types. // In `List(foo).map { cas@@} we want to provide also `case (exhaustive)` completion // which works like exhaustive match, so we need to collect only members from this step - val autoImport = autoImportPosition(pos, text) val sealedDescs = mutable.Set.empty[Symbol] selectorSym.foreachKnownDirectSubClass { sym => sealedDescs += sym.dealiased - autoImport match { - case Some(value) => - val (shortName, edits) = - ShortenedNames.synthesize(sym, pos, context, value) - visit(sym, shortName, edits) - case None => - visit(sym, sym.fullNameSyntax, Nil) - } + visit(sym) } val members = result.result() val edits = { diff --git a/tests/cross/src/test/scala/tests/pc/CompletionCaseSuite.scala b/tests/cross/src/test/scala/tests/pc/CompletionCaseSuite.scala index f5caef1f4b1..994352a9ab2 100644 --- a/tests/cross/src/test/scala/tests/pc/CompletionCaseSuite.scala +++ b/tests/cross/src/test/scala/tests/pc/CompletionCaseSuite.scala @@ -165,8 +165,6 @@ class CompletionCaseSuite extends BaseCompletionSuite { |""".stripMargin ) - // TODO: `Left` has conflicting name in Scope, we should fix it so the result is the same as for scala 2 - // Issue: https://github.com/scalameta/metals/issues/4368 check( "sealed-conflict", """ @@ -860,4 +858,56 @@ class CompletionCaseSuite extends BaseCompletionSuite { "" ) + checkEdit( + "underscore-autoimport", + """ + |object Outer { + | class Cls + |} + |object A { + | val t: Outer.Cls = ??? + | t match { + | case@@ + | } + |}""".stripMargin, + """ + |import Outer.Cls + | + |object Outer { + | class Cls + |} + |object A { + | val t: Outer.Cls = ??? + | t match { + | case _: Cls => $0 + | } + |}""".stripMargin + ) + + checkEdit( + "case-class-autoimport", + """ + |object Outer { + | case class Cls(i: Int) + |} + |object A { + | val t: Outer.Cls = ??? + | t match { + | case@@ + | } + |}""".stripMargin, + """ + |import Outer.Cls + | + |object Outer { + | case class Cls(i: Int) + |} + |object A { + | val t: Outer.Cls = ??? + | t match { + | case Cls(i) => $0 + | } + |}""".stripMargin + ) + } From a807821e750290ec6657cf4a8e11835a1f56f12c Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Tue, 11 Mar 2025 16:44:08 +0100 Subject: [PATCH 2/2] tests: Fix 2.11 case --- .../src/test/scala/tests/pc/SignatureHelpSuite.scala | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/cross/src/test/scala/tests/pc/SignatureHelpSuite.scala b/tests/cross/src/test/scala/tests/pc/SignatureHelpSuite.scala index a50ff39d344..4b9657f2c6a 100644 --- a/tests/cross/src/test/scala/tests/pc/SignatureHelpSuite.scala +++ b/tests/cross/src/test/scala/tests/pc/SignatureHelpSuite.scala @@ -440,14 +440,7 @@ class SignatureHelpSuite extends BaseSignatureHelpSuite { """|to(end: Int): scala.collection.immutable.Range.Inclusive | ^^^^^^^^ |to(end: Int, step: Int): scala.collection.immutable.Range.Inclusive - |""".stripMargin, - "2.11" -> """|^^^^^^ - |to(end: Int): immutable.Range.Inclusive - |to(end: Int, step: Int): immutable.Range.Inclusive - |to(end: T): NumericRange.Inclusive[T] - |to(end: T): Range.Partial[T,NumericRange[T]] - |to(end: T, step: T): NumericRange.Inclusive[T] - |""".stripMargin + |""".stripMargin ) )