Skip to content

Commit 9c8b63d

Browse files
committed
fix: hover and go to definition for named tuples
1 parent a5a9fc8 commit 9c8b63d

File tree

5 files changed

+111
-33
lines changed

5 files changed

+111
-33
lines changed

Diff for: presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala

+36-14
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ object HoverProvider:
104104
) match
105105
case Nil =>
106106
fallbackToDynamics(path, printer, contentType)
107-
case (symbol, tpe) :: _
107+
case (symbol, tpe, _) :: _
108108
if symbol.name == nme.selectDynamic || symbol.name == nme.applyDynamic =>
109109
fallbackToDynamics(path, printer, contentType)
110-
case symbolTpes @ ((symbol, tpe) :: _) =>
110+
case symbolTpes @ ((symbol, tpe, None) :: _) =>
111111
val exprTpw = tpe.widenTermRefExpr.deepDealias
112112
val hoverString =
113113
tpw match
@@ -153,6 +153,21 @@ object HoverProvider:
153153
case _ =>
154154
ju.Optional.empty().nn
155155
end match
156+
case (_, tpe, Some(namedTupleArg)) :: _ =>
157+
val exprTpw = tpe.widenTermRefExpr.deepDealias
158+
printer.expressionType(exprTpw) match
159+
case Some(tpe) =>
160+
ju.Optional.of(
161+
new ScalaHover(
162+
expressionType = Some(tpe),
163+
symbolSignature = Some(s"$namedTupleArg: $tpe"),
164+
docstring = None,
165+
forceExpressionType = false,
166+
contextInfo = printer.getUsedRenamesInfo,
167+
contentType = contentType
168+
)
169+
).nn
170+
case _ => ju.Optional.empty().nn
156171
end match
157172
end if
158173
end hover
@@ -165,23 +180,30 @@ object HoverProvider:
165180
printer: ShortenedTypePrinter,
166181
contentType: ContentType
167182
)(using Context): ju.Optional[HoverSignature] = path match
168-
case SelectDynamicExtractor(sel, n, name) =>
183+
case SelectDynamicExtractor(sel, n, name, rest) =>
169184
def findRefinement(tp: Type): Option[HoverSignature] =
170185
tp match
171-
case RefinedType(_, refName, tpe) if name == refName.toString() =>
186+
case RefinedType(_, refName, tpe) if (name == refName.toString() || refName.toString() == nme.Fields.toString()) =>
187+
val resultType =
188+
rest match
189+
case Select(_, asInstanceOf) :: TypeApply(_, List(tpe)) :: _ if asInstanceOf == nme.asInstanceOfPM => tpe.tpe
190+
case _ if n == nme.selectDynamic => tpe.resultType
191+
case _ => tpe
192+
172193
val tpeString =
173-
if n == nme.selectDynamic then s": ${printer.tpe(tpe.resultType)}"
174-
else printer.tpe(tpe)
194+
if n == nme.selectDynamic then s": ${printer.tpe(resultType)}"
195+
else printer.tpe(resultType)
175196

176197
val valOrDef =
177-
if n == nme.selectDynamic && !tpe.isInstanceOf[ExprType]
178-
then "val"
179-
else "def"
198+
if refName.toString() == nme.Fields.toString() then ""
199+
else if n == nme.selectDynamic && !tpe.isInstanceOf[ExprType]
200+
then "val "
201+
else "def "
180202

181203
Some(
182204
new ScalaHover(
183205
expressionType = Some(tpeString),
184-
symbolSignature = Some(s"$valOrDef $name$tpeString"),
206+
symbolSignature = Some(s"$valOrDef$name$tpeString"),
185207
contextInfo = printer.getUsedRenamesInfo,
186208
contentType = contentType
187209
)
@@ -208,16 +230,16 @@ object SelectDynamicExtractor:
208230
case Select(_, _) :: Apply(
209231
Select(Apply(reflSel, List(sel)), n),
210232
List(Literal(Constant(name: String)))
211-
) :: _
233+
) :: rest
212234
if (n == nme.selectDynamic || n == nme.applyDynamic) &&
213235
nme.reflectiveSelectable == reflSel.symbol.name =>
214-
Some(sel, n, name)
236+
Some(sel, n, name, rest)
215237
// tests `selectable`, `selectable2` and `selectable-full` in HoverScala3TypeSuite
216238
case Select(_, _) :: Apply(
217239
Select(sel, n),
218240
List(Literal(Constant(name: String)))
219-
) :: _ if n == nme.selectDynamic || n == nme.applyDynamic =>
220-
Some(sel, n, name)
241+
) :: rest if n == nme.selectDynamic || n == nme.applyDynamic =>
242+
Some(sel, n, name, rest)
221243
case _ => None
222244
end match
223245
end unapply

Diff for: presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala

+35-18
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import scala.annotation.tailrec
55

66
import dotc.*
77
import ast.*, tpd.*
8+
import dotty.tools.dotc.core.Constants.*
89
import core.*, Contexts.*, Flags.*, Names.*, Symbols.*, Types.*
10+
import dotty.tools.dotc.core.StdNames.*
911
import interactive.*
1012
import util.*
1113
import util.SourcePosition
14+
import dotty.tools.pc.utils.InteractiveEnrichments.*
1215

1316
object MetalsInteractive:
17+
type NamedTupleArg = String
1418

1519
def contextOfStat(
1620
stats: List[Tree],
@@ -110,67 +114,80 @@ object MetalsInteractive:
110114
pos: SourcePosition,
111115
indexed: IndexedContext,
112116
skipCheckOnName: Boolean = false
113-
): List[(Symbol, Type)] =
117+
): List[(Symbol, Type, Option[String])] =
114118
import indexed.ctx
115119
path match
120+
// Handle select on named tuples
121+
case (Apply(Apply(TypeApply(fun, List(t1, t2)), List(ddef)), List(Literal(Constant(i: Int))))) :: _
122+
if fun.symbol.exists && fun.symbol.name == nme.apply &&
123+
fun.symbol.owner.exists && fun.symbol.owner == getModuleIfDefined("scala.NamedTuple").moduleClass =>
124+
def getIndex(t: Tree): Option[Type] =
125+
t.tpe.dealias match
126+
case AppliedType(_, args) => args.get(i)
127+
case _ => None
128+
val name = getIndex(t1) match
129+
case Some(c: ConstantType) => c.value.stringValue
130+
case _ => ""
131+
val tpe = getIndex(t2).getOrElse(NoType)
132+
List((ddef.symbol, tpe, Some(name)))
116133
// For a named arg, find the target `DefDef` and jump to the param
117134
case NamedArg(name, _) :: Apply(fn, _) :: _ =>
118135
val funSym = fn.symbol
119136
if funSym.is(Synthetic) && funSym.owner.is(CaseClass) then
120137
val sym = funSym.owner.info.member(name).symbol
121-
List((sym, sym.info))
138+
List((sym, sym.info, None))
122139
else
123140
val paramSymbol =
124141
for param <- funSym.paramSymss.flatten.find(_.name == name)
125142
yield param
126143
val sym = paramSymbol.getOrElse(fn.symbol)
127-
List((sym, sym.info))
144+
List((sym, sym.info, None))
128145

129146
case (_: untpd.ImportSelector) :: (imp: Import) :: _ =>
130147
importedSymbols(imp, _.span.contains(pos.span)).map(sym =>
131-
(sym, sym.info)
148+
(sym, sym.info, None)
132149
)
133150

134151
case (imp: Import) :: _ =>
135152
importedSymbols(imp, _.span.contains(pos.span)).map(sym =>
136-
(sym, sym.info)
153+
(sym, sym.info, None)
137154
)
138155

139156
// wildcard param
140157
case head :: _ if (head.symbol.is(Param) && head.symbol.is(Synthetic)) =>
141-
List((head.symbol, head.typeOpt))
158+
List((head.symbol, head.typeOpt, None))
142159

143160
case (head @ Select(target, name)) :: _
144161
if head.symbol.is(Synthetic) && name == StdNames.nme.apply =>
145162
val sym = target.symbol
146163
if sym.is(Synthetic) && sym.is(Module) then
147-
List((sym.companionClass, sym.companionClass.info))
148-
else List((target.symbol, target.typeOpt))
164+
List((sym.companionClass, sym.companionClass.info, None))
165+
else List((target.symbol, target.typeOpt, None))
149166

150167
// L@@ft(...)
151168
case (head @ ApplySelect(select)) :: _
152169
if select.qualifier.sourcePos.contains(pos) &&
153170
select.name == StdNames.nme.apply =>
154-
List((head.symbol, head.typeOpt))
171+
List((head.symbol, head.typeOpt, None))
155172

156173
// for Inlined we don't have a symbol, but it's needed to show proper type
157174
case (head @ Inlined(call, bindings, expansion)) :: _ =>
158-
List((call.symbol, head.typeOpt))
175+
List((call.symbol, head.typeOpt, None))
159176

160177
// for comprehension
161178
case (head @ ApplySelect(select)) :: _ if isForSynthetic(head) =>
162179
// If the cursor is on the qualifier, return the symbol for it
163180
// `for { x <- List(1).head@@Option }` returns the symbol of `headOption`
164181
if select.qualifier.sourcePos.contains(pos) then
165-
List((select.qualifier.symbol, select.qualifier.typeOpt))
182+
List((select.qualifier.symbol, select.qualifier.typeOpt, None))
166183
// Otherwise, returns the symbol of for synthetics such as "withFilter"
167-
else List((head.symbol, head.typeOpt))
184+
else List((head.symbol, head.typeOpt, None))
168185

169186
// f@@oo.bar
170187
case Select(target, _) :: _
171188
if target.span.isSourceDerived &&
172189
target.sourcePos.contains(pos) =>
173-
List((target.symbol, target.typeOpt))
190+
List((target.symbol, target.typeOpt, None))
174191

175192
/* In some cases type might be represented by TypeTree, however it's possible
176193
* that the type tree will not be marked properly as synthetic even if it doesn't
@@ -185,7 +202,7 @@ object MetalsInteractive:
185202
*/
186203
case (tpt: TypeTree) :: parent :: _
187204
if tpt.span != parent.span && !tpt.symbol.is(Synthetic) =>
188-
List((tpt.symbol, tpt.typeOpt))
205+
List((tpt.symbol, tpt.typeOpt, None))
189206

190207
/* TypeTest class https://dotty.epfl.ch/docs/reference/other-new-features/type-test.html
191208
* compiler automatically adds unapply if possible, we need to find the type symbol
@@ -195,14 +212,14 @@ object MetalsInteractive:
195212
pat match
196213
case UnApply(fun, _, pats) =>
197214
val tpeSym = pats.head.typeOpt.typeSymbol
198-
List((tpeSym, tpeSym.info))
215+
List((tpeSym, tpeSym.info, None))
199216
case _ =>
200217
Nil
201218

202219
case path @ head :: tail =>
203220
if head.symbol.is(Exported) then
204221
val sym = head.symbol.sourceSymbol
205-
List((sym, sym.info))
222+
List((sym, sym.info, None))
206223
else if head.symbol.is(Synthetic) then
207224
enclosingSymbolsWithExpressionType(
208225
tail,
@@ -217,7 +234,7 @@ object MetalsInteractive:
217234
pos,
218235
indexed.ctx.source
219236
)
220-
then List((head.symbol, head.typeOpt))
237+
then List((head.symbol, head.typeOpt, None))
221238
/* Type tree for List(1) has an Int type variable, which has span
222239
* but doesn't exist in code.
223240
* https://github.com/scala/scala3/issues/15937
@@ -234,7 +251,7 @@ object MetalsInteractive:
234251
indexed,
235252
skipCheckOnName
236253
)
237-
else recovered.map(sym => (sym, sym.info))
254+
else recovered.map(sym => (sym, sym.info, None))
238255
end if
239256
case Nil => Nil
240257
end match

Diff for: presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class PcDefinitionProvider(
101101
val enclosing = path.expandRangeToEnclosingApply(pos)
102102
val typeSymbols = MetalsInteractive
103103
.enclosingSymbolsWithExpressionType(enclosing, pos, indexed)
104-
.map { case (_, tpe) =>
104+
.map { case (_, tpe, _) =>
105105
tpe.typeSymbol
106106
}
107107
typeSymbols match

Diff for: presentation-compiler/test/dotty/tools/pc/tests/definition/PcDefinitionSuite.scala

+9
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,12 @@ class PcDefinitionSuite extends BasePcDefinitionSuite:
504504
|val a = MyIntOut(1).un@@even
505505
|""".stripMargin,
506506
)
507+
508+
@Test def `named-tuples` =
509+
check(
510+
"""|import scala.language.experimental.namedTuples
511+
|
512+
|val <<foo>> = (name = "Bob", age = 42, height = 1.9d)
513+
|val foo_name = foo.na@@me
514+
|""".stripMargin
515+
)

Diff for: presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTermSuite.scala

+30
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,33 @@ class HoverTermSuite extends BaseHoverSuite:
717717
|""".stripMargin,
718718
"""def ???: Nothing""".stripMargin.hover
719719
)
720+
721+
@Test def `named-tuples`: Unit =
722+
check(
723+
"""import scala.language.experimental.namedTuples
724+
|
725+
|val foo = (name = "Bob", age = 42, height = 1.9d)
726+
|val foo_name = foo.na@@me
727+
|""".stripMargin,
728+
"name: String".hover
729+
)
730+
731+
@Test def `named-tuples2`: Unit =
732+
check(
733+
"""|import scala.language.experimental.namedTuples
734+
|
735+
|import NamedTuple.*
736+
|
737+
|class NamedTupleSelectable extends Selectable {
738+
| type Fields <: AnyNamedTuple
739+
| def selectDynamic(name: String): Any = ???
740+
|}
741+
|
742+
|val person = new NamedTupleSelectable {
743+
| type Fields = (name: String, city: String)
744+
|}
745+
|
746+
|val person_name = person.na@@me
747+
|""".stripMargin,
748+
"name: String".hover
749+
)

0 commit comments

Comments
 (0)