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

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

package scala.build.bsp

import ch.epfl.scala.bsp4j.{BuildClient, LogMessageParams, MessageType}
import ch.epfl.scala.bsp4j as b

import java.io.{File, PrintWriter, StringWriter}
import java.net.URI
import java.util.concurrent.{CompletableFuture, TimeUnit}
import java.util as ju

import scala.build.Logger
import scala.build.internal.Constants
import scala.build.options.Scope
import scala.concurrent.{Future, Promise}
import scala.jdk.CollectionConverters.*
import scala.util.Random

class BspServer(
  bloopServer: b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer,
  compile: (() => CompletableFuture[b.CompileResult]) => CompletableFuture[b.CompileResult],
  logger: Logger,
  presetIntelliJ: Boolean = false
) extends BuildServerForwardStubs
    with ScalaScriptBuildServer
    with ScalaBuildServerForwardStubs
    with JavaBuildServerForwardStubs
    with JvmBuildServerForwardStubs
    with HasGeneratedSourcesImpl {

  private var client: Option[BuildClient] = None

  @volatile private var intelliJ: Boolean = presetIntelliJ
  def isIntelliJ: Boolean                 = intelliJ

  def clientOpt: Option[BuildClient] = client

  @volatile private var extraDependencySources: Seq[os.Path] = Nil
  def setExtraDependencySources(sourceJars: Seq[os.Path]): Unit = {
    extraDependencySources = sourceJars
  }

  @volatile private var extraTestDependencySources: Seq[os.Path] = Nil
  def setExtraTestDependencySources(sourceJars: Seq[os.Path]): Unit = {
    extraTestDependencySources = sourceJars
  }

  // Can we accept some errors in some circumstances?
  override protected def onFatalError(throwable: Throwable, context: String): Unit = {
    val sw = new StringWriter()
    throwable.printStackTrace(new PrintWriter(sw))
    val message =
      s"Fatal error has occured within $context. Shutting down the server:\n ${sw.toString}"
    System.err.println(message)
    client.foreach(_.onBuildLogMessage(new LogMessageParams(MessageType.ERROR, message)))

    // wait random bit before shutting down server to reduce risk of multiple scala-cli instances starting bloop at the same time
    val timeout = Random.nextInt(400)
    TimeUnit.MILLISECONDS.sleep(100 + timeout)
    sys.exit(1)
  }

  private def maybeUpdateProjectTargetUri(res: b.WorkspaceBuildTargetsResult): Unit =
    for {
      (_, n) <- projectNames.iterator
      if n.targetUriOpt.isEmpty
      target <- res.getTargets.asScala.iterator.find(_.getDisplayName == n.name)
    } n.targetUriOpt = Some(target.getId.getUri)

  private def stripInvalidTargets(params: b.WorkspaceBuildTargetsResult): Unit = {
    val updatedTargets = params
      .getTargets
      .asScala
      .filter(target => validTarget(target.getId))
      .asJava
    params.setTargets(updatedTargets)
  }

  private def check(params: b.CleanCacheParams): params.type = {
    val invalidTargets = params.getTargets.asScala.filter(!validTarget(_))
    for (target <- invalidTargets)
      logger.debug(s"invalid target in CleanCache request: $target")
    params
  }
  private def check(params: b.CompileParams): params.type = {
    val invalidTargets = params.getTargets.asScala.filter(!validTarget(_))
    for (target <- invalidTargets)
      logger.debug(s"invalid target in Compile request: $target")
    params
  }
  private def check(params: b.DependencySourcesParams): params.type = {
    val invalidTargets = params.getTargets.asScala.filter(!validTarget(_))
    for (target <- invalidTargets)
      logger.debug(s"invalid target in DependencySources request: $target")
    params
  }
  private def check(params: b.ResourcesParams): params.type = {
    val invalidTargets = params.getTargets.asScala.filter(!validTarget(_))
    for (target <- invalidTargets)
      logger.debug(s"invalid target in Resources request: $target")
    params
  }
  private def check(params: b.SourcesParams): params.type = {
    val invalidTargets = params.getTargets.asScala.filter(!validTarget(_))
    for (target <- invalidTargets)
      logger.debug(s"invalid target in Sources request: $target")
    params
  }
  private def check(params: b.TestParams): params.type = {
    val invalidTargets = params.getTargets.asScala.filter(!validTarget(_))
    for (target <- invalidTargets)
      logger.debug(s"invalid target in Test request: $target")
    params
  }
  private def check(params: b.DebugSessionParams): params.type = {
    val invalidTargets = params.getTargets.asScala.filter(!validTarget(_))
    for (target <- invalidTargets)
      logger.debug(s"invalid target in Test request: $target")
    params
  }
  private def check(params: b.OutputPathsParams): params.type = {
    val invalidTargets = params.getTargets.asScala.filter(!validTarget(_))
    for (target <- invalidTargets)
      logger.debug(s"invalid target in buildTargetOutputPaths request: $target")
    params
  }
  private def mapGeneratedSources(res: b.SourcesResult): Unit = {
    val gen = generatedSources.values.toVector
    for {
      item <- res.getItems.asScala
      if validTarget(item.getTarget)
      sourceItem <- item.getSources.asScala
      genSource  <- gen.iterator.flatMap(_.uriMap.get(sourceItem.getUri).iterator).take(1)
      updatedUri <- genSource.reportingPath.toOption.map(_.toNIO.toUri.toASCIIString)
    } {
      sourceItem.setUri(updatedUri)
      sourceItem.setGenerated(false)
    }

    // GeneratedSources not corresponding to files that exist on disk (unlike script wrappers)
    val sourcesWithReportingPathString = generatedSources.values.flatMap(_.sources)
      .filter(_.reportingPath.isLeft)

    for {
      item <- res.getItems.asScala
      if validTarget(item.getTarget)
      sourceItem <- item.getSources.asScala
      if sourcesWithReportingPathString.exists(
        _.generated.toNIO.toUri.toASCIIString == sourceItem.getUri
      )
    } sourceItem.setGenerated(true)
  }

  protected def forwardTo
    : b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer = bloopServer

  private val supportedLanguages: ju.List[String] = List(
    "scala",
    "java",
    // This makes Metals requests "wrapped sources" stuff, that makes it handle .sc files better.
    "scala-sc"
  ).asJava

  private def capabilities: b.BuildServerCapabilities = {
    val capabilities = new b.BuildServerCapabilities
    capabilities.setCompileProvider(new b.CompileProvider(supportedLanguages))
    capabilities.setTestProvider(new b.TestProvider(supportedLanguages))
    capabilities.setRunProvider(new b.RunProvider(supportedLanguages))
    capabilities.setDebugProvider(new b.DebugProvider(supportedLanguages))
    capabilities.setInverseSourcesProvider(true)
    capabilities.setDependencySourcesProvider(true)
    capabilities.setResourcesProvider(true)
    capabilities.setBuildTargetChangedProvider(true)
    capabilities.setJvmRunEnvironmentProvider(true)
    capabilities.setJvmTestEnvironmentProvider(true)
    capabilities.setCanReload(true)
    capabilities.setDependencyModulesProvider(true)
    capabilities
  }

  override def buildInitialize(
    params: b.InitializeBuildParams
  ): CompletableFuture[b.InitializeBuildResult] = {
    val res = new b.InitializeBuildResult(
      "scala-cli",
      Constants.version,
      bloop.rifle.internal.Constants.bspVersion,
      capabilities
    )
    val buildComesFromIntelliJ = params.getDisplayName.toLowerCase.contains("intellij")
    intelliJ = buildComesFromIntelliJ
    logger.debug(s"IntelliJ build: $buildComesFromIntelliJ")
    CompletableFuture.completedFuture(res)
  }

  override def onBuildInitialized(): Unit = ()

  override def buildTargetCleanCache(
    params: b.CleanCacheParams
  ): CompletableFuture[b.CleanCacheResult] =
    super.buildTargetCleanCache(check(params))

  override def buildTargetCompile(params: b.CompileParams): CompletableFuture[b.CompileResult] =
    compile(() => super.buildTargetCompile(check(params)))

  override def buildTargetDependencySources(
    params: b.DependencySourcesParams
  ): CompletableFuture[b.DependencySourcesResult] =
    super.buildTargetDependencySources(check(params)).thenApply { res =>
      val updatedItems = res.getItems.asScala.map {
        case item if validTarget(item.getTarget) =>
          val isTestTarget = item.getTarget.getUri.endsWith("-test")
          val validExtraDependencySources =
            if isTestTarget then (extraDependencySources ++ extraTestDependencySources).distinct
            else extraDependencySources
          val updatedSources = item.getSources.asScala ++ validExtraDependencySources.map {
            sourceJar =>
              sourceJar.toNIO.toUri.toASCIIString
          }
          new b.DependencySourcesItem(item.getTarget, updatedSources.asJava)
        case other => other
      }

      new b.DependencySourcesResult(updatedItems.asJava)
    }

  override def buildTargetResources(
    params: b.ResourcesParams
  ): CompletableFuture[b.ResourcesResult] =
    super.buildTargetResources(check(params))

  override def buildTargetRun(params: b.RunParams): CompletableFuture[b.RunResult] = {
    val target = params.getTarget
    if (!validTarget(target))
      logger.debug(
        s"Got invalid target in Run request: ${target.getUri} (expected ${targetScopeIdOpt(Scope.Main).orNull})"
      )
    super.buildTargetRun(params)
  }

  override def buildTargetSources(params: b.SourcesParams): CompletableFuture[b.SourcesResult] =
    super.buildTargetSources(check(params)).thenApply { res =>
      val res0 = res.duplicate()
      mapGeneratedSources(res0)
      res0
    }

  override def buildTargetTest(params: b.TestParams): CompletableFuture[b.TestResult] =
    super.buildTargetTest(check(params))

  override def debugSessionStart(params: b.DebugSessionParams)
    : CompletableFuture[b.DebugSessionAddress] =
    super.debugSessionStart(check(params))

  override def buildTargetOutputPaths(params: b.OutputPathsParams)
    : CompletableFuture[b.OutputPathsResult] = {
    check(params)
    val targets = params.getTargets.asScala.filter(validTarget)
    val outputPathsItem =
      targets
        .map(buildTargetId => (buildTargetId, targetWorkspaceDirOpt(buildTargetId)))
        .collect { case (buildTargetId, Some(targetUri)) => (buildTargetId, targetUri) }
        .map {
          case (buildTargetId, targetUri) =>
            new b.OutputPathsItem(
              buildTargetId,
              List(b.OutputPathItem(targetUri, b.OutputPathItemKind.DIRECTORY)).asJava
            )
        }

    CompletableFuture.completedFuture(new b.OutputPathsResult(outputPathsItem.asJava))
  }

  override def workspaceBuildTargets(): CompletableFuture[b.WorkspaceBuildTargetsResult] =
    super.workspaceBuildTargets().thenApply { res =>
      maybeUpdateProjectTargetUri(res)
      val res0 = res.duplicate()
      stripInvalidTargets(res0)
      for (target <- res0.getTargets.asScala) {
        val capabilities = target.getCapabilities
        capabilities.setCanDebug(true)
        val baseDirectory = new File(new URI(target.getBaseDirectory))
        if (
          isIntelliJ && baseDirectory.getName == Constants.workspaceDirName && baseDirectory.getParentFile != null
        ) {
          val newBaseDirectory = baseDirectory.getParentFile.toPath.toUri.toASCIIString
          target.setBaseDirectory(newBaseDirectory)
        }
      }
      res0
    }

  def buildTargetWrappedSources(params: WrappedSourcesParams)
    : CompletableFuture[WrappedSourcesResult] = {
    def sourcesItemOpt(scope: Scope) = targetScopeIdOpt(scope).map { id =>
      val items = generatedSources
        .getOrElse(scope, HasGeneratedSources.GeneratedSources(Nil))
        .sources
        .flatMap { s =>
          s.reportingPath.toSeq.map(_.toNIO.toUri.toASCIIString).map { uri =>
            val item    = new WrappedSourceItem(uri, s.generated.toNIO.toUri.toASCIIString)
            val content = os.read(s.generated)
            item.setTopWrapper(
              content
                .linesIterator
                .take(s.wrapperParamsOpt.map(_.topWrapperLineCount).getOrElse(0))
                .mkString("", System.lineSeparator(), System.lineSeparator())
            )
            item.setBottomWrapper("}") // meh
            item
          }
        }
      new WrappedSourcesItem(id, items.asJava)
    }
    val sourceItems = Seq(Scope.Main, Scope.Test).flatMap(sourcesItemOpt(_).toSeq)
    val res         = new WrappedSourcesResult(sourceItems.asJava)
    CompletableFuture.completedFuture(res)
  }

  private val shutdownPromise = Promise[Unit]()
  override def buildShutdown(): CompletableFuture[Object] = {
    if (!shutdownPromise.isCompleted)
      shutdownPromise.success(())
    CompletableFuture.completedFuture(null)
  }

  override def onBuildExit(): Unit = ()

  def initiateShutdown: Future[Unit] =
    shutdownPromise.future
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy