Skip to content

Commit 96c4b40

Browse files
harpocratestgodzik
authored andcommitted
feat: add inlay hints for by-name parameters
This adds `=>` hints for function parameters that are passed by name. The common case of block by-name arguments moves the `=>` to occur right after the opening brace instead of before the entire argument. Implements <scalameta/metals-feature-requests#377>
1 parent cfa8484 commit 96c4b40

File tree

10 files changed

+143
-6
lines changed

10 files changed

+143
-6
lines changed

Diff for: metals-bench/src/main/scala/bench/InlayHintsBench.scala

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class InlayHintsBench extends PcBenchmark {
9494
true,
9595
true,
9696
true,
97+
true,
9798
false,
9899
)
99100
pc.inlayHints(pcParams).get().asScala.toList

Diff for: metals/src/main/scala/scala/meta/internal/metals/Compilers.scala

+1
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ class Compilers(
661661
implicitParameters = options.implicitArguments,
662662
implicitConversions = options.implicitConversions,
663663
typeParameters = options.typeParameters,
664+
byNameParameters = options.byNameParameters,
664665
hintsInPatternMatch = options.hintsInPatternMatch,
665666
)
666667

Diff for: metals/src/main/scala/scala/meta/internal/metals/InlayHintsOptions.scala

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ case class InlayHintsOptions(options: Map[InlayHintsOption, Boolean])
1010
options.getOrElse(InlayHintsOption.ImplicitArguments, false)
1111
def typeParameters: Boolean =
1212
options.getOrElse(InlayHintsOption.TypeParameters, false)
13+
def byNameParameters: Boolean =
14+
options.getOrElse(InlayHintsOption.ByNameParameters, false)
1315
def hintsInPatternMatch: Boolean =
1416
options.getOrElse(InlayHintsOption.HintsInPatternMatch, false)
1517
def areSyntheticsEnabled: Boolean = options.exists(_._2)
@@ -22,6 +24,7 @@ object InlayHintsOptions {
2224
InlayHintsOption.ImplicitConversions -> true,
2325
InlayHintsOption.ImplicitArguments -> true,
2426
InlayHintsOption.TypeParameters -> true,
27+
InlayHintsOption.ByNameParameters -> true,
2528
InlayHintsOption.HintsInPatternMatch -> true,
2629
)
2730
)
@@ -33,13 +36,15 @@ object InlayHintsOption {
3336
case object ImplicitConversions extends InlayHintsOption
3437
case object ImplicitArguments extends InlayHintsOption
3538
case object TypeParameters extends InlayHintsOption
39+
case object ByNameParameters extends InlayHintsOption
3640
case object HintsInPatternMatch extends InlayHintsOption
3741
def unapply(value: String): Option[InlayHintsOption] =
3842
StringCase.kebabToCamel(value) match {
3943
case "inferredTypes" => Some(InferredType)
4044
case "implicitConversions" => Some(ImplicitConversions)
4145
case "implicitArguments" => Some(ImplicitArguments)
4246
case "typeParameters" => Some(TypeParameters)
47+
case "byNameParameters" => Some(ByNameParameters)
4348
case "hintsInPatternMatch" => Some(HintsInPatternMatch)
4449
case _ => None
4550
}

Diff for: mtags-interfaces/src/main/java/scala/meta/pc/InlayHintsParams.java

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ public interface InlayHintsParams extends RangeParams {
2424
*/
2525
boolean implicitParameters();
2626

27+
/**
28+
* Response should contain decorations for by name parameters.
29+
*/
30+
default boolean byNameParameters() {
31+
return false;
32+
}
33+
2734
/**
2835
* Response should contain decorations for implicit conversions.
2936
*/

Diff for: mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerInlayHintsParams.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ case class CompilerInlayHintsParams(
1212
inferredTypes: Boolean,
1313
typeParameters: Boolean,
1414
implicitParameters: Boolean,
15+
override val byNameParameters: Boolean,
1516
implicitConversions: Boolean,
1617
override val hintsInPatternMatch: Boolean
1718
) extends InlayHintsParams {
@@ -27,7 +28,8 @@ case class CompilerInlayHintsParams(
2728
inferredTypes = inferredTypes,
2829
typeParameters = typeParameters,
2930
implicitConversions = implicitConversions,
30-
implicitParameters = implicitParameters
31+
implicitParameters = implicitParameters,
32+
byNameParameters = byNameParameters
3133
)
3234
}
3335

Diff for: mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerSyntheticDecorationsParams.scala

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ case class CompilerSyntheticDecorationsParams(
1111
inferredTypes: Boolean,
1212
typeParameters: Boolean,
1313
implicitParameters: Boolean,
14+
byNameParameters: Boolean,
1415
implicitConversions: Boolean
1516
) extends SyntheticDecorationsParams {
1617
override def uri(): URI = virtualFileParams.uri

Diff for: mtags/src/main/scala-2/scala/meta/internal/pc/PcInlayHintsProvider.scala

+64
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ final class PcInlayHintsProvider(
7575
label,
7676
InlayHintKind.Type
7777
)
78+
case ByNameParameters(byNameArgs) =>
79+
byNameArgs.foldLeft(inlayHints) { case (ih, pos) =>
80+
ih.add(
81+
adjustPos(pos.focusStart).toLsp,
82+
List(LabelPart("=> ")),
83+
InlayHintKind.Parameter
84+
)
85+
}
7886
case InferredType(tpe, pos) if tpe != null && !tpe.isError =>
7987
val adjustedPos = adjustPos(pos).focusEnd
8088
if (inlayHints.containsDef(adjustedPos.start)) inlayHints
@@ -389,6 +397,62 @@ final class PcInlayHintsProvider(
389397
}
390398
}
391399

400+
object ByNameParameters {
401+
// Extract the positions at which `=>` hints should be inserted
402+
def unapply(tree: Tree): Option[List[Position]] =
403+
if (params.byNameParameters())
404+
tree match {
405+
case Apply(sel: Select, _)
406+
if isForComprehensionMethod(
407+
sel
408+
) || sel.symbol.name == nme.unapply =>
409+
None
410+
case Apply(fun, args) =>
411+
val params = fun.tpe.params
412+
413+
/* Special handling for a single block argument:
414+
*
415+
* someOption.getOrElse {/*=> */
416+
* fallbackCode
417+
* }
418+
*
419+
* We cannot just match on the `Block` node because the case of a single expression in
420+
* braces is not represented as a block. Instead, we search for the braces directly in
421+
* the source code.
422+
*/
423+
def singleArgBlockCase = (args, params) match {
424+
case (Seq(_), Seq(param)) if param.isByNameParam =>
425+
byNamePosForBlockLike(tree.pos.withStart(fun.pos.end))
426+
.map(List(_))
427+
case _ => None
428+
}
429+
430+
/* For all other cases, we just insert the label before the argument tree:
431+
*
432+
* someOption.getOrElse(/*=> */fallbackCode)
433+
*/
434+
Some(singleArgBlockCase.getOrElse {
435+
args
436+
.zip(params)
437+
.collect { case (tree, param) if param.isByNameParam => tree }
438+
.map(tree => tree.pos)
439+
})
440+
case _ => None
441+
}
442+
else None
443+
444+
// If the position passed in wraps a brace-delimited expression, return the position after the opening brace
445+
private def byNamePosForBlockLike(pos: Position): Option[Position] = {
446+
val start = text.indexWhere(!_.isWhitespace, pos.start)
447+
val end = text.lastIndexWhere(!_.isWhitespace, pos.end - 1)
448+
if (text.lift(start).contains('{') && text.lift(end).contains('}')) {
449+
Some(pos.withStart(start + 1))
450+
} else {
451+
None
452+
}
453+
}
454+
}
455+
392456
private def syntheticTupleApply(sel: Select): Boolean = {
393457
if (
394458
sel.tpe != null && compiler.definitions.isTupleType(

Diff for: tests/cross/src/main/scala/tests/BaseInlayHintsSuite.scala

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class BaseInlayHintsSuite extends BasePCSuite {
3636
true,
3737
true,
3838
true,
39+
true,
3940
hintsInPatternMatch
4041
)
4142

Diff for: tests/cross/src/test/scala/tests/pc/InlayHintsSuite.scala

+59-5
Original file line numberDiff line numberDiff line change
@@ -544,17 +544,17 @@ class InlayHintsSuite extends BaseInlayHintsSuite {
544544
|class DemoSpec {
545545
| import ScalatestMock._
546546
|
547-
| /*StringTestOps<<(6:17)>>(*/"foo"/*)*/ should {
548-
| /*StringTestOps<<(6:17)>>(*/"checkThing1"/*)*/ in {
547+
| /*StringTestOps<<(6:17)>>(*/"foo"/*)*/ should {/*=> */
548+
| /*StringTestOps<<(6:17)>>(*/"checkThing1"/*)*/ in {/*=> */
549549
| checkThing1[String]/*(instancesString<<(10:15)>>)*/
550550
| }/*(here<<(5:15)>>)*/
551-
| /*StringTestOps<<(6:17)>>(*/"checkThing2"/*)*/ in {
551+
| /*StringTestOps<<(6:17)>>(*/"checkThing2"/*)*/ in {/*=> */
552552
| checkThing2[String]/*(instancesString<<(10:15)>>, instancesString<<(10:15)>>)*/
553553
| }/*(here<<(5:15)>>)*/
554554
| }/*(subjectRegistrationFunction<<(3:15)>>)*/
555555
|
556-
| /*StringTestOps<<(6:17)>>(*/"bar"/*)*/ should {
557-
| /*StringTestOps<<(6:17)>>(*/"checkThing1"/*)*/ in {
556+
| /*StringTestOps<<(6:17)>>(*/"bar"/*)*/ should {/*=> */
557+
| /*StringTestOps<<(6:17)>>(*/"checkThing1"/*)*/ in {/*=> */
558558
| checkThing1[String]/*(instancesString<<(10:15)>>)*/
559559
| }/*(here<<(5:15)>>)*/
560560
| }/*(subjectRegistrationFunction<<(3:15)>>)*/
@@ -1064,4 +1064,58 @@ class InlayHintsSuite extends BaseInlayHintsSuite {
10641064
|""".stripMargin
10651065
)
10661066

1067+
check(
1068+
"by-name-regular",
1069+
"""|object Main{
1070+
| def foo(x: => Int, y: Int, z: => Int)(w: Int, v: => Int): Unit = ()
1071+
| foo(1, 2, 3)(4, 5)
1072+
|}
1073+
|""".stripMargin,
1074+
"""|object Main{
1075+
| def foo(x: => Int, y: Int, z: => Int)(w: Int, v: => Int): Unit = ()
1076+
| foo(/*=> */1, 2, /*=> */3)(4, /*=> */5)
1077+
|}
1078+
|""".stripMargin
1079+
)
1080+
1081+
check(
1082+
"by-name-block",
1083+
"""|object Main{
1084+
| def Future[A](arg: => A): A = arg
1085+
|
1086+
| Future(1 + 2)
1087+
| Future {
1088+
| 1 + 2
1089+
| }
1090+
| Future {
1091+
| val x = 1
1092+
| val y = 2
1093+
| x + y
1094+
| }
1095+
| Some(Option(2).getOrElse {
1096+
| List(1,2)
1097+
| .headOption
1098+
| })
1099+
|}
1100+
|""".stripMargin,
1101+
"""|package `by-name-block`
1102+
|object Main{
1103+
| def Future[A](arg: => A): A = arg
1104+
|
1105+
| Future/*[Int<<scala/Int#>>]*/(/*=> */1 + 2)
1106+
| Future/*[Int<<scala/Int#>>]*/ {/*=> */
1107+
| 1 + 2
1108+
| }
1109+
| Future/*[Int<<scala/Int#>>]*/ {/*=> */
1110+
| val x/*: Int<<scala/Int#>>*/ = 1
1111+
| val y/*: Int<<scala/Int#>>*/ = 2
1112+
| x + y
1113+
| }
1114+
| Some/*[Any<<scala/Any#>>]*/(Option/*[Int<<scala/Int#>>]*/(2).getOrElse/*[Any<<scala/Any#>>]*/ {/*=> */
1115+
| List/*[Int<<scala/Int#>>]*/(1,2)
1116+
| .headOption
1117+
| })
1118+
|}
1119+
|""".stripMargin
1120+
)
10671121
}

Diff for: tests/unit/src/main/scala/tests/BaseInlayHintsExpectSuite.scala

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ abstract class BaseInlayHintsExpectSuite(
3434
true,
3535
true,
3636
true,
37+
true,
3738
)
3839
val inlayHints =
3940
compiler

0 commit comments

Comments
 (0)