Skip to content

fix: hover and go to definition for named tuples #22202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 37 additions & 14 deletions presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -110,67 +114,67 @@ 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
case NamedArg(name, _) :: Apply(fn, _) :: _ =>
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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<foo>> = (name = "Bob", age = 42, height = 1.9d)
|val foo_name = foo.na@@me
|""".stripMargin
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Loading