Skip to content

Commit

Permalink
feat: account for ScalaDiagnostic in diagnostic data (#5338)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ckipp01 authored Jul 4, 2023
1 parent 977d53e commit 2d6030a
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
2 changes: 1 addition & 1 deletion project/V.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 2d6030a

Please sign in to comment.