diff --git a/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala index fd363dbd37a2..6aa0c3d7dc4d 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala @@ -104,10 +104,10 @@ object HoverProvider: ) match case Nil => fallbackToDynamics(path, printer, contentType) - case (symbol, tpe) :: _ + case (symbol, tpe, _) :: _ if symbol.name == nme.selectDynamic || symbol.name == nme.applyDynamic => fallbackToDynamics(path, printer, contentType) - case symbolTpes @ ((symbol, tpe) :: _) => + case symbolTpes @ ((symbol, tpe, None) :: _) => val exprTpw = tpe.widenTermRefExpr.deepDealias val hoverString = tpw match @@ -153,6 +153,21 @@ object HoverProvider: case _ => ju.Optional.empty().nn end match + case (_, tpe, Some(namedTupleArg)) :: _ => + val exprTpw = tpe.widenTermRefExpr.deepDealias + printer.expressionType(exprTpw) match + case Some(tpe) => + ju.Optional.of( + new ScalaHover( + expressionType = Some(tpe), + symbolSignature = Some(s"$namedTupleArg: $tpe"), + docstring = None, + forceExpressionType = false, + contextInfo = printer.getUsedRenamesInfo, + contentType = contentType + ) + ).nn + case _ => ju.Optional.empty().nn end match end if end hover @@ -165,23 +180,31 @@ object HoverProvider: printer: ShortenedTypePrinter, contentType: ContentType )(using Context): ju.Optional[HoverSignature] = path match - case SelectDynamicExtractor(sel, n, name) => + case SelectDynamicExtractor(sel, n, name, rest) => def findRefinement(tp: Type): Option[HoverSignature] = tp match - case RefinedType(_, refName, tpe) if name == refName.toString() => + case RefinedType(_, refName, tpe) if (name == refName.toString() || refName.toString() == nme.Fields.toString()) => + val resultType = + rest match + case Select(_, asInstanceOf) :: TypeApply(_, List(tpe)) :: _ if asInstanceOf == nme.asInstanceOfPM => + tpe.tpe.widenTermRefExpr.deepDealias + case _ if n == nme.selectDynamic => tpe.resultType + case _ => tpe + val tpeString = - if n == nme.selectDynamic then s": ${printer.tpe(tpe.resultType)}" - else printer.tpe(tpe) + if n == nme.selectDynamic then s": ${printer.tpe(resultType)}" + else printer.tpe(resultType) val valOrDef = - if n == nme.selectDynamic && !tpe.isInstanceOf[ExprType] - then "val" - else "def" + if refName.toString() == nme.Fields.toString() then "" + else if n == nme.selectDynamic && !tpe.isInstanceOf[ExprType] + then "val " + else "def " Some( new ScalaHover( expressionType = Some(tpeString), - symbolSignature = Some(s"$valOrDef $name$tpeString"), + symbolSignature = Some(s"$valOrDef$name$tpeString"), contextInfo = printer.getUsedRenamesInfo, contentType = contentType ) @@ -208,16 +231,16 @@ object SelectDynamicExtractor: case Select(_, _) :: Apply( Select(Apply(reflSel, List(sel)), n), List(Literal(Constant(name: String))) - ) :: _ + ) :: rest if (n == nme.selectDynamic || n == nme.applyDynamic) && nme.reflectiveSelectable == reflSel.symbol.name => - Some(sel, n, name) + Some(sel, n, name, rest) // tests `selectable`, `selectable2` and `selectable-full` in HoverScala3TypeSuite case Select(_, _) :: Apply( Select(sel, n), List(Literal(Constant(name: String))) - ) :: _ if n == nme.selectDynamic || n == nme.applyDynamic => - Some(sel, n, name) + ) :: rest if n == nme.selectDynamic || n == nme.applyDynamic => + Some(sel, n, name, rest) case _ => None end match end unapply diff --git a/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala b/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala index 9a541ef69942..ef583ea2a225 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala @@ -5,12 +5,16 @@ import scala.annotation.tailrec import dotc.* import ast.*, tpd.* +import dotty.tools.dotc.core.Constants.* import core.*, Contexts.*, Flags.*, Names.*, Symbols.*, Types.* +import dotty.tools.dotc.core.StdNames.* import interactive.* import util.* import util.SourcePosition +import dotty.tools.pc.utils.InteractiveEnrichments.* object MetalsInteractive: + type NamedTupleArg = String def contextOfStat( stats: List[Tree], @@ -110,7 +114,7 @@ object MetalsInteractive: pos: SourcePosition, indexed: IndexedContext, skipCheckOnName: Boolean = false - ): List[(Symbol, Type)] = + ): List[(Symbol, Type, Option[String])] = import indexed.ctx path match // For a named arg, find the target `DefDef` and jump to the param @@ -118,59 +122,59 @@ object MetalsInteractive: val funSym = fn.symbol if funSym.is(Synthetic) && funSym.owner.is(CaseClass) then val sym = funSym.owner.info.member(name).symbol - List((sym, sym.info)) + List((sym, sym.info, None)) else val paramSymbol = for param <- funSym.paramSymss.flatten.find(_.name == name) yield param val sym = paramSymbol.getOrElse(fn.symbol) - List((sym, sym.info)) + List((sym, sym.info, None)) case (_: untpd.ImportSelector) :: (imp: Import) :: _ => importedSymbols(imp, _.span.contains(pos.span)).map(sym => - (sym, sym.info) + (sym, sym.info, None) ) case (imp: Import) :: _ => importedSymbols(imp, _.span.contains(pos.span)).map(sym => - (sym, sym.info) + (sym, sym.info, None) ) // wildcard param case head :: _ if (head.symbol.is(Param) && head.symbol.is(Synthetic)) => - List((head.symbol, head.typeOpt)) + List((head.symbol, head.typeOpt, None)) case (head @ Select(target, name)) :: _ if head.symbol.is(Synthetic) && name == StdNames.nme.apply => val sym = target.symbol if sym.is(Synthetic) && sym.is(Module) then - List((sym.companionClass, sym.companionClass.info)) - else List((target.symbol, target.typeOpt)) + List((sym.companionClass, sym.companionClass.info, None)) + else List((target.symbol, target.typeOpt, None)) // L@@ft(...) case (head @ ApplySelect(select)) :: _ if select.qualifier.sourcePos.contains(pos) && select.name == StdNames.nme.apply => - List((head.symbol, head.typeOpt)) + List((head.symbol, head.typeOpt, None)) // for Inlined we don't have a symbol, but it's needed to show proper type case (head @ Inlined(call, bindings, expansion)) :: _ => - List((call.symbol, head.typeOpt)) + List((call.symbol, head.typeOpt, None)) // for comprehension case (head @ ApplySelect(select)) :: _ if isForSynthetic(head) => // If the cursor is on the qualifier, return the symbol for it // `for { x <- List(1).head@@Option }` returns the symbol of `headOption` if select.qualifier.sourcePos.contains(pos) then - List((select.qualifier.symbol, select.qualifier.typeOpt)) + List((select.qualifier.symbol, select.qualifier.typeOpt, None)) // Otherwise, returns the symbol of for synthetics such as "withFilter" - else List((head.symbol, head.typeOpt)) + else List((head.symbol, head.typeOpt, None)) // f@@oo.bar case Select(target, _) :: _ if target.span.isSourceDerived && target.sourcePos.contains(pos) => - List((target.symbol, target.typeOpt)) + List((target.symbol, target.typeOpt, None)) /* In some cases type might be represented by TypeTree, however it's possible * that the type tree will not be marked properly as synthetic even if it doesn't @@ -185,7 +189,7 @@ object MetalsInteractive: */ case (tpt: TypeTree) :: parent :: _ if tpt.span != parent.span && !tpt.symbol.is(Synthetic) => - List((tpt.symbol, tpt.typeOpt)) + List((tpt.symbol, tpt.typeOpt, None)) /* TypeTest class https://dotty.epfl.ch/docs/reference/other-new-features/type-test.html * compiler automatically adds unapply if possible, we need to find the type symbol @@ -195,14 +199,28 @@ object MetalsInteractive: pat match case UnApply(fun, _, pats) => val tpeSym = pats.head.typeOpt.typeSymbol - List((tpeSym, tpeSym.info)) + List((tpeSym, tpeSym.info, None)) case _ => Nil + // Handle select on named tuples + case (Apply(Apply(TypeApply(fun, List(t1, t2)), List(ddef)), List(Literal(Constant(i: Int))))) :: _ + if fun.symbol.exists && fun.symbol.name == nme.apply && + fun.symbol.owner.exists && fun.symbol.owner == getModuleIfDefined("scala.NamedTuple").moduleClass => + def getIndex(t: Tree): Option[Type] = + t.tpe.dealias match + case AppliedType(_, args) => args.get(i) + case _ => None + val name = getIndex(t1) match + case Some(c: ConstantType) => c.value.stringValue + case _ => "" + val tpe = getIndex(t2).getOrElse(NoType) + List((ddef.symbol, tpe, Some(name))) + case path @ head :: tail => if head.symbol.is(Exported) then val sym = head.symbol.sourceSymbol - List((sym, sym.info)) + List((sym, sym.info, None)) else if head.symbol.is(Synthetic) then enclosingSymbolsWithExpressionType( tail, @@ -217,7 +235,7 @@ object MetalsInteractive: pos, indexed.ctx.source ) - then List((head.symbol, head.typeOpt)) + then List((head.symbol, head.typeOpt, None)) /* Type tree for List(1) has an Int type variable, which has span * but doesn't exist in code. * https://github.com/scala/scala3/issues/15937 @@ -234,7 +252,7 @@ object MetalsInteractive: indexed, skipCheckOnName ) - else recovered.map(sym => (sym, sym.info)) + else recovered.map(sym => (sym, sym.info, None)) end if case Nil => Nil end match diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala index 3b2284bef1d0..7d8e4dfad081 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala @@ -101,7 +101,7 @@ class PcDefinitionProvider( val enclosing = path.expandRangeToEnclosingApply(pos) val typeSymbols = MetalsInteractive .enclosingSymbolsWithExpressionType(enclosing, pos, indexed) - .map { case (_, tpe) => + .map { case (_, tpe, _) => tpe.typeSymbol } typeSymbols match diff --git a/presentation-compiler/test/dotty/tools/pc/tests/definition/PcDefinitionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/definition/PcDefinitionSuite.scala index fab21ffdee0a..ff4c6ec25e27 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/definition/PcDefinitionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/definition/PcDefinitionSuite.scala @@ -504,3 +504,12 @@ class PcDefinitionSuite extends BasePcDefinitionSuite: |val a = MyIntOut(1).un@@even |""".stripMargin, ) + + @Test def `named-tuples` = + check( + """|import scala.language.experimental.namedTuples + | + |val <> = (name = "Bob", age = 42, height = 1.9d) + |val foo_name = foo.na@@me + |""".stripMargin + ) 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 ec1431187e56..ba77e2b16cdc 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTermSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTermSuite.scala @@ -717,3 +717,33 @@ class HoverTermSuite extends BaseHoverSuite: |""".stripMargin, """def ???: Nothing""".stripMargin.hover ) + + @Test def `named-tuples`: Unit = + check( + """import scala.language.experimental.namedTuples + | + |val foo = (name = "Bob", age = 42, height = 1.9d) + |val foo_name = foo.na@@me + |""".stripMargin, + "name: String".hover + ) + + @Test def `named-tuples2`: Unit = + check( + """|import scala.language.experimental.namedTuples + | + |import NamedTuple.* + | + |class NamedTupleSelectable extends Selectable { + | type Fields <: AnyNamedTuple + | def selectDynamic(name: String): Any = ??? + |} + | + |val person = new NamedTupleSelectable { + | type Fields = (name: String, city: String) + |} + | + |val person_name = person.na@@me + |""".stripMargin, + "name: String".hover + )