Skip to content

Commit

Permalink
fix: hover and go to definition for named tuples
Browse files Browse the repository at this point in the history
  • Loading branch information
kasiaMarek committed Dec 12, 2024
1 parent d685c0b commit 00cacd0
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 33 deletions.
50 changes: 36 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,30 @@ 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
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 +230,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,80 @@ object MetalsInteractive:
pos: SourcePosition,
indexed: IndexedContext,
skipCheckOnName: Boolean = false
): List[(Symbol, Type)] =
): List[(Symbol, Type, Option[String])] =
import indexed.ctx
path match
// 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)))
// 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 +202,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 +212,14 @@ 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

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 +234,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 +251,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
)

0 comments on commit 00cacd0

Please sign in to comment.