From 88fec28aafcd217a241b7618a14a047c600c0606 Mon Sep 17 00:00:00 2001 From: Katarzyna Marek Date: Wed, 30 Aug 2023 10:20:53 +0200 Subject: [PATCH] improvement: show bsp errors to the user --- .../internal/builds/BSPErrorHandler.scala | 126 ++++++++++++++++++ .../metals/ForwardingMetalsBuildClient.scala | 4 +- .../internal/metals/MetalsEnrichments.scala | 2 +- .../internal/metals/MetalsLspService.scala | 10 ++ .../internal/metals/debug/DebugProxy.scala | 2 +- .../internal/worksheets/MdocEnrichments.scala | 4 +- 6 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala diff --git a/metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala b/metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala new file mode 100644 index 00000000000..9e74080243f --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala @@ -0,0 +1,126 @@ +package scala.meta.internal.builds + +import java.nio.file.Files +import java.util.concurrent.atomic.AtomicReference + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future +import scala.util.control.NonFatal + +import scala.meta.internal.bsp.BspSession +import scala.meta.internal.metals.ClientCommands +import scala.meta.internal.metals.Directories +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.clients.language.MetalsLanguageClient +import scala.meta.io.AbsolutePath + +import org.eclipse.{lsp4j => l} + +class BspErrorHandler( + languageClient: MetalsLanguageClient, + workspaceFolder: AbsolutePath, + restartBspServer: () => Future[Boolean], + currentSession: () => Option[BspSession], +)(implicit context: ExecutionContext) { + + private def logsPath = workspaceFolder.resolve(Directories.log) + private val lastError = new AtomicReference[String]("") + + def onError(message: String): Unit = { + if (shouldShowBspError) { + val previousError = lastError.getAndSet(message) + if (message != previousError) { + showError(message) + } + } else { + scribe.error(message) + } + } + + private def shouldShowBspError = currentSession().exists(session => + session.main.isBloop || session.main.isScalaCLI + ) + + private def showError(message: String): Future[Unit] = { + val bspError = s"${BspErrorHandler.errorInBsp}: $message" + scribe.error(bspError) + val params = BspErrorHandler.makeShowMessage(message) + languageClient.showMessageRequest(params).asScala.flatMap { + case BspErrorHandler.goToLogs => + val errorMsgStartLine = + bspError.linesIterator.headOption + .flatMap(findLine(_)) + .getOrElse(0) + Future.successful(gotoLogs(errorMsgStartLine)) + case BspErrorHandler.restartBuildServer => + restartBspServer().ignoreValue + case _ => Future.successful(()) + } + } + + private def findLine(line: String): Option[Int] = + try { + val lineNumber = + Files + .readAllLines(logsPath.toNIO) + .asScala + .lastIndexWhere(_.contains(line)) + if (lineNumber >= 0) Some(lineNumber) else None + } catch { + case NonFatal(_) => None + } + + private def gotoLogs(line: Int) = { + val pos = new l.Position(line, 0) + val location = new l.Location( + logsPath.toURI.toString(), + new l.Range(pos, pos), + ) + languageClient.metalsExecuteClientCommand( + ClientCommands.GotoLocation.toExecuteCommandParams( + ClientCommands.WindowLocation( + location.getUri(), + location.getRange(), + ) + ) + ) + } +} + +object BspErrorHandler { + def makeShowMessage( + message: String + ): l.ShowMessageRequestParams = { + val (msg, actions) = + if (message.length() <= MESSAGE_MAX_LENGTH) { + ( + s"""|$errorHeader + |$message""".stripMargin, + List(restartBuildServer, dismiss), + ) + } else { + ( + s"""|$errorHeader + |${message.take(MESSAGE_MAX_LENGTH)}... + |$gotoLogsToSeeFull""".stripMargin, + List(goToLogs, restartBuildServer, dismiss), + ) + } + val params = new l.ShowMessageRequestParams() + params.setType(l.MessageType.Error) + params.setMessage(msg) + params.setActions(actions.asJava) + params + } + + private val errorHeader = "Encountered an error in the build server:" + private val goToLogs = new l.MessageActionItem("Go to logs.") + private val restartBuildServer = + new l.MessageActionItem("Restart build server.") + private val dismiss = new l.MessageActionItem("Dismiss.") + private val gotoLogsToSeeFull = "Go to logs to see the full error" + private val errorInBsp = "Build server error:" + + private val MESSAGE_MAX_LENGTH = 150 + +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/ForwardingMetalsBuildClient.scala b/metals/src/main/scala/scala/meta/internal/metals/ForwardingMetalsBuildClient.scala index c6ed5d98969..a7238847985 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ForwardingMetalsBuildClient.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ForwardingMetalsBuildClient.scala @@ -8,6 +8,7 @@ import scala.collection.concurrent.TrieMap import scala.concurrent.Promise import scala.util.control.NonFatal +import scala.meta.internal.builds.BspErrorHandler import scala.meta.internal.metals.BuildTargets import scala.meta.internal.metals.Cancelable import scala.meta.internal.metals.ClientConfiguration @@ -41,6 +42,7 @@ final class ForwardingMetalsBuildClient( didCompile: CompileReport => Unit, onBuildTargetDidCompile: BuildTargetIdentifier => Unit, onBuildTargetDidChangeFunc: b.DidChangeBuildTarget => Unit, + bspErrorHandler: BspErrorHandler, ) extends MetalsBuildClient with Cancelable { @@ -97,7 +99,7 @@ final class ForwardingMetalsBuildClient( def onBuildLogMessage(params: l.MessageParams): Unit = params.getType match { case l.MessageType.Error => - scribe.error(params.getMessage) + bspErrorHandler.onError(params.getMessage()) case l.MessageType.Warning => scribe.warn(params.getMessage) case l.MessageType.Info => 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 713721db95e..2c34a699cbc 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -1147,7 +1147,7 @@ object MetalsEnrichments * As long as the color codes are valid this should correctly strip * anything that is ESC (U+001B) plus [ */ - def filerANSIColorCodes(str: String): String = + def filterANSIColorCodes(str: String): String = str.replaceAll("\u001B\\[[;\\d]*m", "") } diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index bc8d095894d..54c65d8a6f5 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -28,6 +28,7 @@ import scala.meta.internal.bsp.BspSession import scala.meta.internal.bsp.BuildChange import scala.meta.internal.bsp.ScalaCliBspScope import scala.meta.internal.builds.BloopInstall +import scala.meta.internal.builds.BspErrorHandler import scala.meta.internal.builds.BuildServerProvider import scala.meta.internal.builds.BuildTool import scala.meta.internal.builds.BuildToolSelector @@ -365,6 +366,14 @@ class MetalsLspService( ) ) + private val bspErrorHandler: BspErrorHandler = + new BspErrorHandler( + languageClient, + folder, + restartBspServer, + () => bspSession, + ) + private val buildClient: ForwardingMetalsBuildClient = new ForwardingMetalsBuildClient( languageClient, @@ -390,6 +399,7 @@ class MetalsLspService( onBuildTargetDidChangeFunc = params => { maybeQuickConnectToBuildServer(params) }, + bspErrorHandler, ) private val bloopServers: BloopServers = new BloopServers( diff --git a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProxy.scala b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProxy.scala index a8fae0395cf..47a8baf1909 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProxy.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProxy.scala @@ -217,7 +217,7 @@ private[debug] final class DebugProxy( case message @ OutputNotification(output) if stripColor => val raw = output.getOutput() - val msgWithoutColorCodes = filerANSIColorCodes(raw) + val msgWithoutColorCodes = filterANSIColorCodes(raw) output.setOutput(msgWithoutColorCodes) message.setParams(output) client.consume(message) diff --git a/metals/src/main/scala/scala/meta/internal/worksheets/MdocEnrichments.scala b/metals/src/main/scala/scala/meta/internal/worksheets/MdocEnrichments.scala index b87aeb12ac3..d8ae80113a3 100644 --- a/metals/src/main/scala/scala/meta/internal/worksheets/MdocEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/worksheets/MdocEnrichments.scala @@ -67,9 +67,9 @@ object MdocEnrichments { statement: EvaluatedWorksheetStatement ) { def prettyDetails(): String = - MetalsEnrichments.filerANSIColorCodes(statement.details()) + MetalsEnrichments.filterANSIColorCodes(statement.details()) def prettySummary(): String = - MetalsEnrichments.filerANSIColorCodes(statement.summary()) + MetalsEnrichments.filterANSIColorCodes(statement.summary()) } }