From c6e307baee41158028af7aa72f6b4da866749b54 Mon Sep 17 00:00:00 2001 From: kasiaMarek Date: Thu, 20 Feb 2025 12:56:20 +0100 Subject: [PATCH] feat: convert to enum code action --- .../meta/internal/metals/Compilers.scala | 23 ++++ .../codeactions/CodeActionProvider.scala | 1 + .../codeactions/ConvertToEnumCodeAction.scala | 109 ++++++++++++++++++ .../main/java/scala/meta/pc/CodeActionId.java | 1 + .../test/scala/tests/ConvertToEnumSuite.scala | 28 +++++ 5 files changed, 162 insertions(+) create mode 100644 metals/src/main/scala/scala/meta/internal/metals/codeactions/ConvertToEnumCodeAction.scala create mode 100644 tests/unit/src/test/scala/tests/ConvertToEnumSuite.scala diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala index 9eb664f7a37..672a2950eb0 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala @@ -735,6 +735,29 @@ class Compilers( } }.getOrElse(Future.successful(Nil.asJava)) + def convertToEnum( + params: TextDocumentPositionParams, + token: CancelToken, + ): Future[ju.List[TextEdit]] = { + withPCAndAdjustLsp(params) { (pc, pos, adjust) => + if (pc.supportedCodeActions().contains(CodeActionId.ConvertToEnum)) { + val offsetParams = CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) + val result = pc.codeAction( + offsetParams, + CodeActionId.ConvertToEnum, + ju.Optional.empty(), + ) + result.asScala.map { edits => + adjust.adjustTextEdits(edits) + } + } else Future.successful(List.empty[TextEdit].asJava) + }.getOrElse(Future.successful(Nil.asJava)) + } + def inlineEdits( params: TextDocumentPositionParams, token: CancelToken, 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 5ed5356e5e3..9863297cd49 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 @@ -46,6 +46,7 @@ final class CodeActionProvider( new MillifyDependencyCodeAction(buffers), new MillifyScalaCliDependencyCodeAction(buffers), new ConvertCommentCodeAction(buffers), + new ConvertToEnumCodeAction(trees, compilers), ) def actionsForParams(params: l.CodeActionParams): List[CodeAction] = { diff --git a/metals/src/main/scala/scala/meta/internal/metals/codeactions/ConvertToEnumCodeAction.scala b/metals/src/main/scala/scala/meta/internal/metals/codeactions/ConvertToEnumCodeAction.scala new file mode 100644 index 00000000000..611d9a09e11 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/codeactions/ConvertToEnumCodeAction.scala @@ -0,0 +1,109 @@ +package scala.meta.internal.metals.codeactions + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import scala.meta.Defn +import scala.meta.Mod +import scala.meta.Tree +import scala.meta.internal.metals.Compilers +import scala.meta.internal.metals.JsonParser.XtensionSerializableToJson +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.parsing.Trees +import scala.meta.pc.CancelToken +import scala.meta.pc.CodeActionId + +import org.eclipse.lsp4j +import org.eclipse.lsp4j.CodeActionParams +import org.eclipse.{lsp4j => l} + +class ConvertToEnumCodeAction( + trees: Trees, + compilers: Compilers, +) extends CodeAction { + + case class ConvertToEnumCodeActionData( + position: l.TextDocumentPositionParams + ) extends CodeActionResolveData + + override def kind: String = l.CodeActionKind.RefactorRewrite + + override def contribute(params: CodeActionParams, token: CancelToken)(implicit + ec: ExecutionContext + ): Future[Seq[lsp4j.CodeAction]] = { + val path = params.getTextDocument().getUri().toAbsolutePath + if ( + compilers + .supportedCodeActions(path) + .asScala + .contains(CodeActionId.ConvertToEnum) + ) { + val range = params.getRange() + val maybeDefn = for { + term <- trees.findLastEnclosingAt[Tree]( + path, + range.getStart(), + { + case _: Defn.Class | _: Defn.Trait => true + case _ => false + }, + ) + } yield term + + def codeAction(name: String, isTrait: Boolean) = + Future.successful( + Seq( + CodeActionBuilder.build( + title = ConvertToEnumCodeAction.title(name, isTrait), + kind = kind, + data = Some( + ConvertToEnumCodeActionData( + new l.TextDocumentPositionParams( + params.getTextDocument(), + params.getRange().getStart(), + ) + ).toJsonObject + ), + ) + ) + ) + + maybeDefn match { + case Some(d: Defn.Class) + if d.name.pos + .encloses(range) && d.mods.exists(_.isInstanceOf[Mod.Sealed]) => + codeAction(d.name.value, isTrait = false) + case Some(d: Defn.Trait) + if d.name.pos + .encloses(range) && d.mods.exists(_.isInstanceOf[Mod.Sealed]) => + codeAction(d.name.value, isTrait = true) + case _ => Future.successful(Seq()) + } + } else Future.successful(Seq()) + } + + override def resolveCodeAction( + codeAction: lsp4j.CodeAction, + token: CancelToken, + )(implicit ec: ExecutionContext): Option[Future[lsp4j.CodeAction]] = + parseData[ConvertToEnumCodeActionData](codeAction) match { + case Some(data) => + Some( + compilers + .convertToEnum(data.position, token) + .map(edits => { + val uri = data.position.getTextDocument().getUri() + val workspaceEdit = new l.WorkspaceEdit(Map(uri -> edits).asJava) + codeAction.setEdit(workspaceEdit) + codeAction + }) + ) + case None => None + } + +} + +object ConvertToEnumCodeAction { + def title(name: String, isTrait: Boolean): String = + s"Convert sealed ${if (isTrait) "trait" else "class"} $name to enum." +} 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..f8112c5a85b 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 ConvertToEnum = "ConvertToEnum"; } diff --git a/tests/unit/src/test/scala/tests/ConvertToEnumSuite.scala b/tests/unit/src/test/scala/tests/ConvertToEnumSuite.scala new file mode 100644 index 00000000000..35590255974 --- /dev/null +++ b/tests/unit/src/test/scala/tests/ConvertToEnumSuite.scala @@ -0,0 +1,28 @@ +package tests + +import scala.meta.internal.metals.BuildInfo +import scala.meta.internal.metals.codeactions.ConvertToEnumCodeAction + +import org.eclipse.lsp4j.CodeAction +import tests.codeactions.BaseCodeActionLspSuite + +class ConvertToEnumSuite extends BaseCodeActionLspSuite("convertToEnum") { + override protected val scalaVersion: String = BuildInfo.latestScala3Next + + val filterAction: CodeAction => Boolean = + _.getTitle().startsWith("Convert sealed") + + check( + "basic".ignore, // not yet implemented + """|sealed trait <>ow + |object Cow: + | class HolsteinFriesian extends Cow + | class Highland extends Cow + | class BrownSwiss extends Cow + |""".stripMargin, + ConvertToEnumCodeAction.title("Cow", isTrait = true), + """|enum Cow: + | case HolsteinFriesian, Highland, BrownSwiss + |""".stripMargin, + ) +}