All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scala.build.bsp.BspImpl.scala Maven / Gradle / Ivy

package scala.build.bsp

import bloop.rifle.{BloopRifleConfig, BloopServer}
import ch.epfl.scala.bsp4j as b
import com.github.plokhotnyuk.jsoniter_scala.core.{JsonReaderException, readFromArray}
import dependency.ScalaParameters
import org.eclipse.lsp4j.jsonrpc
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError

import java.io.{InputStream, OutputStream}
import java.util.UUID
import java.util.concurrent.{CompletableFuture, Executor}

import scala.build.EitherCps.{either, value}
import scala.build.*
import scala.build.compiler.BloopCompiler
import scala.build.errors.{
  BuildException,
  CompositeBuildException,
  Diagnostic,
  ParsingInputsException
}
import scala.build.input.{Inputs, ScalaCliInvokeData}
import scala.build.internal.Constants
import scala.build.options.{BuildOptions, Scope}
import scala.collection.mutable.ListBuffer
import scala.concurrent.duration.DurationInt
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.jdk.CollectionConverters.*
import scala.util.{Failure, Success}

/** The implementation for [[Bsp]].
  *
  * @param argsToInputs
  *   a function transforming terminal args to [[Inputs]]
  * @param bspReloadableOptionsReference
  *   reference to the current instance of [[BspReloadableOptions]]
  * @param threads
  *   BSP threads
  * @param in
  *   the input stream of bytes
  * @param out
  *   the output stream of bytes
  */
final class BspImpl(
  argsToInputs: Seq[String] => Either[BuildException, Inputs],
  bspReloadableOptionsReference: BspReloadableOptions.Reference,
  threads: BspThreads,
  in: InputStream,
  out: OutputStream,
  actionableDiagnostics: Option[Boolean]
)(using ScalaCliInvokeData) extends Bsp {

  import BspImpl.{PreBuildData, PreBuildProject, buildTargetIdToEvent, responseError}

  /** Sends the buildTarget/didChange BSP notification to the BSP client, indicating that the build
    * targets defined in the current session have changed.
    *
    * @param currentBloopSession
    *   the current Bloop session
    */
  private def notifyBuildChange(currentBloopSession: BloopSession): Unit = {
    val events =
      for (targetId <- currentBloopSession.bspServer.targetIds)
        yield {
          val event = new b.BuildTargetEvent(targetId)
          event.setKind(b.BuildTargetEventKind.CHANGED)
          event
        }
    val params = new b.DidChangeBuildTarget(events.asJava)
    actualLocalClient.onBuildTargetDidChange(params)
  }

  /** Initial setup for the Bloop project.
    *
    * @param currentBloopSession
    *   the current Bloop session
    * @param reloadableOptions
    *   options which may be reloaded on a bsp workspace/reload request
    * @param maybeRecoverOnError
    *   a function handling [[BuildException]] instances based on [[Scope]], possibly recovering
    *   them; returns None on recovery, Some(e: BuildException) otherwise
    */
  private def prepareBuild(
    currentBloopSession: BloopSession,
    reloadableOptions: BspReloadableOptions,
    maybeRecoverOnError: Scope => BuildException => Option[BuildException] = _ => e => Some(e)
  ): Either[(BuildException, Scope), PreBuildProject] = either[(BuildException, Scope)] {
    val logger       = reloadableOptions.logger
    val buildOptions = reloadableOptions.buildOptions
    val verbosity    = reloadableOptions.verbosity
    logger.log("Preparing build")

    val persistentLogger = new PersistentDiagnosticLogger(logger)
    val bspServer        = currentBloopSession.bspServer
    val inputs           = currentBloopSession.inputs

    // allInputs contains elements from using directives
    val (crossSources, allInputs) = value {
      CrossSources.forInputs(
        inputs = inputs,
        preprocessors = Sources.defaultPreprocessors(
          buildOptions.archiveCache,
          buildOptions.internal.javaClassNameVersionOpt,
          () => buildOptions.javaHome().value.javaCommand
        ),
        logger = persistentLogger,
        suppressWarningOptions = buildOptions.suppressWarningOptions,
        exclude = buildOptions.internal.exclude,
        maybeRecoverOnError = maybeRecoverOnError(Scope.Main)
      ).left.map((_, Scope.Main))
    }

    val sharedOptions = crossSources.sharedOptions(buildOptions)

    if (verbosity >= 3)
      pprint.err.log(crossSources)

    val scopedSources =
      value(crossSources.scopedSources(buildOptions).left.map((_, Scope.Main)))

    if (verbosity >= 3)
      pprint.err.log(scopedSources)

    val sourcesMain = value {
      scopedSources.sources(Scope.Main, sharedOptions, allInputs.workspace, persistentLogger)
        .left.map((_, Scope.Main))
    }

    val sourcesTest = value {
      scopedSources.sources(Scope.Test, sharedOptions, allInputs.workspace, persistentLogger)
        .left.map((_, Scope.Test))
    }

    if (verbosity >= 3)
      pprint.err.log(sourcesMain)

    val options0Main = sourcesMain.buildOptions
    val options0Test = sourcesTest.buildOptions.orElse(options0Main)

    val generatedSourcesMain = sourcesMain.generateSources(allInputs.generatedSrcRoot(Scope.Main))
    val generatedSourcesTest = sourcesTest.generateSources(allInputs.generatedSrcRoot(Scope.Test))

    bspServer.setExtraDependencySources(options0Main.classPathOptions.extraSourceJars)
    bspServer.setExtraTestDependencySources(options0Test.classPathOptions.extraSourceJars)
    bspServer.setGeneratedSources(Scope.Main, generatedSourcesMain)
    bspServer.setGeneratedSources(Scope.Test, generatedSourcesTest)

    val (classesDir0Main, scalaParamsMain, artifactsMain, projectMain, buildChangedMain) = value {
      val res = Build.prepareBuild(
        allInputs,
        sourcesMain,
        generatedSourcesMain,
        options0Main,
        None,
        Scope.Main,
        currentBloopSession.remoteServer,
        persistentLogger,
        localClient,
        maybeRecoverOnError(Scope.Main)
      )
      res.left.map((_, Scope.Main))
    }

    val (classesDir0Test, scalaParamsTest, artifactsTest, projectTest, buildChangedTest) = value {
      val res = Build.prepareBuild(
        allInputs,
        sourcesTest,
        generatedSourcesTest,
        options0Test,
        None,
        Scope.Test,
        currentBloopSession.remoteServer,
        persistentLogger,
        localClient,
        maybeRecoverOnError(Scope.Test)
      )
      res.left.map((_, Scope.Test))
    }

    localClient.setGeneratedSources(Scope.Main, generatedSourcesMain)
    localClient.setGeneratedSources(Scope.Test, generatedSourcesTest)

    val mainScope = PreBuildData(
      sourcesMain,
      options0Main,
      classesDir0Main,
      scalaParamsMain,
      artifactsMain,
      projectMain,
      generatedSourcesMain,
      buildChangedMain
    )

    val testScope = PreBuildData(
      sourcesTest,
      options0Test,
      classesDir0Test,
      scalaParamsTest,
      artifactsTest,
      projectTest,
      generatedSourcesTest,
      buildChangedTest
    )

    if (actionableDiagnostics.getOrElse(true)) {
      val projectOptions = options0Test.orElse(options0Main)
      projectOptions.logActionableDiagnostics(persistentLogger)
    }

    PreBuildProject(mainScope, testScope, persistentLogger.diagnostics)
  }

  private def buildE(
    currentBloopSession: BloopSession,
    notifyChanges: Boolean,
    reloadableOptions: BspReloadableOptions
  ): Either[(BuildException, Scope), Unit] = {
    def doBuildOnce(data: PreBuildData, scope: Scope): Either[(BuildException, Scope), Build] =
      Build.buildOnce(
        currentBloopSession.inputs,
        data.sources,
        data.generatedSources,
        data.buildOptions,
        scope,
        reloadableOptions.logger,
        actualLocalClient,
        currentBloopSession.remoteServer,
        partialOpt = None
      ).left.map(_ -> scope)

    either[(BuildException, Scope)] {
      val preBuild = value(prepareBuild(currentBloopSession, reloadableOptions))
      if (notifyChanges && (preBuild.mainScope.buildChanged || preBuild.testScope.buildChanged))
        notifyBuildChange(currentBloopSession)
      value(doBuildOnce(preBuild.mainScope, Scope.Main))
      value(doBuildOnce(preBuild.testScope, Scope.Test))
      ()
    }
  }

  private def build(
    currentBloopSession: BloopSession,
    client: BspClient,
    notifyChanges: Boolean,
    reloadableOptions: BspReloadableOptions
  ): Unit =
    buildE(currentBloopSession, notifyChanges, reloadableOptions) match {
      case Left((ex, scope)) =>
        client.reportBuildException(
          currentBloopSession.bspServer.targetScopeIdOpt(scope),
          ex
        )
        reloadableOptions.logger.debug(s"Caught $ex during BSP build, ignoring it")
      case Right(()) =>
        for (targetId <- currentBloopSession.bspServer.targetIds)
          client.resetBuildExceptionDiagnostics(targetId)
    }

  private val shownGlobalMessages =
    new java.util.concurrent.ConcurrentHashMap[String, Unit]()

  private def showGlobalWarningOnce(msg: String): Unit =
    shownGlobalMessages.computeIfAbsent(
      msg,
      _ => {
        val params = new b.ShowMessageParams(b.MessageType.WARNING, msg)
        actualLocalClient.onBuildShowMessage(params)
      }
    )

  /** Compilation logic, to be called on a buildTarget/compile BSP request.
    *
    * @param currentBloopSession
    *   the current Bloop session
    * @param executor
    *   executor
    * @param reloadableOptions
    *   options which may be reloaded on a bsp workspace/reload request
    * @param doCompile
    *   (self-)reference to calling the compilation logic
    * @return
    *   a future of [[b.CompileResult]]
    */
  private def compile(
    currentBloopSession: BloopSession,
    executor: Executor,
    reloadableOptions: BspReloadableOptions,
    doCompile: () => CompletableFuture[b.CompileResult]
  ): CompletableFuture[b.CompileResult] = {
    val preBuild = CompletableFuture.supplyAsync(
      () =>
        prepareBuild(currentBloopSession, reloadableOptions) match {
          case Right(preBuild) =>
            if (preBuild.mainScope.buildChanged || preBuild.testScope.buildChanged)
              notifyBuildChange(currentBloopSession)
            Right(preBuild)
          case Left((ex, scope)) =>
            Left((ex, scope))
        },
      executor
    )

    preBuild.thenCompose {
      case Left((ex, scope)) =>
        val taskId = new b.TaskId(UUID.randomUUID().toString)

        for targetId <- currentBloopSession.bspServer.targetScopeIdOpt(scope) do {
          val target = targetId.getUri match {
            case s"$_?id=$targetId" => targetId
            case targetIdUri        => targetIdUri
          }

          val taskStartParams = new b.TaskStartParams(taskId)
          taskStartParams.setEventTime(System.currentTimeMillis())
          taskStartParams.setMessage(s"Preprocessing '$target'")
          taskStartParams.setDataKind(b.TaskStartDataKind.COMPILE_TASK)
          taskStartParams.setData(new b.CompileTask(targetId))

          actualLocalClient.onBuildTaskStart(taskStartParams)

          actualLocalClient.reportBuildException(
            Some(targetId),
            ex
          )

          val taskFinishParams = new b.TaskFinishParams(taskId, b.StatusCode.ERROR)
          taskFinishParams.setEventTime(System.currentTimeMillis())
          taskFinishParams.setMessage(s"Preprocessed '$target'")
          taskFinishParams.setDataKind(b.TaskFinishDataKind.COMPILE_REPORT)

          val errorSize = ex match {
            case c: CompositeBuildException => c.exceptions.size
            case _                          => 1
          }

          taskFinishParams.setData(new b.CompileReport(targetId, errorSize, 0))

          actualLocalClient.onBuildTaskFinish(taskFinishParams)
        }

        CompletableFuture.completedFuture(
          new b.CompileResult(b.StatusCode.ERROR)
        )
      case Right(params) =>
        for (targetId <- currentBloopSession.bspServer.targetIds)
          actualLocalClient.resetBuildExceptionDiagnostics(targetId)

        val targetId = currentBloopSession.bspServer.targetIds.head
        actualLocalClient.reportDiagnosticsForFiles(targetId, params.diagnostics, reset = false)

        doCompile().thenCompose { res =>
          def doPostProcess(data: PreBuildData, scope: Scope): Unit =
            for (sv <- data.project.scalaCompiler.map(_.scalaVersion))
              Build.postProcess(
                data.generatedSources,
                currentBloopSession.inputs.generatedSrcRoot(scope),
                data.classesDir,
                reloadableOptions.logger,
                currentBloopSession.inputs.workspace,
                updateSemanticDbs = true,
                scalaVersion = sv
              ).left.foreach(_.foreach(showGlobalWarningOnce))

          if (res.getStatusCode == b.StatusCode.OK)
            CompletableFuture.supplyAsync(
              () => {
                doPostProcess(params.mainScope, Scope.Main)
                doPostProcess(params.testScope, Scope.Test)
                res
              },
              executor
            )
          else
            CompletableFuture.completedFuture(res)
        }
    }
  }

  private var actualLocalClient: BspClient                     = _
  private var localClient: b.BuildClient with BloopBuildClient = _

  /** Returns a reference to the [[BspClient]], respecting the given verbosity
    * @param verbosity
    *   verbosity to be passed to the resulting [[BspImpl.LoggingBspClient]]
    * @return
    *   BSP client
    */
  private def getLocalClient(verbosity: Int): b.BuildClient with BloopBuildClient =
    if (verbosity >= 3)
      new BspImpl.LoggingBspClient(actualLocalClient)
    else
      actualLocalClient

  /** Creates a fresh Bloop session
    * @param inputs
    *   all the inputs to be included in the session's context
    * @param reloadableOptions
    *   options which may be reloaded on a bsp workspace/reload request
    * @param presetIntelliJ
    *   a flag marking if this is in context of a BSP connection with IntelliJ (allowing to pass
    *   this setting from a past session)
    * @return
    *   a new [[BloopSession]]
    */
  private def newBloopSession(
    inputs: Inputs,
    reloadableOptions: BspReloadableOptions,
    presetIntelliJ: Boolean = false
  ): BloopSession = {
    val logger       = reloadableOptions.logger
    val buildOptions = reloadableOptions.buildOptions
    val createBloopServer =
      () =>
        BloopServer.buildServer(
          reloadableOptions.bloopRifleConfig,
          "scala-cli",
          Constants.version,
          (inputs.workspace / Constants.workspaceDirName).toNIO,
          Build.classesRootDir(inputs.workspace, inputs.projectName).toNIO,
          localClient,
          threads.buildThreads.bloop,
          logger.bloopRifleLogger
        )
    val remoteServer = new BloopCompiler(
      createBloopServer,
      20.seconds,
      strictBloopJsonCheck = buildOptions.internal.strictBloopJsonCheckOrDefault
    )
    lazy val bspServer = new BspServer(
      remoteServer.bloopServer.server,
      doCompile =>
        compile(bloopSession0, threads.prepareBuildExecutor, reloadableOptions, doCompile),
      logger,
      presetIntelliJ
    )

    lazy val watcher = new Build.Watcher(
      ListBuffer(),
      threads.buildThreads.fileWatcher,
      build(bloopSession0, actualLocalClient, notifyChanges = true, reloadableOptions),
      ()
    )
    lazy val bloopSession0: BloopSession = BloopSession(inputs, remoteServer, bspServer, watcher)

    bloopSession0.registerWatchInputs()
    bspServer.newInputs(inputs)

    bloopSession0
  }

  private val bloopSession = new BloopSession.Reference

  /** The logic for the actual running of the `bsp` command, initializing the BSP connection.
    * @param initialInputs
    *   the initial input sources passed upon initializing the BSP connection (which are subject to
    *   change on subsequent workspace/reload requests)
    */
  def run(initialInputs: Inputs, initialBspOptions: BspReloadableOptions): Future[Unit] = {
    val logger    = initialBspOptions.logger
    val verbosity = initialBspOptions.verbosity

    actualLocalClient = new BspClient(
      threads.buildThreads.bloop.jsonrpc, // meh
      logger
    )
    localClient = getLocalClient(verbosity)

    val currentBloopSession = newBloopSession(initialInputs, initialBspOptions)
    bloopSession.update(null, currentBloopSession, "BSP server already initialized")

    val actualLocalServer
      : b.BuildServer with b.ScalaBuildServer with b.JavaBuildServer with b.JvmBuildServer
        with ScalaScriptBuildServer with HasGeneratedSources =
      new BuildServerProxy(
        () => bloopSession.get().bspServer,
        () => onReload()
      )

    val localServer: b.BuildServer with b.ScalaBuildServer with b.JavaBuildServer
      with b.JvmBuildServer with ScalaScriptBuildServer =
      if (verbosity >= 3)
        new LoggingBuildServerAll(actualLocalServer)
      else
        actualLocalServer

    val launcher = new jsonrpc.Launcher.Builder[b.BuildClient]()
      .setExecutorService(threads.buildThreads.bloop.jsonrpc) // FIXME No
      .setInput(in)
      .setOutput(out)
      .setRemoteInterface(classOf[b.BuildClient])
      .setLocalService(localServer)
      .create()
    val remoteClient = launcher.getRemoteProxy
    actualLocalClient.forwardToOpt = Some(remoteClient)

    actualLocalClient.newInputs(initialInputs)
    currentBloopSession.resetDiagnostics(actualLocalClient)

    val recoverOnError: Scope => BuildException => Option[BuildException] = scope =>
      e => {
        actualLocalClient.reportBuildException(actualLocalServer.targetScopeIdOpt(scope), e)
        logger.log(e)
        None
      }

    prepareBuild(
      currentBloopSession,
      initialBspOptions,
      maybeRecoverOnError = recoverOnError
    ) match {
      case Left((ex, scope)) => recoverOnError(scope)(ex)
      case Right(_)          =>
    }

    logger.log {
      val hasConsole = System.console() != null
      if (hasConsole)
        "Listening to incoming JSONRPC BSP requests, press Ctrl+D to exit."
      else
        "Listening to incoming JSONRPC BSP requests."
    }
    val f = launcher.startListening()

    val initiateFirstBuild: Runnable = { () =>
      try build(currentBloopSession, actualLocalClient, notifyChanges = false, initialBspOptions)
      catch {
        case t: Throwable =>
          logger.debug(s"Caught $t during initial BSP build, ignoring it")
      }
    }
    threads.prepareBuildExecutor.submit(initiateFirstBuild)

    val es = ExecutionContext.fromExecutorService(threads.buildThreads.bloop.jsonrpc)
    val futures = Seq(
      BspImpl.naiveJavaFutureToScalaFuture(f).map(_ => ())(es),
      currentBloopSession.bspServer.initiateShutdown
    )
    Future.firstCompletedOf(futures)(es)
  }

  /** Shuts down the current Bloop session
    */
  def shutdown(): Unit =
    for (currentBloopSession <- bloopSession.getAndNullify())
      currentBloopSession.dispose()

  /** BSP reload logic, to be used on a workspace/reload BSP request
    *
    * @param currentBloopSession
    *   the current Bloop session
    * @param previousInputs
    *   all the input sources present in the context before the reload
    * @param newInputs
    *   all the input sources to be included in the new context after the reload
    * @param reloadableOptions
    *   options which may be reloaded on a bsp workspace/reload request
    * @return
    *   a future containing a valid workspace/reload response
    */
  private def reloadBsp(
    currentBloopSession: BloopSession,
    previousInputs: Inputs,
    newInputs: Inputs,
    reloadableOptions: BspReloadableOptions
  ): CompletableFuture[AnyRef] = {
    val previousTargetIds = currentBloopSession.bspServer.targetIds
    val wasIntelliJ       = currentBloopSession.bspServer.isIntelliJ

    currentBloopSession.dispose()
    val newBloopSession0 = newBloopSession(newInputs, reloadableOptions, wasIntelliJ)
    bloopSession.update(currentBloopSession, newBloopSession0, "Concurrent reload of workspace")
    actualLocalClient.newInputs(newInputs)

    newBloopSession0.resetDiagnostics(actualLocalClient)
    prepareBuild(newBloopSession0, reloadableOptions) match {
      case Left((buildException, scope)) =>
        CompletableFuture.completedFuture(
          responseError(
            s"Can't reload workspace, build failed for scope ${scope.name}: ${buildException.message}"
          )
        )
      case Right(preBuildProject) =>
        lazy val projectJavaHome = preBuildProject.mainScope.buildOptions
          .javaHome()
          .value

        val finalBloopSession =
          if (
            bloopSession.get().remoteServer.jvmVersion.exists(_.value < projectJavaHome.version)
          ) {
            reloadableOptions.logger.log(
              s"Bloop JVM version too low, current ${bloopSession.get().remoteServer.jvmVersion.get.value} expected ${projectJavaHome.version}, restarting server"
            )
            // RelodableOptions don't take into account buildOptions from sources
            val updatedReloadableOptions = reloadableOptions.copy(
              buildOptions =
                reloadableOptions.buildOptions orElse preBuildProject.mainScope.buildOptions,
              bloopRifleConfig = reloadableOptions.bloopRifleConfig.copy(
                javaPath = projectJavaHome.javaCommand,
                minimumBloopJvm = projectJavaHome.version
              )
            )

            newBloopSession0.dispose()
            val bloopSessionWithJvmOkay =
              newBloopSession(newInputs, updatedReloadableOptions, wasIntelliJ)
            bloopSession.update(
              newBloopSession0,
              bloopSessionWithJvmOkay,
              "Concurrent reload of workspace"
            )
            bloopSessionWithJvmOkay
          }
          else newBloopSession0

        if (previousInputs.projectName != preBuildProject.mainScope.project.projectName)
          for (client <- finalBloopSession.bspServer.clientOpt) {
            val newTargetIds = finalBloopSession.bspServer.targetIds
            val events =
              newTargetIds.map(buildTargetIdToEvent(_, b.BuildTargetEventKind.CREATED)) ++
                previousTargetIds.map(buildTargetIdToEvent(_, b.BuildTargetEventKind.DELETED))
            val didChangeBuildTargetParams = new b.DidChangeBuildTarget(events.asJava)
            client.onBuildTargetDidChange(didChangeBuildTargetParams)
          }
        CompletableFuture.completedFuture(new Object())
    }
  }

  /** All the logic surrounding a workspace/reload (establishing the new inputs, settings and
    * refreshing all the relevant variables), including the actual BSP workspace reloading.
    *
    * @return
    *   a future containing a valid workspace/reload response
    */
  private def onReload(): CompletableFuture[AnyRef] = {
    val currentBloopSession = bloopSession.get()
    bspReloadableOptionsReference.reload()
    val reloadableOptions = bspReloadableOptionsReference.get
    val logger            = reloadableOptions.logger
    val verbosity         = reloadableOptions.verbosity
    actualLocalClient.logger = logger
    localClient = getLocalClient(verbosity)
    val ideInputsJsonPath =
      currentBloopSession.inputs.workspace / Constants.workspaceDirName / "ide-inputs.json"
    if (os.isFile(ideInputsJsonPath)) {
      val maybeResponse = either[BuildException] {
        val ideInputs = value {
          try Right(readFromArray(os.read.bytes(ideInputsJsonPath))(IdeInputs.codec))
          catch {
            case e: JsonReaderException =>
              logger.debug(s"Caught $e while decoding $ideInputsJsonPath")
              Left(new ParsingInputsException(e.getMessage, e))
          }
        }
        val newInputs      = value(argsToInputs(ideInputs.args))
        val newHash        = newInputs.sourceHash()
        val previousInputs = currentBloopSession.inputs
        val previousHash   = currentBloopSession.inputsHash
        if newInputs == previousInputs && newHash == previousHash then
          CompletableFuture.completedFuture(new Object)
        else reloadBsp(currentBloopSession, previousInputs, newInputs, reloadableOptions)
      }
      maybeResponse match {
        case Left(errorMessage) =>
          CompletableFuture.completedFuture(
            responseError(s"Workspace reload failed, couldn't load sources: $errorMessage")
          )
        case Right(r) => r
      }
    }
    else
      CompletableFuture.completedFuture(
        responseError(
          s"Workspace reload failed, inputs file missing from workspace directory: ${ideInputsJsonPath.toString()}"
        )
      )
  }
}

object BspImpl {

  private def buildTargetIdToEvent(
    targetId: b.BuildTargetIdentifier,
    eventKind: b.BuildTargetEventKind
  ): b.BuildTargetEvent = {
    val event = new b.BuildTargetEvent(targetId)
    event.setKind(eventKind)
    event
  }

  private def responseError(
    message: String,
    errorCode: Int = JsonRpcErrorCodes.InternalError
  ): ResponseError =
    new ResponseError(errorCode, message, new Object())

  // from https://github.com/com-lihaoyi/Ammonite/blob/7eb58c58ec8c252dc5bd1591b041fcae01cccf90/amm/interp/src/main/scala/ammonite/interp/script/AmmoniteBuildServer.scala#L550-L565
  private def naiveJavaFutureToScalaFuture[T](
    f: java.util.concurrent.Future[T]
  ): Future[T] = {
    val p = Promise[T]()
    val t = new Thread {
      setDaemon(true)
      setName("bsp-wait-for-exit")
      override def run(): Unit =
        p.complete {
          try Success(f.get())
          catch { case t: Throwable => Failure(t) }
        }
    }
    t.start()
    p.future
  }

  private final class LoggingBspClient(actualLocalClient: BspClient) extends LoggingBuildClient
      with BloopBuildClient {
    // in Scala 3 type of the method needs to be explicitly overridden
    def underlying: scala.build.bsp.BspClient = actualLocalClient
    def clear()                               = underlying.clear()
    def diagnostics                           = underlying.diagnostics
    def setProjectParams(newParams: Seq[String]) =
      underlying.setProjectParams(newParams)
    def setGeneratedSources(scope: Scope, newGeneratedSources: Seq[GeneratedSource]) =
      underlying.setGeneratedSources(scope, newGeneratedSources)
  }

  private final case class PreBuildData(
    sources: Sources,
    buildOptions: BuildOptions,
    classesDir: os.Path,
    scalaParams: Option[ScalaParameters],
    artifacts: Artifacts,
    project: Project,
    generatedSources: Seq[GeneratedSource],
    buildChanged: Boolean
  )

  private final case class PreBuildProject(
    mainScope: PreBuildData,
    testScope: PreBuildData,
    diagnostics: Seq[Diagnostic]
  )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy