From fba6b351a6e7130f3fef27f2f7790d6e5bc7df70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Per=C5=82akowski?= <17816164+Perl99@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:12:22 +0200 Subject: [PATCH] Improve symbol order in completions provided by the presentation compiler (#23888) Extension methods that are not in the same file are placed after all Product methods and even after extension methods like "ensuring". This PR penalizes the following methods, so that they are no longer at the top of the suggestions: - scala.Product.* - scala.Equals.* - scala.Predef.ArrowAssoc.* - scala.Predef.Ensuring.* - scala.Predef.StringFormat.* - scala.Predef.nn - scala.Predef.runtimeChecked Resolves https://github.com/scalameta/metals/issues/7642 [Cherry-picked 7d27633c72325ec8e436fba3674c88438d86e3b9] --- .../tools/pc/completions/Completions.scala | 23 ++++++++++- .../completion/CompletionExtensionSuite.scala | 41 +++++++++++++++++++ .../pc/tests/completion/CompletionSuite.scala | 18 ++++---- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 80e551dacd31..b594c6fbdb2f 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -15,6 +15,7 @@ import dotty.tools.dotc.ast.untpd import dotty.tools.dotc.core.Comments.Comment import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Decorators.toTermName import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Flags.* @@ -765,6 +766,13 @@ class Completions( ).flatMap(_.alternatives.map(_.symbol)).toSet ) + private lazy val EqualsClass: ClassSymbol = requiredClass("scala.Equals") + private lazy val ArrowAssocClass: ClassSymbol = requiredClass("scala.Predef.ArrowAssoc") + private lazy val EnsuringClass: ClassSymbol = requiredClass("scala.Predef.Ensuring") + private lazy val StringFormatClass: ClassSymbol = requiredClass("scala.Predef.StringFormat") + private lazy val nnMethod: Symbol = defn.ScalaPredefModule.info.member("nn".toTermName).symbol + private lazy val runtimeCheckedMethod: Symbol = defn.ScalaPredefModule.info.member("runtimeChecked".toTermName).symbol + private def isNotLocalForwardReference(sym: Symbol)(using Context): Boolean = !sym.isLocalToBlock || !sym.srcPos.isAfter(completionPos.originalCursorPosition) || @@ -783,6 +791,17 @@ class Completions( (sym.isField && !isJavaClass && !isModuleOrClass) || sym.getter != NoSymbol catch case _ => false + def isInheritedFromScalaLibrary(sym: Symbol) = + sym.owner == defn.AnyClass || + sym.owner == defn.ObjectClass || + sym.owner == defn.ProductClass || + sym.owner == EqualsClass || + sym.owner == ArrowAssocClass || + sym.owner == EnsuringClass || + sym.owner == StringFormatClass || + sym == nnMethod || + sym == runtimeCheckedMethod + def symbolRelevance(sym: Symbol): Int = var relevance = 0 // symbols defined in this file are more relevant @@ -800,7 +819,7 @@ class Completions( case _ => // symbols whose owner is a base class are less relevant - if sym.owner == defn.AnyClass || sym.owner == defn.ObjectClass + if isInheritedFromScalaLibrary(sym) then relevance |= IsInheritedBaseMethod // symbols not provided via an implicit are more relevant if sym.is(Implicit) || @@ -812,7 +831,7 @@ class Completions( // accessors of case class members are more relevant if !sym.is(CaseAccessor) then relevance |= IsNotCaseAccessor // public symbols are more relevant - if !sym.isPublic then relevance |= IsNotCaseAccessor + if !sym.isPublic then relevance |= IsNotPublic // synthetic symbols are less relevant (e.g. `copy` on case classes) if sym.is(Synthetic) && !sym.isAllOf(EnumCase) then relevance |= IsSynthetic diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala index b41084af4a8e..16535381165c 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala @@ -437,3 +437,44 @@ class CompletionExtensionSuite extends BaseCompletionSuite: |""".stripMargin, assertSingleItem = false ) + + @Test def `extension-for-case-class` = + check( + """|case class Bar(): + | def baz(): Unit = ??? + | + |object Bar: + | extension (f: Bar) + | def qux: Unit = ??? + | + |object Main: + | val _ = Bar().@@ + |""".stripMargin, + """|baz(): Unit + |copy(): Bar + |qux: Unit + |asInstanceOf[X0]: X0 + |canEqual(that: Any): Boolean + |equals(x$0: Any): Boolean + |getClass[X0 >: Bar](): Class[? <: X0] + |hashCode(): Int + |isInstanceOf[X0]: Boolean + |productArity: Int + |productElement(n: Int): Any + |productElementName(n: Int): String + |productElementNames: Iterator[String] + |productIterator: Iterator[Any] + |productPrefix: String + |synchronized[X0](x$0: X0): X0 + |toString(): String + |->[B](y: B): (Bar, B) + |ensuring(cond: Boolean): Bar + |ensuring(cond: Bar => Boolean): Bar + |ensuring(cond: Boolean, msg: => Any): Bar + |ensuring(cond: Bar => Boolean, msg: => Any): Bar + |nn: `?1`.type + |runtimeChecked: `?2`.type + |formatted(fmtstr: String): String + |→[B](y: B): (Bar, B) + | """.stripMargin + ) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 4dad18a78181..c02d217e45d9 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -109,18 +109,9 @@ class CompletionSuite extends BaseCompletionSuite: |tabulate[A](n: Int)(f: Int => A): List[A] |unapplySeq[A](x: List[A] @uncheckedVariance): UnapplySeqWrapper[A] |unfold[A, S](init: S)(f: S => Option[(A, S)]): List[A] - |->[B](y: B): (List.type, B) - |ensuring(cond: Boolean): List.type - |ensuring(cond: List.type => Boolean): List.type - |ensuring(cond: Boolean, msg: => Any): List.type - |ensuring(cond: List.type => Boolean, msg: => Any): List.type |fromSpecific(from: Any)(it: IterableOnce[Nothing]): List[Nothing] |fromSpecific(it: IterableOnce[Nothing]): List[Nothing] - |nn: List.type - |runtimeChecked scala.collection.immutable |toFactory(from: Any): Factory[Nothing, List[Nothing]] - |formatted(fmtstr: String): String - |→[B](y: B): (List.type, B) |iterableFactory[A]: Factory[A, List[A]] |asInstanceOf[X0]: X0 |equals(x$0: Any): Boolean @@ -129,6 +120,15 @@ class CompletionSuite extends BaseCompletionSuite: |isInstanceOf[X0]: Boolean |synchronized[X0](x$0: X0): X0 |toString(): String + |->[B](y: B): (List.type, B) + |ensuring(cond: Boolean): List.type + |ensuring(cond: List.type => Boolean): List.type + |ensuring(cond: Boolean, msg: => Any): List.type + |ensuring(cond: List.type => Boolean, msg: => Any): List.type + |nn: List.type + |runtimeChecked scala.collection.immutable + |formatted(fmtstr: String): String + |→[B](y: B): (List.type, B) |""".stripMargin )