From d981e08d744c524d7ffd04bf8114f727cdb9ea6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Fornal?= <24961583+Florian3k@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:55:45 +0200 Subject: [PATCH] Compare span points in pathTo to determine best span (#23581) Closes #22207 [Cherry-picked 98c594f39d38a859719f09f39124d56dbb1ed755] --- .../dotty/tools/dotc/ast/NavigateAST.scala | 20 ++++++++--- .../tools/pc/tests/hover/HoverTermSuite.scala | 36 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala index 90bd9b7923ae..861db55b1903 100644 --- a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala +++ b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala @@ -94,7 +94,8 @@ object NavigateAST { * When choosing better fit we compare spans. If candidate span has starting or ending point inside (exclusive) * current best fit it is selected as new best fit. This means that same spans are failing the first predicate. * - * In case when spans start and end at same offsets we prefer non synthethic one. + * In case when spans start and end at same offsets we prefer non synthethic one, + * and then one with better point (see isBetterPoint below). */ def isBetterFit(currentBest: List[Positioned], candidate: List[Positioned]): Boolean = if currentBest.isEmpty && candidate.nonEmpty then true @@ -102,9 +103,20 @@ object NavigateAST { val bestSpan = currentBest.head.span val candidateSpan = candidate.head.span - bestSpan != candidateSpan && - envelops(bestSpan, candidateSpan) || - bestSpan.contains(candidateSpan) && bestSpan.isSynthetic && !candidateSpan.isSynthetic + def isBetterPoint = + // Given two spans with same end points, + // we compare their points in relation to the point we are looking for (span.point) + // The candidate (candidateSpan.point) is better than what we have so far (bestSpan.point), when: + // 1) candidate is closer to target from the right + span.point <= candidateSpan.point && candidateSpan.point < bestSpan.point + // 2) candidate is closer to target from the left + || bestSpan.point < candidateSpan.point && candidateSpan.point <= span.point + // 3) candidate is to on the left side of target, and best so far is on the right + || candidateSpan.point <= span.point && span.point < bestSpan.point + + bestSpan != candidateSpan && envelops(bestSpan, candidateSpan) + || bestSpan.contains(candidateSpan) && bestSpan.isSynthetic && !candidateSpan.isSynthetic + || candidateSpan.start == bestSpan.start && candidateSpan.end == bestSpan.end && isBetterPoint else false def isRecoveryTree(sel: untpd.Select): Boolean = diff --git a/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTermSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTermSuite.scala index e0501d4c9496..3e3019e80ff7 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTermSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTermSuite.scala @@ -747,3 +747,39 @@ class HoverTermSuite extends BaseHoverSuite: |""".stripMargin, "def substring(x$0: Int, x$1: Int): String".hover ) + + @Test def `multiple-valdefs-1` = + check( + """|object O { + | val x@@x, yy, zz = 1 + |} + |""".stripMargin, + "val xx: Int".hover + ) + + @Test def `multiple-valdefs-2` = + check( + """|object O { + | val xx, y@@y, zz = 1 + |} + |""".stripMargin, + "val yy: Int".hover + ) + + @Test def `multiple-valdefs-3` = + check( + """|object O { + | val xx, yy, z@@z = 1 + |} + |""".stripMargin, + "val zz: Int".hover + ) + + @Test def `multiple-valdefs-4` = + check( + """|object O { + | val xx, thisIsAVeryLongNa@@me, zz = 1 + |} + |""".stripMargin, + "val thisIsAVeryLongName: Int".hover + )