package mill.contrib.bsp import java.io.File import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.ConcurrentHashMap import ch.epfl.scala.bsp4j._ import ch.epfl.scala.{bsp4j => bsp} import sbt.internal.inc.ManagedLoggedReporter import sbt.internal.util.ManagedLogger import xsbti.{Problem, Severity} import scala.collection.JavaConverters._ import scala.collection.concurrent import scala.compat.java8.OptionConverters._ /** * Specialized reporter that sends compilation diagnostics * for each problem it logs, either as information, warning or * error as well as task finish notifications of type `compile-report`. * * @param client the client to send diagnostics to * @param targetId the target id of the target whose compilation * the diagnostics are related to * @param taskId a unique id of the compilation task of the target * specified by `targetId` * @param compilationOriginId optional origin id the client assigned to * the compilation request. Needs to be sent * back as part of the published diagnostics * as well as compile report * @param maxErrors The maximum number of errors to be logged during the * compilation of targetId * @param logger The logger that will log the messages for each Problem. */ class BspLoggedReporter(client: bsp.BuildClient, targetId: BuildTargetIdentifier, taskId: TaskId, compilationOriginId: Option[String], maxErrors: Int, logger: ManagedLogger) extends ManagedLoggedReporter(maxErrors, logger) { var errors = new AtomicInteger(0) var warnings = new AtomicInteger(0) var infos = new AtomicInteger(0) var diagnosticMap: concurrent.Map[TextDocumentIdentifier, bsp.PublishDiagnosticsParams] = new ConcurrentHashMap[TextDocumentIdentifier, bsp.PublishDiagnosticsParams]().asScala override def logError(problem: Problem): Unit = { client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId)) errors.incrementAndGet() super.logError(problem) } override def logInfo(problem: Problem): Unit = { client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId)) infos.incrementAndGet() super.logInfo(problem) } override def logWarning(problem: Problem): Unit = { client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId)) warnings.incrementAndGet() super.logWarning(problem) } override def printSummary(): Unit = { val taskFinishParams = new TaskFinishParams(taskId, getStatusCode) taskFinishParams.setEventTime(System.currentTimeMillis()) taskFinishParams.setMessage("Finished compiling target: " + targetId.getUri) taskFinishParams.setDataKind("compile-report") val compileReport = new CompileReport(targetId, errors.get, warnings.get) compilationOriginId match { case Some(id) => compileReport.setOriginId(id) case None => } taskFinishParams.setData(compileReport) client.onBuildTaskFinish(taskFinishParams) } // Obtains the parameters for sending diagnostics about the given Problem ( as well as // about all previous problems generated for the same text file ) related to the specified // targetId, incorporating the given optional originId ( generated by the client for the // compile request ) //TODO: document that if the problem is a general information without a text document // associated to it, then the document field of the diagnostic is set to the uri of the target private[this] def getDiagnostics(problem: Problem, targetId: bsp.BuildTargetIdentifier, originId: Option[String]): bsp.PublishDiagnosticsParams = { val diagnostic = getSingleDiagnostic(problem) val sourceFile = problem.position().sourceFile().asScala val textDocument = new TextDocumentIdentifier( sourceFile.getOrElse(None) match { case None => targetId.getUri case f: File => f.toPath.toUri.toString }) val params = new bsp.PublishDiagnosticsParams(textDocument, targetId, appendDiagnostics(textDocument, diagnostic).asJava, true) if (originId.nonEmpty) { params.setOriginId(originId.get) } diagnosticMap.put(textDocument, params) params } // Compute the compilation status code private[this] def getStatusCode: StatusCode = { if (errors.get > 0) StatusCode.ERROR else StatusCode.OK } // Update the published diagnostics for the fiven text file by // adding the recently computed diagnostic to the list of // all previous diagnostics generated for the same file. private[this] def appendDiagnostics(textDocument: TextDocumentIdentifier, currentDiagnostic: Diagnostic): List[Diagnostic] = { diagnosticMap.putIfAbsent(textDocument, new bsp.PublishDiagnosticsParams( textDocument, targetId, List.empty[Diagnostic].asJava, true)) diagnosticMap(textDocument).getDiagnostics.asScala.toList ++ List(currentDiagnostic) } // Computes the diagnostic related to the given Problem private[this] def getSingleDiagnostic(problem: Problem): Diagnostic ={ val start = new bsp.Position( problem.position.startLine.asScala.getOrElse(problem.position.line.asScala.getOrElse(0)), problem.position.startOffset.asScala.getOrElse(problem.position.offset.asScala.getOrElse(0))) val end = new bsp.Position( problem.position.endLine.asScala.getOrElse(problem.position.line.asScala.getOrElse(start.getLine)), problem.position.endOffset.asScala.getOrElse(problem.position.offset.asScala.getOrElse(start.getCharacter))) val diagnostic = new bsp.Diagnostic(new bsp.Range(start, end), problem.message) diagnostic.setCode(problem.position.lineContent) diagnostic.setSource("compiler from mill") diagnostic.setSeverity( problem.severity match { case Severity.Info => bsp.DiagnosticSeverity.INFORMATION case Severity.Error => bsp.DiagnosticSeverity.ERROR case Severity.Warn => bsp.DiagnosticSeverity.WARNING } ) diagnostic } }