diff --git a/metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala index f50fe29ce83..f7edaa591ce 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala @@ -49,6 +49,7 @@ final class CodeActionProvider( new ConvertCommentCodeAction(buffers), new RemoveInvalidImportQuickFix(trees, buildTargets), new SourceRemoveInvalidImports(trees, buildTargets, diagnostics), + new ConvertToNamedLambdaParameters(trees, compilers), ) def actionsForParams(params: l.CodeActionParams): List[CodeAction] = { diff --git a/metals/src/main/scala/scala/meta/internal/metals/codeactions/ConvertToNamedLambdaParameters.scala b/metals/src/main/scala/scala/meta/internal/metals/codeactions/ConvertToNamedLambdaParameters.scala new file mode 100644 index 00000000000..c53faf1b380 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/codeactions/ConvertToNamedLambdaParameters.scala @@ -0,0 +1,102 @@ +package scala.meta.internal.metals.codeactions + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import scala.meta.Term +import scala.meta.internal.metals.Compilers +import scala.meta.internal.metals.JsonParser.XtensionSerializableToJson +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.codeactions.CodeAction +import scala.meta.internal.metals.codeactions.CodeActionBuilder +import scala.meta.internal.metals.logging +import scala.meta.internal.parsing.Trees +import scala.meta.pc.CancelToken +import scala.meta.pc.CodeActionId + +import org.eclipse.{lsp4j => l} + +/** + * Code action to convert a wildcard lambda to a lambda with named parameters + * e.g. + * + * List(1, 2).map(<<_>> + 1) => List(1, 2).map(i => i + 1) + */ +class ConvertToNamedLambdaParameters( + trees: Trees, + compilers: Compilers, +) extends CodeAction { + + override val kind: String = l.CodeActionKind.RefactorRewrite + + private case class ConvertToNamedLambdaParametersData( + codeActionId: String, + position: l.TextDocumentPositionParams, + ) extends CodeActionResolveData + + override def resolveCodeAction( + codeAction: l.CodeAction, + token: CancelToken, + )(implicit ec: ExecutionContext): Option[Future[l.CodeAction]] = { + parseData[ConvertToNamedLambdaParametersData](codeAction) match { + case Some(data) => + Some { + val uri = data.position.getTextDocument().getUri() + for { + edits <- compilers.codeAction( + data.position, + token, + CodeActionId.ConvertToNamedLambdaParameters, + None, + ) + _ = logging.logErrorWhen( + edits.isEmpty(), + s"Could not convert lambda at position ${data.position} to named lambda", + ) + workspaceEdit = new l.WorkspaceEdit(Map(uri -> edits).asJava) + _ = codeAction.setEdit(workspaceEdit) + } yield codeAction + } + case _ => None + } + } + + override def contribute( + params: l.CodeActionParams, + token: CancelToken, + )(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]] = { + val path = params.getTextDocument().getUri().toAbsolutePath + val range = params.getRange() + val maybeLambda = + trees.findLastEnclosingAt[Term.AnonymousFunction](path, range.getStart()) + maybeLambda + .map { lambda => + val position = new l.TextDocumentPositionParams( + params.getTextDocument(), + new l.Position(lambda.pos.startLine, lambda.pos.startColumn), + ) + val data = + ConvertToNamedLambdaParametersData( + codeActionId = CodeActionId.ConvertToNamedLambdaParameters, + position = position, + ) + val codeAction = CodeActionBuilder.build( + title = ConvertToNamedLambdaParameters.title, + kind = kind, + data = Some(data.toJsonObject), + ) + Future.successful(Seq(codeAction)) + } + .filter(_ => + compilers + .supportedCodeActions(path) + .contains(CodeActionId.ConvertToNamedLambdaParameters) + ) + .getOrElse(Future.successful(Nil)) + } + +} + +object ConvertToNamedLambdaParameters { + def title: String = "Convert to named lambda parameters" +} diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/CodeActionId.java b/mtags-interfaces/src/main/java/scala/meta/pc/CodeActionId.java index 582dc7b510a..bd7d1d99e3e 100644 --- a/mtags-interfaces/src/main/java/scala/meta/pc/CodeActionId.java +++ b/mtags-interfaces/src/main/java/scala/meta/pc/CodeActionId.java @@ -12,4 +12,5 @@ public class CodeActionId { public static final String InlineValue = "InlineValue"; public static final String InsertInferredType = "InsertInferredType"; public static final String InsertInferredMethod = "InsertInferredMethod"; + public static final String ConvertToNamedLambdaParameters = "ConvertToNamedLambdaParameters"; } diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/ExtractMethodProvider.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/ExtractMethodProvider.scala index 5f96b6c607d..77edbc0d4e0 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/ExtractMethodProvider.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/ExtractMethodProvider.scala @@ -14,6 +14,7 @@ final class ExtractMethodProvider( )(implicit queryInfo: PcQueryContext) extends ExtractMethodUtils { import compiler._ + def extractMethod: List[l.TextEdit] = { val text = range.text() val unit = addCompilationUnit( diff --git a/tests/cross/src/main/scala/tests/BaseExtractMethodSuite.scala b/tests/cross/src/main/scala/tests/BaseExtractMethodSuite.scala index 480bc0125f1..320ca0e3471 100644 --- a/tests/cross/src/main/scala/tests/BaseExtractMethodSuite.scala +++ b/tests/cross/src/main/scala/tests/BaseExtractMethodSuite.scala @@ -10,7 +10,6 @@ import scala.meta.internal.metals.TextEdits import munit.Location import munit.TestOptions import org.eclipse.{lsp4j => l} -import tests.BaseCodeActionSuite class BaseExtractMethodSuite extends BaseCodeActionSuite { def checkEdit( diff --git a/tests/slow/src/test/scala/tests/feature/Scala3CodeActionLspSuite.scala b/tests/slow/src/test/scala/tests/feature/Scala3CodeActionLspSuite.scala index 5c466aa7b29..e91b1078c0b 100644 --- a/tests/slow/src/test/scala/tests/feature/Scala3CodeActionLspSuite.scala +++ b/tests/slow/src/test/scala/tests/feature/Scala3CodeActionLspSuite.scala @@ -2,6 +2,7 @@ package tests.feature import scala.meta.internal.metals.BuildInfo import scala.meta.internal.metals.codeactions.ConvertToNamedArguments +import scala.meta.internal.metals.codeactions.ConvertToNamedLambdaParameters import scala.meta.internal.metals.codeactions.CreateCompanionObjectCodeAction import scala.meta.internal.metals.codeactions.ExtractMethodCodeAction import scala.meta.internal.metals.codeactions.ExtractRenameMember @@ -771,6 +772,29 @@ class Scala3CodeActionLspSuite |""".stripMargin, ) + check( + "wildcard lambda", + """|package a + | + |object A { + | val l = List(1, 2, 3) + | l.map(_ + <<1>>) + |} + |""".stripMargin, + s"""|${ConvertToNamedArguments.title("map(...)")} + |${ConvertToNamedLambdaParameters.title} + |""".stripMargin, + """|package a + | + |object A { + | val l = List(1, 2, 3) + | l.map(i => i + 1) + |} + |""".stripMargin, + selectedActionIndex = 1, + scalaVersion = "3.7.1-RC1-bin-20250501-83ffe00-NIGHTLY", + ) + private def getPath(name: String) = s"a/src/main/scala/a/$name" def checkExtractedMember(