Skip to content

Commit 6fd2149

Browse files
committed
feat: implement 'convert to named lambda parameters' code action
1 parent 057e7bc commit 6fd2149

File tree

14 files changed

+564
-1
lines changed

14 files changed

+564
-1
lines changed

metals/src/main/scala/scala/meta/internal/metals/Compilers.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,20 @@ class Compilers(
860860
}
861861
}.getOrElse(Future.successful(Nil.asJava))
862862

863+
def convertToNamedLambdaParameters(
864+
position: TextDocumentPositionParams,
865+
token: CancelToken,
866+
): Future[ju.List[TextEdit]] = {
867+
withPCAndAdjustLsp(position) { (pc, pos, adjust) =>
868+
pc.convertToNamedLambdaParameters(
869+
CompilerOffsetParamsUtils.fromPos(pos, token)
870+
).asScala
871+
.map { edits =>
872+
adjust.adjustTextEdits(edits)
873+
}
874+
}
875+
}.getOrElse(Future.successful(Nil.asJava))
876+
863877
def implementAbstractMembers(
864878
params: TextDocumentPositionParams,
865879
token: CancelToken,

metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,20 @@ object ServerCommands {
662662
|""".stripMargin,
663663
)
664664

665+
final case class ConvertToNamedLambdaParametersRequest(
666+
position: TextDocumentPositionParams
667+
)
668+
val ConvertToNamedLambdaParameters =
669+
new ParametrizedCommand[ConvertToNamedLambdaParametersRequest](
670+
"convert-to-named-lambda-parameters",
671+
"Convert wildcard lambda parameters to named parameters",
672+
"""|Whenever a user chooses code action to convert to named lambda parameters, this command is later run to
673+
|rewrite the lambda to use named parameters.
674+
|""".stripMargin,
675+
"""|Object with [TextDocumentPositionParams](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentPositionParams) of the target lambda
676+
|""".stripMargin,
677+
)
678+
665679
val GotoLog = new Command(
666680
"goto-log",
667681
"Check logs",

metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ final class CodeActionProvider(
5959
new MillifyDependencyCodeAction(buffers),
6060
new MillifyScalaCliDependencyCodeAction(buffers),
6161
new ConvertCommentCodeAction(buffers),
62+
new ConvertToNamedLambdaParameters(trees, compilers, languageClient),
6263
)
6364

6465
def codeActions(
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package scala.meta.internal.metals.codeactions
2+
3+
import scala.concurrent.ExecutionContext
4+
import scala.concurrent.Future
5+
6+
import scala.meta.Term
7+
import scala.meta.internal.metals.Compilers
8+
import scala.meta.internal.metals.MetalsEnrichments._
9+
import scala.meta.internal.metals.ServerCommands
10+
import scala.meta.internal.metals.clients.language.MetalsLanguageClient
11+
import scala.meta.internal.metals.codeactions.CodeAction
12+
import scala.meta.internal.metals.codeactions.CodeActionBuilder
13+
import scala.meta.internal.metals.logging
14+
import scala.meta.internal.parsing.Trees
15+
import scala.meta.pc.CancelToken
16+
17+
import org.eclipse.{lsp4j => l}
18+
19+
/**
20+
* Code action to convert a wildcard lambda to a lambda with named parameters
21+
* e.g.
22+
*
23+
* List(1, 2).map(<<_>> + 1) => List(1, 2).map(i => i + 1)
24+
*/
25+
class ConvertToNamedLambdaParameters(
26+
trees: Trees,
27+
compilers: Compilers,
28+
languageClient: MetalsLanguageClient,
29+
) extends CodeAction {
30+
31+
override val kind: String = l.CodeActionKind.RefactorRewrite
32+
33+
override type CommandData =
34+
ServerCommands.ConvertToNamedLambdaParametersRequest
35+
36+
override def command: Option[ActionCommand] = Some(
37+
ServerCommands.ConvertToNamedLambdaParameters
38+
)
39+
40+
override def handleCommand(
41+
data: ServerCommands.ConvertToNamedLambdaParametersRequest,
42+
token: CancelToken,
43+
)(implicit ec: ExecutionContext): Future[Unit] = {
44+
val uri = data.position.getTextDocument().getUri()
45+
for {
46+
edits <- compilers.convertToNamedLambdaParameters(
47+
data.position,
48+
token,
49+
)
50+
_ = logging.logErrorWhen(
51+
edits.isEmpty(),
52+
s"Could not convert lambda at position ${data.position} to named lambda",
53+
)
54+
workspaceEdit = new l.WorkspaceEdit(Map(uri -> edits).asJava)
55+
_ <- languageClient
56+
.applyEdit(new l.ApplyWorkspaceEditParams(workspaceEdit))
57+
.asScala
58+
} yield ()
59+
}
60+
61+
override def contribute(
62+
params: l.CodeActionParams,
63+
token: CancelToken,
64+
)(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]] = {
65+
val path = params.getTextDocument().getUri().toAbsolutePath
66+
val range = params.getRange()
67+
val maybeLambda =
68+
trees.findLastEnclosingAt[Term.AnonymousFunction](path, range.getStart())
69+
maybeLambda
70+
.map { lambda =>
71+
val position = new l.TextDocumentPositionParams(
72+
params.getTextDocument(),
73+
new l.Position(lambda.pos.startLine, lambda.pos.startColumn),
74+
)
75+
val command =
76+
ServerCommands.ConvertToNamedLambdaParameters.toLsp(
77+
ServerCommands.ConvertToNamedLambdaParametersRequest(position)
78+
)
79+
val codeAction = CodeActionBuilder.build(
80+
title = ConvertToNamedLambdaParameters.title,
81+
kind = kind,
82+
command = Some(command),
83+
)
84+
Future.successful(Seq(codeAction))
85+
}
86+
.getOrElse(Future.successful(Nil))
87+
}
88+
89+
}
90+
91+
object ConvertToNamedLambdaParameters {
92+
def title: String = "Convert to named lambda parameters"
93+
}

mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ public CompletableFuture<List<TextEdit>> inlineValue(OffsetParams params) {
159159
public abstract CompletableFuture<List<TextEdit>> convertToNamedArguments(OffsetParams params,
160160
List<Integer> argIndices);
161161

162+
/**
163+
* Return the text edits for converting a wildcard lambda to a named lambda.
164+
*/
165+
public abstract CompletableFuture<List<TextEdit>> convertToNamedLambdaParameters(OffsetParams params);
166+
162167
/**
163168
* The text contents of the given file changed.
164169
*/

mtags-java/src/main/scala/scala/meta/internal/pc/JavaPresentationCompiler.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ case class JavaPresentationCompiler(
140140
): CompletableFuture[util.List[TextEdit]] =
141141
CompletableFuture.completedFuture(Nil.asJava)
142142

143+
override def convertToNamedLambdaParameters(
144+
params: OffsetParams
145+
): CompletableFuture[util.List[TextEdit]] =
146+
CompletableFuture.completedFuture(Nil.asJava)
147+
143148
override def inlayHints(
144149
params: InlayHintsParams
145150
): CompletableFuture[util.List[lsp4j.InlayHint]] =
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package scala.meta.internal.pc
2+
3+
import scala.meta.pc.OffsetParams
4+
5+
import org.eclipse.{lsp4j => l}
6+
7+
final class ConvertToNamedLambdaParametersProvider(
8+
val compiler: MetalsGlobal,
9+
offsetParam: OffsetParams
10+
) {
11+
12+
def convertToNamedLambdaParameters: Either[String, List[l.TextEdit]] = {
13+
Right(Nil)
14+
}
15+
16+
}

mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,23 @@ case class ScalaPresentationCompiler(
294294
}
295295
}
296296

297+
override def convertToNamedLambdaParameters(
298+
params: OffsetParams
299+
): CompletableFuture[ju.List[TextEdit]] = {
300+
val empty: Either[String, List[TextEdit]] = Right(List())
301+
(compilerAccess
302+
.withInterruptableCompiler(Some(params))(empty, params.token) { pc =>
303+
new ConvertToNamedLambdaParametersProvider(
304+
pc.compiler(),
305+
params
306+
).convertToNamedLambdaParameters
307+
})
308+
.thenApply {
309+
case Left(error: String) => throw new DisplayableException(error)
310+
case Right(edits: List[TextEdit]) => edits.asJava
311+
}
312+
}
313+
297314
override def autoImports(
298315
name: String,
299316
params: OffsetParams,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package scala.meta.internal.mtags
2+
3+
/**
4+
* Helpers for generating variable names based on the desired types.
5+
*/
6+
object TermNameInference {
7+
8+
/** Single character names for types. (`Int` => `i`, `i1`, `i2`, ...) */
9+
def singleLetterNameStream(typeName: String): LazyList[String] = {
10+
val typeName1 = sanitizeInput(typeName)
11+
val firstCharStr = typeName1.headOption.getOrElse('x').toLower.toString
12+
numberedStreamFromName(firstCharStr)
13+
}
14+
15+
/** Names only from upper case letters (`OnDemandSymbolIndex` => `odsi`, `odsi1`, `odsi2`, ...) */
16+
def shortNameStream(typeName: String): LazyList[String] = {
17+
val typeName1 = sanitizeInput(typeName)
18+
val upperCases = typeName1.filter(_.isUpper).map(_.toLower)
19+
val name = if (upperCases.isEmpty) typeName1 else upperCases
20+
numberedStreamFromName(name)
21+
}
22+
23+
/** Names from lower case letters (`OnDemandSymbolIndex` => `onDemandSymbolIndex`, `onDemandSymbolIndex1`, ...) */
24+
def fullNameStream(typeName: String): LazyList[String] = {
25+
val typeName1 = sanitizeInput(typeName)
26+
val withFirstLower =
27+
typeName1.headOption.map(_.toLower).getOrElse('x') + typeName1.drop(1)
28+
numberedStreamFromName(withFirstLower)
29+
}
30+
31+
/** A lazy list of names: a, b, ..., z, aa, ab, ..., az, ba, bb, ... */
32+
def saneNamesStream: LazyList[String] = {
33+
val letters = ('a' to 'z').map(_.toString)
34+
def computeNext(acc: String): String = {
35+
if (acc.last == 'z')
36+
computeNext(acc.init) + letters.head
37+
else
38+
acc.init + letters(letters.indexOf(acc.last) + 1)
39+
}
40+
def loop(acc: String): LazyList[String] =
41+
acc #:: loop(computeNext(acc))
42+
loop("a")
43+
}
44+
45+
private def sanitizeInput(typeName: String): String =
46+
typeName.filter(_.isLetterOrDigit)
47+
48+
private def numberedStreamFromName(name: String): LazyList[String] = {
49+
val rest = LazyList.from(1).map(name + _)
50+
name #:: rest
51+
}
52+
}

0 commit comments

Comments
 (0)