From 2d6030adba65e0edc36c454cd0052bdf6c09b432 Mon Sep 17 00:00:00 2001 From: Chris Kipp Date: Tue, 4 Jul 2023 08:00:48 +0200 Subject: [PATCH] feat: account for `ScalaDiagnostic` in diagnostic data (#5338) This pr bumps the version of BSP to the latest that includes the changes necessary for the new `ScalaDiagnostic` that is included in the `data` field of the Diagnostic coming over BSP. These include actions coming straight from the Scala 3 compiler. --- .../internal/metals/MetalsEnrichments.scala | 41 ++++++++++++ .../internal/metals/ScalacDiagnostic.scala | 10 ++- .../codeactions/ActionableDiagnostic.scala | 64 ++++++++++++++----- .../mtags/CommonMtagsEnrichments.scala | 15 +++-- project/V.scala | 2 +- 5 files changed, 108 insertions(+), 24 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index 399641a3749..baa58c11377 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -2,6 +2,7 @@ package scala.meta.internal.metals import java.io.File import java.io.IOException +import java.lang.reflect.Type import java.net.URI import java.nio.charset.StandardCharsets import java.nio.file.FileAlreadyExistsException @@ -47,6 +48,12 @@ import scala.meta.io.AbsolutePath import scala.meta.io.RelativePath import ch.epfl.scala.{bsp4j => b} +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject import fansi.ErrorMode import io.undertow.server.HttpServerExchange import org.eclipse.lsp4j.TextDocumentIdentifier @@ -679,6 +686,40 @@ object MetalsEnrichments def asTextEdit: Option[l.TextEdit] = { decodeJson(d.getData, classOf[l.TextEdit]) } + + /** + * Useful for decoded the diagnostic data since there are overlapping + * unrequired keys in the structure that causes issues when we try to + * deserialize the old top level text edit vs the newly nested actions. + */ + object DiagnosticDataDeserializer + extends JsonDeserializer[Either[l.TextEdit, b.ScalaDiagnostic]] { + override def deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext, + ): Either[l.TextEdit, b.ScalaDiagnostic] = { + json match { + case o: JsonObject if o.has("actions") => + Right(new Gson().fromJson(o, classOf[b.ScalaDiagnostic])) + case o => Left(new Gson().fromJson(o, classOf[l.TextEdit])) + } + } + } + + def asScalaDiagnostic: Option[Either[l.TextEdit, b.ScalaDiagnostic]] = { + val gson = new GsonBuilder() + .registerTypeAdapter( + classOf[Either[l.TextEdit, b.ScalaDiagnostic]], + DiagnosticDataDeserializer, + ) + .create() + decodeJson( + d.getData(), + classOf[Either[l.TextEdit, b.ScalaDiagnostic]], + Some(gson), + ) + } } implicit class XtensionSeverityBsp(sev: b.DiagnosticSeverity) { diff --git a/metals/src/main/scala/scala/meta/internal/metals/ScalacDiagnostic.scala b/metals/src/main/scala/scala/meta/internal/metals/ScalacDiagnostic.scala index a1567cf6d94..c0cd5350237 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ScalacDiagnostic.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ScalacDiagnostic.scala @@ -2,14 +2,22 @@ package scala.meta.internal.metals import scala.meta.internal.metals.MetalsEnrichments._ +import ch.epfl.scala.bsp4j import org.eclipse.{lsp4j => l} object ScalacDiagnostic { - object ScalaAction { + object LegacyScalaAction { def unapply(d: l.Diagnostic): Option[l.TextEdit] = d.asTextEdit } + object ScalaDiagnostic { + def unapply( + d: l.Diagnostic + ): Option[Either[l.TextEdit, bsp4j.ScalaDiagnostic]] = + d.asScalaDiagnostic + } + object NotAMember { private val regex = """(?s)value (.+) is not a member of.*""".r def unapply(d: l.Diagnostic): Option[String] = diff --git a/metals/src/main/scala/scala/meta/internal/metals/codeactions/ActionableDiagnostic.scala b/metals/src/main/scala/scala/meta/internal/metals/codeactions/ActionableDiagnostic.scala index 566c01525b1..b71822a7d53 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/codeactions/ActionableDiagnostic.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/codeactions/ActionableDiagnostic.scala @@ -7,6 +7,7 @@ import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals._ import scala.meta.pc.CancelToken +import ch.epfl.scala.{bsp4j => b} import org.eclipse.{lsp4j => l} class ActionableDiagnostic() extends CodeAction { @@ -19,35 +20,66 @@ class ActionableDiagnostic() extends CodeAction { def createActionableDiagnostic( diagnostic: l.Diagnostic, - textEdit: l.TextEdit, + action: Either[l.TextEdit, b.ScalaAction], ): l.CodeAction = { - val diagMessage = diagnostic.getMessage - val uri = params.getTextDocument().getUri() - - CodeActionBuilder.build( - title = - s"Apply suggestion: ${diagMessage.linesIterator.headOption.getOrElse(diagMessage)}", - kind = l.CodeActionKind.QuickFix, - changes = List(uri.toAbsolutePath -> Seq(textEdit)), - diagnostics = List(diagnostic), - ) + action match { + case Left(textEdit) => + val diagMessage = diagnostic.getMessage + val uri = params.getTextDocument().getUri() + + CodeActionBuilder.build( + title = + s"Apply suggestion: ${diagMessage.linesIterator.headOption.getOrElse(diagMessage)}", + kind = l.CodeActionKind.QuickFix, + changes = List(uri.toAbsolutePath -> Seq(textEdit)), + diagnostics = List(diagnostic), + ) + case Right(scalaAction) => + val uri = params.getTextDocument().getUri() + val edits = scalaAction + .getEdit() + .getChanges() + .asScala + .toSeq + .map(edit => + new l.TextEdit(edit.getRange().toLsp, edit.getNewText()) + ) + CodeActionBuilder.build( + title = scalaAction.getTitle(), + kind = l.CodeActionKind.QuickFix, + changes = List(uri.toAbsolutePath -> edits), + diagnostics = List(diagnostic), + ) + } + } val codeActions = params .getContext() .getDiagnostics() .asScala + .toSeq .groupBy { - case ScalacDiagnostic.ScalaAction(textEdit) => - Some(textEdit) + case ScalacDiagnostic.ScalaDiagnostic(action) => + Some(action) case _ => None } .collect { - case (Some(textEdit), diags) + case (Some(Left(textEdit)), diags) + if params.getRange().overlapsWith(diags.head.getRange()) => + Seq(createActionableDiagnostic(diags.head, Left(textEdit))) + case (Some(Right(action)), diags) if params.getRange().overlapsWith(diags.head.getRange()) => - createActionableDiagnostic(diags.head, textEdit) + action + .getActions() + .asScala + .toSeq + .map(action => + createActionableDiagnostic(diags.head, Right(action)) + ) } - .toList + .toSeq + .flatten .sorted Future.successful(codeActions) diff --git a/mtags-shared/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala b/mtags-shared/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala index 8224206a425..2911180fe16 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala @@ -35,16 +35,19 @@ trait CommonMtagsEnrichments { private def logger: Logger = Logger.getLogger(classOf[CommonMtagsEnrichments].getName) - protected def decodeJson[T](obj: AnyRef, cls: java.lang.Class[T]): Option[T] = + protected def decodeJson[T]( + obj: AnyRef, + cls: java.lang.Class[T], + gson: Option[Gson] = None + ): Option[T] = for { data <- Option(obj) value <- try { - Some( - new Gson().fromJson[T]( - data.asInstanceOf[JsonElement], - cls - ) + Option( + gson + .getOrElse(new Gson()) + .fromJson[T](data.asInstanceOf[JsonElement], cls) ) } catch { case NonFatal(e) => diff --git a/project/V.scala b/project/V.scala index a841d8048ae..3cce9c32fb6 100644 --- a/project/V.scala +++ b/project/V.scala @@ -16,7 +16,7 @@ object V { val betterMonadicFor = "0.3.1" val bloop = "1.5.6" val bloopConfig = "1.5.5" - val bsp = "2.1.0-M4" + val bsp = "2.1.0-M5" val coursier = "2.1.5" val coursierInterfaces = "1.0.18" val debugAdapter = "3.1.3"