From 3e782384e83d8165ac6ba7330d795143ea41d510 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Wed, 17 Jul 2024 14:59:55 +0200 Subject: [PATCH] feat: Implement completions for named tuple fields (#21202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes scala#20478 --------- Co-authored-by: Jędrzej Rochala <48657087+rochala@users.noreply.github.com> --- .../tools/dotc/interactive/Completion.scala | 43 +++++++++++++++-- .../tools/languageserver/CompletionTest.scala | 10 ++++ .../pc/tests/completion/CompletionSuite.scala | 47 +++++++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 7882d635f84a..1395d9b80b53 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -32,6 +32,8 @@ import dotty.tools.dotc.core.Names import dotty.tools.dotc.core.Types import dotty.tools.dotc.core.Symbols import dotty.tools.dotc.core.Constants +import dotty.tools.dotc.core.TypeOps +import dotty.tools.dotc.core.StdNames /** * One of the results of a completion query. @@ -200,7 +202,8 @@ object Completion: private def computeCompletions( pos: SourcePosition, - mode: Mode, rawPrefix: String, + mode: Mode, + rawPrefix: String, adjustedPath: List[tpd.Tree], untpdPath: List[untpd.Tree], matches: Option[Name => Boolean] @@ -442,9 +445,17 @@ object Completion: def selectionCompletions(qual: tpd.Tree)(using Context): CompletionMap = val adjustedQual = widenQualifier(qual) - implicitConversionMemberCompletions(adjustedQual) ++ - extensionCompletions(adjustedQual) ++ - directMemberCompletions(adjustedQual) + val implicitConversionMembers = implicitConversionMemberCompletions(adjustedQual) + val extensionMembers = extensionCompletions(adjustedQual) + val directMembers = directMemberCompletions(adjustedQual) + val namedTupleMembers = namedTupleCompletions(adjustedQual) + + List( + implicitConversionMembers, + extensionMembers, + directMembers, + namedTupleMembers + ).reduce(_ ++ _) /** Completions for members of `qual`'s type. * These include inherited definitions but not members added by extensions or implicit conversions @@ -516,6 +527,30 @@ object Completion: .toSeq .groupByName + /** Completions for named tuples */ + private def namedTupleCompletions(qual: tpd.Tree)(using Context): CompletionMap = + def namedTupleCompletionsFromType(tpe: Type): CompletionMap = + val freshCtx = ctx.fresh.setExploreTyperState() + inContext(freshCtx): + tpe.namedTupleElementTypes + .map { (name, tpe) => + val symbol = newSymbol(owner = NoSymbol, name, EmptyFlags, tpe) + val denot = SymDenotation(symbol, NoSymbol, name, EmptyFlags, tpe) + name -> denot + } + .toSeq + .filter((name, denot) => include(denot, name)) + .groupByName + + val qualTpe = qual.typeOpt + if qualTpe.isNamedTupleType then + namedTupleCompletionsFromType(qualTpe) + else if qualTpe.derivesFrom(defn.SelectableClass) then + val pre = if !TypeOps.isLegalPrefix(qualTpe) then Types.SkolemType(qualTpe) else qualTpe + val fieldsType = pre.select(StdNames.tpnme.Fields).dealias.simplified + namedTupleCompletionsFromType(fieldsType) + else Map.empty + /** Completions from extension methods */ private def extensionCompletions(qual: tpd.Tree)(using Context): CompletionMap = def asDefLikeType(tpe: Type): Type = tpe match diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index d64bb44c1a5d..887c7a983729 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1723,4 +1723,14 @@ class CompletionTest { .completion(m5, Set()) .completion(m6, Set()) + @Test def namedTupleCompletion: Unit = + code"""|import scala.language.experimental.namedTuples + | + |val person: (name: String, city: String) = + | (name = "Jamie", city = "Lausanne") + | + |val n = person.na$m1 + |""" + .completion(m1, Set(("name", Field, "String"))) + } 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 f660baa6af6d..f281f42d9db3 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -1983,3 +1983,50 @@ class CompletionSuite extends BaseCompletionSuite: |val foo: SomeClass |""".stripMargin, ) + + @Test def `namedTuple completions` = + check( + """|import scala.language.experimental.namedTuples + |import scala.NamedTuple.* + | + |val person = (name = "Jamie", city = "Lausanne") + | + |val n = person.na@@""".stripMargin, + "name: String", + filter = _.contains("name") + ) + + @Test def `Selectable with namedTuple Fields member` = + check( + """|import scala.language.experimental.namedTuples + |import scala.NamedTuple.* + | + |class NamedTupleSelectable extends Selectable { + | type Fields <: AnyNamedTuple + | def selectDynamic(name: String): Any = ??? + |} + | + |val person2 = new NamedTupleSelectable { + | type Fields = (name: String, city: String) + |} + | + |val n = person2.na@@""".stripMargin, + """|name: String + |selectDynamic(name: String): Any + """.stripMargin, + filter = _.contains("name") + ) + + @Test def `Selectable without namedTuple Fields mamber` = + check( + """|class NonNamedTupleSelectable extends Selectable { + | def selectDynamic(name: String): Any = ??? + |} + | + |val person2 = new NonNamedTupleSelectable {} + | + |val n = person2.na@@""".stripMargin, + """|selectDynamic(name: String): Any + """.stripMargin, + filter = _.contains("name") + )