Merge pull request #6553 from retronym/topic/bsp-sbt-target

Support the SBT extension in BSP import
This commit is contained in:
eugene yokota 2021-07-05 12:51:34 -04:00 committed by GitHub
commit 1497686e57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 335 additions and 66 deletions

View File

@ -164,7 +164,10 @@ def mimaSettingsSince(versions: Seq[String]): Seq[Def.Setting[_]] = Def settings
exclude[FinalClassProblem]("sbt.internal.*"), exclude[FinalClassProblem]("sbt.internal.*"),
exclude[FinalMethodProblem]("sbt.internal.*"), exclude[FinalMethodProblem]("sbt.internal.*"),
exclude[IncompatibleResultTypeProblem]("sbt.internal.*"), exclude[IncompatibleResultTypeProblem]("sbt.internal.*"),
exclude[ReversedMissingMethodProblem]("sbt.internal.*") exclude[ReversedMissingMethodProblem]("sbt.internal.*"),
exclude[DirectMissingMethodProblem]("sbt.PluginData.apply"),
exclude[DirectMissingMethodProblem]("sbt.PluginData.copy"),
exclude[DirectMissingMethodProblem]("sbt.PluginData.this"),
), ),
) )

View File

@ -429,7 +429,7 @@ object Defaults extends BuildCommon {
LanguageServerProtocol.handler(fileConverter.value), LanguageServerProtocol.handler(fileConverter.value),
BuildServerProtocol.handler( BuildServerProtocol.handler(
loadedBuild.value, loadedBuild.value,
bspWorkspace.value, bspFullWorkspace.value,
sbtVersion.value, sbtVersion.value,
semanticdbEnabled.value, semanticdbEnabled.value,
semanticdbVersion.value semanticdbVersion.value

View File

@ -143,14 +143,18 @@ final case class PluginData(
definitionClasspath: Seq[Attributed[File]], definitionClasspath: Seq[Attributed[File]],
resolvers: Option[Vector[Resolver]], resolvers: Option[Vector[Resolver]],
report: Option[UpdateReport], report: Option[UpdateReport],
scalacOptions: Seq[String] scalacOptions: Seq[String],
unmanagedSourceDirectories: Seq[File],
unmanagedSources: Seq[File],
managedSourceDirectories: Seq[File],
managedSources: Seq[File]
) { ) {
val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath
} }
object PluginData { object PluginData {
private[sbt] def apply(dependencyClasspath: Def.Classpath): PluginData = private[sbt] def apply(dependencyClasspath: Def.Classpath): PluginData =
PluginData(dependencyClasspath, Nil, None, None, Nil) PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
} }
object EvaluateTask { object EvaluateTask {

View File

@ -10,7 +10,6 @@ package sbt
import java.nio.file.{ Path => NioPath } import java.nio.file.{ Path => NioPath }
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import lmcoursier.definitions.{ CacheLogger, ModuleMatchers, Reconciliation } import lmcoursier.definitions.{ CacheLogger, ModuleMatchers, Reconciliation }
import lmcoursier.{ CoursierConfiguration, FallbackDependency } import lmcoursier.{ CoursierConfiguration, FallbackDependency }
import org.apache.ivy.core.module.descriptor.ModuleDescriptor import org.apache.ivy.core.module.descriptor.ModuleDescriptor
@ -26,6 +25,7 @@ import sbt.internal.inc.ScalaInstance
import sbt.internal.io.WatchState import sbt.internal.io.WatchState
import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt } import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt }
import sbt.internal.remotecache.RemoteCacheArtifact import sbt.internal.remotecache.RemoteCacheArtifact
import sbt.internal.server.BuildServerProtocol.BspFullWorkspace
import sbt.internal.server.{ BuildServerReporter, ServerHandler } import sbt.internal.server.{ BuildServerReporter, ServerHandler }
import sbt.internal.util.{ AttributeKey, ProgressState, SourcePosition } import sbt.internal.util.{ AttributeKey, ProgressState, SourcePosition }
import sbt.io._ import sbt.io._
@ -398,8 +398,10 @@ object Keys {
val bspConfig = taskKey[Unit]("Create or update the BSP connection files").withRank(DSetting) val bspConfig = taskKey[Unit]("Create or update the BSP connection files").withRank(DSetting)
val bspEnabled = SettingKey[Boolean](BasicKeys.bspEnabled) val bspEnabled = SettingKey[Boolean](BasicKeys.bspEnabled)
val bspSbtEnabled = settingKey[Boolean]("Should BSP export meta-targets for the SBT build itself?")
val bspTargetIdentifier = settingKey[BuildTargetIdentifier]("Build target identifier of a project and configuration.").withRank(DSetting) val bspTargetIdentifier = settingKey[BuildTargetIdentifier]("Build target identifier of a project and configuration.").withRank(DSetting)
val bspWorkspace = settingKey[Map[BuildTargetIdentifier, Scope]]("Mapping of BSP build targets to sbt scopes").withRank(DSetting) val bspWorkspace = settingKey[Map[BuildTargetIdentifier, Scope]]("Mapping of BSP build targets to sbt scopes").withRank(DSetting)
private[sbt] val bspFullWorkspace = settingKey[BspFullWorkspace]("Mapping of BSP build targets to sbt scopes and meta-targets for the SBT build itself").withRank(DSetting)
val bspInternalDependencyConfigurations = settingKey[Seq[(ProjectRef, Set[ConfigKey])]]("The project configurations that this configuration depends on, possibly transitivly").withRank(DSetting) val bspInternalDependencyConfigurations = settingKey[Seq[(ProjectRef, Set[ConfigKey])]]("The project configurations that this configuration depends on, possibly transitivly").withRank(DSetting)
val bspWorkspaceBuildTargets = taskKey[Seq[BuildTarget]]("List all the BSP build targets").withRank(DTask) val bspWorkspaceBuildTargets = taskKey[Seq[BuildTarget]]("List all the BSP build targets").withRank(DTask)
val bspBuildTarget = taskKey[BuildTarget]("Description of the BSP build targets").withRank(DTask) val bspBuildTarget = taskKey[BuildTarget]("Description of the BSP build targets").withRank(DTask)

View File

@ -1164,12 +1164,20 @@ private[sbt] object Load {
val prod = (Configurations.Runtime / exportedProducts).value val prod = (Configurations.Runtime / exportedProducts).value
val cp = (Configurations.Runtime / fullClasspath).value val cp = (Configurations.Runtime / fullClasspath).value
val opts = (Configurations.Compile / scalacOptions).value val opts = (Configurations.Compile / scalacOptions).value
val unmanagedSrcDirs = (Configurations.Compile / unmanagedSourceDirectories).value
val unmanagedSrcs = (Configurations.Compile / unmanagedSources).value
val managedSrcDirs = (Configurations.Compile / managedSourceDirectories).value
val managedSrcs = (Configurations.Compile / managedSources).value
PluginData( PluginData(
removeEntries(cp, prod), removeEntries(cp, prod),
prod, prod,
Some(fullResolvers.value.toVector), Some(fullResolvers.value.toVector),
Some(update.value), Some(update.value),
opts opts,
unmanagedSrcDirs,
unmanagedSrcs,
managedSrcDirs,
managedSrcs,
) )
}, },
scalacOptions += "-Wconf:cat=unused-nowarn:s", scalacOptions += "-Wconf:cat=unused-nowarn:s",
@ -1225,7 +1233,7 @@ private[sbt] object Load {
loadPluginDefinition( loadPluginDefinition(
dir, dir,
config, config,
PluginData(config.globalPluginClasspath, Nil, None, None, Nil) PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
) )
def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins = def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins =
@ -1417,6 +1425,10 @@ final case class LoadBuildConfiguration(
data.internalClasspath, data.internalClasspath,
Some(data.resolvers), Some(data.resolvers),
Some(data.updateReport), Some(data.updateReport),
Nil,
Nil,
Nil,
Nil,
Nil Nil
) )
case None => PluginData(globalPluginClasspath) case None => PluginData(globalPluginClasspath)

View File

@ -10,11 +10,13 @@ package internal
package server package server
import java.net.URI import java.net.URI
import sbt.BuildPaths.{ configurationSources, projectStandard }
import sbt.BuildSyntax._ import sbt.BuildSyntax._
import sbt.Def._ import sbt.Def._
import sbt.Keys._ import sbt.Keys._
import sbt.Project._ import sbt.Project._
import sbt.ScopeFilter.Make._ import sbt.ScopeFilter.Make._
import sbt.Scoped.richTaskSeq
import sbt.SlashSyntax0._ import sbt.SlashSyntax0._
import sbt.StandardMain.exchange import sbt.StandardMain.exchange
import sbt.internal.bsp._ import sbt.internal.bsp._
@ -22,13 +24,17 @@ import sbt.internal.langserver.ErrorCodes
import sbt.internal.protocol.JsonRpcRequestMessage import sbt.internal.protocol.JsonRpcRequestMessage
import sbt.internal.util.Attributed import sbt.internal.util.Attributed
import sbt.internal.util.complete.{ Parser, Parsers } import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.librarymanagement.Configuration import sbt.librarymanagement.CrossVersion.binaryScalaVersion
import sbt.librarymanagement.{ Configuration, ScalaArtifacts }
import sbt.std.TaskExtra import sbt.std.TaskExtra
import sbt.util.Logger import sbt.util.Logger
import sjsonnew.shaded.scalajson.ast.unsafe.{ JNull, JValue } import sjsonnew.shaded.scalajson.ast.unsafe.{ JNull, JValue }
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser => JsonParser } import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser => JsonParser }
import xsbti.CompileFailed import xsbti.CompileFailed
import java.io.File
import scala.collection.mutable
// import scala.annotation.nowarn // import scala.annotation.nowarn
import scala.util.control.NonFatal import scala.util.control.NonFatal
import scala.util.{ Failure, Success, Try } import scala.util.{ Failure, Success, Try }
@ -93,36 +99,66 @@ object BuildServerProtocol {
} }
}, },
bspEnabled := true, bspEnabled := true,
bspWorkspace := bspWorkspaceSetting.value, bspSbtEnabled := true,
bspFullWorkspace := bspFullWorkspaceSetting.value,
bspWorkspace := bspFullWorkspace.value.scopes,
bspWorkspaceBuildTargets := Def.taskDyn { bspWorkspaceBuildTargets := Def.taskDyn {
val workspace = Keys.bspWorkspace.value val workspace = Keys.bspFullWorkspace.value
val state = Keys.state.value val state = Keys.state.value
val allTargets = ScopeFilter.in(workspace.values.toSeq) val allTargets = ScopeFilter.in(workspace.scopes.values.toSeq)
val sbtTargets: List[Def.Initialize[Task[BuildTarget]]] = workspace.builds.map {
case (buildTargetIdentifier, loadedBuildUnit) =>
val buildFor = workspace.buildToScope.getOrElse(buildTargetIdentifier, Nil)
sbtBuildTarget(loadedBuildUnit, buildTargetIdentifier, buildFor)
}.toList
Def.task { Def.task {
val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector
state.respondEvent(WorkspaceBuildTargetsResult(buildTargets)) val allBuildTargets = buildTargets ++ sbtTargets.join.value
buildTargets state.respondEvent(WorkspaceBuildTargetsResult(allBuildTargets))
allBuildTargets
} }
}.value, }.value,
// https://github.com/build-server-protocol/build-server-protocol/blob/master/docs/specification.md#build-target-sources-request // https://github.com/build-server-protocol/build-server-protocol/blob/master/docs/specification.md#build-target-sources-request
bspBuildTargetSources := Def.inputTaskDyn { bspBuildTargetSources := Def.inputTaskDyn {
val s = state.value val s = state.value
val workspace = bspWorkspace.value
val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri)))
val filter = ScopeFilter.in(targets.map(workspace)) val workspace = bspFullWorkspace.value.filter(targets)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
// run the worker task concurrently // run the worker task concurrently
Def.task { Def.task {
val items = bspBuildTargetSourcesItem.all(filter).value val items = bspBuildTargetSourcesItem.all(filter).value
val result = SourcesResult(items.toVector) val buildItems = workspace.builds.toVector.map {
case (id, loadedBuildUnit) =>
val base = loadedBuildUnit.localBase
val sbtFiles = configurationSources(base)
val pluginData = loadedBuildUnit.unit.plugins.pluginData
val all = Vector.newBuilder[SourceItem]
def add(fs: Seq[File], sourceItemKind: Int, generated: Boolean): Unit = {
fs.foreach(f => all += (SourceItem(f.toURI, sourceItemKind, generated = generated)))
}
all += (SourceItem(
loadedBuildUnit.unit.plugins.base.toURI,
SourceItemKind.Directory,
generated = false
))
add(pluginData.unmanagedSourceDirectories, SourceItemKind.Directory, generated = false)
add(pluginData.unmanagedSources, SourceItemKind.File, generated = false)
add(pluginData.managedSourceDirectories, SourceItemKind.Directory, generated = true)
add(pluginData.managedSources, SourceItemKind.File, generated = true)
add(sbtFiles, SourceItemKind.File, generated = false)
SourcesItem(id, all.result())
}
val result = SourcesResult((items ++ buildItems).toVector)
s.respondEvent(result) s.respondEvent(result)
} }
}.evaluated, }.evaluated,
bspBuildTargetSources / aggregate := false, bspBuildTargetSources / aggregate := false,
bspBuildTargetResources := Def.inputTaskDyn { bspBuildTargetResources := Def.inputTaskDyn {
val s = state.value val s = state.value
val workspace = bspWorkspace.value
val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri)))
val filter = ScopeFilter.in(targets.map(workspace)) val workspace = bspFullWorkspace.value.filter(targets)
workspace.warnIfBuildsNonEmpty(Method.Resources, s.log)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
// run the worker task concurrently // run the worker task concurrently
Def.task { Def.task {
val items = bspBuildTargetResourcesItem.all(filter).value val items = bspBuildTargetResourcesItem.all(filter).value
@ -133,9 +169,9 @@ object BuildServerProtocol {
bspBuildTargetResources / aggregate := false, bspBuildTargetResources / aggregate := false,
bspBuildTargetDependencySources := Def.inputTaskDyn { bspBuildTargetDependencySources := Def.inputTaskDyn {
val s = state.value val s = state.value
val workspace = bspWorkspace.value
val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri)))
val filter = ScopeFilter.in(targets.map(workspace)) val workspace = bspFullWorkspace.value.filter(targets)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
// run the worker task concurrently // run the worker task concurrently
Def.task { Def.task {
import sbt.internal.bsp.codec.JsonProtocol._ import sbt.internal.bsp.codec.JsonProtocol._
@ -147,9 +183,10 @@ object BuildServerProtocol {
bspBuildTargetDependencySources / aggregate := false, bspBuildTargetDependencySources / aggregate := false,
bspBuildTargetCompile := Def.inputTaskDyn { bspBuildTargetCompile := Def.inputTaskDyn {
val s: State = state.value val s: State = state.value
val workspace = bspWorkspace.value
val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri)))
val filter = ScopeFilter.in(targets.map(workspace)) val workspace = bspFullWorkspace.value.filter(targets)
workspace.warnIfBuildsNonEmpty(Method.Compile, s.log)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
Def.task { Def.task {
val statusCode = Keys.bspBuildTargetCompileItem.all(filter).value.max val statusCode = Keys.bspBuildTargetCompileItem.all(filter).value.max
s.respondEvent(BspCompileResult(None, statusCode)) s.respondEvent(BspCompileResult(None, statusCode))
@ -160,21 +197,40 @@ object BuildServerProtocol {
bspBuildTargetTest / aggregate := false, bspBuildTargetTest / aggregate := false,
bspBuildTargetScalacOptions := Def.inputTaskDyn { bspBuildTargetScalacOptions := Def.inputTaskDyn {
val s = state.value val s = state.value
val workspace = bspWorkspace.value
val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri)))
val filter = ScopeFilter.in(targets.map(workspace)) val workspace = bspFullWorkspace.value.filter(targets)
val builds = workspace.builds
val filter = ScopeFilter.in(workspace.scopes.values.toList)
Def.task { Def.task {
val items = bspBuildTargetScalacOptionsItem.all(filter).value val items = bspBuildTargetScalacOptionsItem.all(filter).value
val result = ScalacOptionsResult(items.toVector) val appProvider = appConfiguration.value.provider()
val sbtJars = appProvider.mainClasspath()
val buildItems = builds.map {
build =>
val plugins: LoadedPlugins = build._2.unit.plugins
val scalacOptions = plugins.pluginData.scalacOptions
val pluginClassPath = plugins.classpath
val classpath = (pluginClassPath ++ sbtJars).map(_.toURI).toVector
ScalacOptionsItem(
build._1,
scalacOptions.toVector,
classpath,
new File(build._2.localBase, "project/target").toURI
)
}
val result = ScalacOptionsResult((items ++ buildItems).toVector)
s.respondEvent(result) s.respondEvent(result)
} }
}.evaluated, }.evaluated,
bspBuildTargetScalacOptions / aggregate := false, bspBuildTargetScalacOptions / aggregate := false,
bspScalaTestClasses := Def.inputTaskDyn { bspScalaTestClasses := Def.inputTaskDyn {
val s = state.value val s = state.value
val workspace = bspWorkspace.value
val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri)))
val filter = ScopeFilter.in(targets.map(workspace)) val workspace = bspFullWorkspace.value.filter(targets)
workspace.warnIfBuildsNonEmpty(Method.ScalaTestClasses, s.log)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
Def.task { Def.task {
val items = bspScalaTestClassesItem.all(filter).value val items = bspScalaTestClassesItem.all(filter).value
val result = ScalaTestClassesResult(items.toVector, None) val result = ScalaTestClassesResult(items.toVector, None)
@ -183,9 +239,10 @@ object BuildServerProtocol {
}.evaluated, }.evaluated,
bspScalaMainClasses := Def.inputTaskDyn { bspScalaMainClasses := Def.inputTaskDyn {
val s = state.value val s = state.value
val workspace = bspWorkspace.value
val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri)))
val filter = ScopeFilter.in(targets.map(workspace)) val workspace = bspFullWorkspace.value.filter(targets)
workspace.warnIfBuildsNonEmpty(Method.ScalaMainClasses, s.log)
val filter = ScopeFilter.in(workspace.scopes.values.toList)
Def.task { Def.task {
val items = bspScalaMainClassesItem.all(filter).value val items = bspScalaMainClassesItem.all(filter).value
val result = ScalaMainClassesResult(items.toVector, None) val result = ScalaMainClassesResult(items.toVector, None)
@ -247,10 +304,27 @@ object BuildServerProtocol {
} }
} }
) )
private object Method {
final val Initialize = "build/initialize"
final val BuildTargets = "workspace/buildTargets"
final val Reload = "workspace/reload"
final val Shutdown = "build/shutdown"
final val Sources = "buildTarget/sources"
final val Resources = "buildTarget/resources"
final val DependencySources = "buildTarget/dependencySources"
final val Compile = "buildTarget/compile"
final val Test = "buildTarget/test"
final val Run = "buildTarget/run"
final val ScalacOptions = "buildTarget/scalacOptions"
final val ScalaTestClasses = "buildTarget/scalaTestClasses"
final val ScalaMainClasses = "buildTarget/scalaMainClasses"
final val Exit = "build/exit"
}
identity(Method) // silence spurious "private object Method in object BuildServerProtocol is never used" warning!
def handler( def handler(
loadedBuild: LoadedBuild, loadedBuild: LoadedBuild,
workspace: Map[BuildTargetIdentifier, Scope], workspace: BspFullWorkspace,
sbtVersion: String, sbtVersion: String,
semanticdbEnabled: Boolean, semanticdbEnabled: Boolean,
semanticdbVersion: String semanticdbVersion: String
@ -264,7 +338,7 @@ object BuildServerProtocol {
ServerHandler { callback => ServerHandler { callback =>
ServerIntent( ServerIntent(
onRequest = { onRequest = {
case r if r.method == "build/initialize" => case r if r.method == Method.Initialize =>
val params = Converter.fromJson[InitializeBuildParams](json(r)).get val params = Converter.fromJson[InitializeBuildParams](json(r)).get
checkMetalsCompatibility(semanticdbEnabled, semanticdbVersion, params, callback.log) checkMetalsCompatibility(semanticdbEnabled, semanticdbVersion, params, callback.log)
@ -277,42 +351,42 @@ object BuildServerProtocol {
) )
callback.jsonRpcRespond(response, Some(r.id)); () callback.jsonRpcRespond(response, Some(r.id)); ()
case r if r.method == "workspace/buildTargets" => case r if r.method == Method.BuildTargets =>
val _ = callback.appendExec(Keys.bspWorkspaceBuildTargets.key.toString, Some(r.id)) val _ = callback.appendExec(Keys.bspWorkspaceBuildTargets.key.toString, Some(r.id))
case r if r.method == "workspace/reload" => case r if r.method == Method.Reload =>
val _ = callback.appendExec(s"$bspReload ${r.id}", Some(r.id)) val _ = callback.appendExec(s"$bspReload ${r.id}", Some(r.id))
case r if r.method == "build/shutdown" => case r if r.method == Method.Shutdown =>
callback.jsonRpcRespond(JNull, Some(r.id)) callback.jsonRpcRespond(JNull, Some(r.id))
case r if r.method == "buildTarget/sources" => case r if r.method == Method.Sources =>
val param = Converter.fromJson[SourcesParams](json(r)).get val param = Converter.fromJson[SourcesParams](json(r)).get
val targets = param.targets.map(_.uri).mkString(" ") val targets = param.targets.map(_.uri).mkString(" ")
val command = Keys.bspBuildTargetSources.key val command = Keys.bspBuildTargetSources.key
val _ = callback.appendExec(s"$command $targets", Some(r.id)) val _ = callback.appendExec(s"$command $targets", Some(r.id))
case r if r.method == "buildTarget/dependencySources" => case r if r.method == Method.DependencySources =>
val param = Converter.fromJson[DependencySourcesParams](json(r)).get val param = Converter.fromJson[DependencySourcesParams](json(r)).get
val targets = param.targets.map(_.uri).mkString(" ") val targets = param.targets.map(_.uri).mkString(" ")
val command = Keys.bspBuildTargetDependencySources.key val command = Keys.bspBuildTargetDependencySources.key
val _ = callback.appendExec(s"$command $targets", Some(r.id)) val _ = callback.appendExec(s"$command $targets", Some(r.id))
case r if r.method == "buildTarget/compile" => case r if r.method == Method.Compile =>
val param = Converter.fromJson[CompileParams](json(r)).get val param = Converter.fromJson[CompileParams](json(r)).get
val targets = param.targets.map(_.uri).mkString(" ") val targets = param.targets.map(_.uri).mkString(" ")
val command = Keys.bspBuildTargetCompile.key val command = Keys.bspBuildTargetCompile.key
val _ = callback.appendExec(s"$command $targets", Some(r.id)) val _ = callback.appendExec(s"$command $targets", Some(r.id))
case r: JsonRpcRequestMessage if r.method == "buildTarget/test" => case r: JsonRpcRequestMessage if r.method == Method.Test =>
val task = bspBuildTargetTest.key val task = bspBuildTargetTest.key
val paramStr = CompactPrinter(json(r)) val paramStr = CompactPrinter(json(r))
val _ = callback.appendExec(s"$task $paramStr", Some(r.id)) val _ = callback.appendExec(s"$task $paramStr", Some(r.id))
case r if r.method == "buildTarget/run" => case r if r.method == Method.Run =>
val paramJson = json(r) val paramJson = json(r)
val param = Converter.fromJson[RunParams](json(r)).get val param = Converter.fromJson[RunParams](json(r)).get
val scope = workspace.getOrElse( val scope = workspace.scopes.getOrElse(
param.target, param.target,
throw LangServerError( throw LangServerError(
ErrorCodes.InvalidParams, ErrorCodes.InvalidParams,
@ -328,25 +402,25 @@ object BuildServerProtocol {
Some(r.id) Some(r.id)
) )
case r if r.method == "buildTarget/scalacOptions" => case r if r.method == Method.ScalacOptions =>
val param = Converter.fromJson[ScalacOptionsParams](json(r)).get val param = Converter.fromJson[ScalacOptionsParams](json(r)).get
val targets = param.targets.map(_.uri).mkString(" ") val targets = param.targets.map(_.uri).mkString(" ")
val command = Keys.bspBuildTargetScalacOptions.key val command = Keys.bspBuildTargetScalacOptions.key
val _ = callback.appendExec(s"$command $targets", Some(r.id)) val _ = callback.appendExec(s"$command $targets", Some(r.id))
case r if r.method == "buildTarget/scalaTestClasses" => case r if r.method == Method.ScalaTestClasses =>
val param = Converter.fromJson[ScalaTestClassesParams](json(r)).get val param = Converter.fromJson[ScalaTestClassesParams](json(r)).get
val targets = param.targets.map(_.uri).mkString(" ") val targets = param.targets.map(_.uri).mkString(" ")
val command = Keys.bspScalaTestClasses.key val command = Keys.bspScalaTestClasses.key
val _ = callback.appendExec(s"$command $targets", Some(r.id)) val _ = callback.appendExec(s"$command $targets", Some(r.id))
case r if r.method == "buildTarget/scalaMainClasses" => case r if r.method == Method.ScalaMainClasses =>
val param = Converter.fromJson[ScalaMainClassesParams](json(r)).get val param = Converter.fromJson[ScalaMainClassesParams](json(r)).get
val targets = param.targets.map(_.uri).mkString(" ") val targets = param.targets.map(_.uri).mkString(" ")
val command = Keys.bspScalaMainClasses.key val command = Keys.bspScalaMainClasses.key
val _ = callback.appendExec(s"$command $targets", Some(r.id)) val _ = callback.appendExec(s"$command $targets", Some(r.id))
case r if r.method == "buildTarget/resources" => case r if r.method == Method.Resources =>
val param = Converter.fromJson[ResourcesParams](json(r)).get val param = Converter.fromJson[ResourcesParams](json(r)).get
val targets = param.targets.map(_.uri).mkString(" ") val targets = param.targets.map(_.uri).mkString(" ")
val command = Keys.bspBuildTargetResources.key val command = Keys.bspBuildTargetResources.key
@ -354,7 +428,7 @@ object BuildServerProtocol {
}, },
onResponse = PartialFunction.empty, onResponse = PartialFunction.empty,
onNotification = { onNotification = {
case r if r.method == "build/exit" => case r if r.method == Method.Exit =>
val _ = callback.appendExec(BasicCommandStrings.TerminateAction, None) val _ = callback.appendExec(BasicCommandStrings.TerminateAction, None)
}, },
) )
@ -403,7 +477,7 @@ object BuildServerProtocol {
) )
@nowarn @nowarn
private def bspWorkspaceSetting: Def.Initialize[Map[BuildTargetIdentifier, Scope]] = private def bspFullWorkspaceSetting: Def.Initialize[BspFullWorkspace] =
Def.settingDyn { Def.settingDyn {
val loadedBuild = Keys.loadedBuild.value val loadedBuild = Keys.loadedBuild.value
@ -423,11 +497,32 @@ object BuildServerProtocol {
.map(_ / Keys.bspEnabled) .map(_ / Keys.bspEnabled)
.join .join
.value .value
val result = for { val buildsMap =
mutable.HashMap[BuildTargetIdentifier, mutable.ListBuffer[BuildTargetIdentifier]]()
val scopeMap = for {
(targetId, scope, bspEnabled) <- (targetIds, scopes, bspEnabled).zipped (targetId, scope, bspEnabled) <- (targetIds, scopes, bspEnabled).zipped
if bspEnabled if bspEnabled
} yield targetId -> scope } yield {
result.toMap scope.project.toOption match {
case Some(ProjectRef(buildUri, _)) =>
val loadedBuildUnit = loadedBuild.units(buildUri)
buildsMap.getOrElseUpdate(
toSbtTargetId(loadedBuildUnit),
new mutable.ListBuffer
) += targetId
}
targetId -> scope
}
val buildMap = if (bspSbtEnabled.value) {
for (loadedBuildUnit <- loadedBuild.units.values) yield {
val rootProjectId = loadedBuildUnit.root
toSbtTargetId(loadedBuildUnit) -> loadedBuildUnit
}
} else {
Nil
}
BspFullWorkspace(scopeMap.toMap, buildMap.toMap, buildsMap.mapValues(_.result()).toMap)
} }
} }
@ -469,6 +564,43 @@ object BuildServerProtocol {
} }
} }
private def sbtBuildTarget(
loadedUnit: LoadedBuildUnit,
buildTargetIdentifier: BuildTargetIdentifier,
buildFor: Seq[BuildTargetIdentifier]
): Def.Initialize[Task[BuildTarget]] = Def.task {
val scalaProvider = appConfiguration.value.provider().scalaProvider()
appConfiguration.value.provider().mainClasspath()
val scalaJars = scalaProvider.jars()
val compileData = ScalaBuildTarget(
scalaOrganization = ScalaArtifacts.Organization,
scalaVersion = scalaProvider.version(),
scalaBinaryVersion = binaryScalaVersion(scalaProvider.version()),
platform = ScalaPlatform.JVM,
jars = scalaJars.toVector.map(_.toURI.toString)
)
val sbtVersionValue = sbtVersion.value
val sbtData = SbtBuildTarget(
sbtVersionValue,
loadedUnit.imports.toVector,
compileData,
None,
buildFor.toVector
)
BuildTarget(
buildTargetIdentifier,
toSbtTargetIdName(loadedUnit),
projectStandard(loadedUnit.unit.localBase).toURI,
Vector(),
BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false),
BuildServerConnection.languages,
Vector(),
"sbt",
data = Converter.toJsonUnsafe(sbtData),
)
}
private def scalacOptionsTask: Def.Initialize[Task[ScalacOptionsItem]] = Def.taskDyn { private def scalacOptionsTask: Def.Initialize[Task[ScalacOptionsItem]] = Def.taskDyn {
val target = Keys.bspTargetIdentifier.value val target = Keys.bspTargetIdentifier.value
val scalacOptions = Keys.scalacOptions.value val scalacOptions = Keys.scalacOptions.value
@ -571,7 +703,7 @@ object BuildServerProtocol {
.map(_.flatMap(json => Converter.fromJson[TestParams](json))) .map(_.flatMap(json => Converter.fromJson[TestParams](json)))
.parsed .parsed
.get .get
val workspace = bspWorkspace.value val workspace = bspFullWorkspace.value
val resultTask: Def.Initialize[Task[Result[Seq[Unit]]]] = testParams.dataKind match { val resultTask: Def.Initialize[Task[Result[Seq[Unit]]]] = testParams.dataKind match {
case Some("scala-test") => case Some("scala-test") =>
@ -582,7 +714,7 @@ object BuildServerProtocol {
case Success(value) => value.testClasses case Success(value) => value.testClasses
} }
val testTasks: Seq[Def.Initialize[Task[Unit]]] = items.map { item => val testTasks: Seq[Def.Initialize[Task[Unit]]] = items.map { item =>
val scope = workspace(item.target) val scope = workspace.scopes(item.target)
item.classes.toList match { item.classes.toList match {
case Nil => Def.task(()) case Nil => Def.task(())
case classes => case classes =>
@ -599,7 +731,7 @@ object BuildServerProtocol {
case None => case None =>
// run allTests in testParams.targets // run allTests in testParams.targets
val filter = ScopeFilter.in(testParams.targets.map(workspace)) val filter = ScopeFilter.in(testParams.targets.map(workspace.scopes))
test.all(filter).result test.all(filter).result
} }
@ -644,7 +776,7 @@ object BuildServerProtocol {
@nowarn @nowarn
private def internalDependencyConfigurationsSetting = Def.settingDyn { private def internalDependencyConfigurationsSetting = Def.settingDyn {
val allScopes = bspWorkspace.value.map { case (_, scope) => scope }.toSet val allScopes = bspFullWorkspace.value.scopes.map { case (_, scope) => scope }.toSet
val directDependencies = Keys.internalDependencyConfigurations.value val directDependencies = Keys.internalDependencyConfigurations.value
.map { .map {
case (project, rawConfigs) => case (project, rawConfigs) =>
@ -705,6 +837,20 @@ object BuildServerProtocol {
) )
} }
// naming convention still seems like the only reliable way to get IntelliJ to import this correctly
// https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84
private def toSbtTargetIdName(ref: LoadedBuildUnit): String = {
ref.root + "-build"
}
private def toSbtTargetId(ref: LoadedBuildUnit): BuildTargetIdentifier = {
val name = toSbtTargetIdName(ref)
val build = ref.unit.uri
val sanitized = build.toString.indexOf("#") match {
case i if i > 0 => build.toString.take(i)
case _ => build.toString
}
BuildTargetIdentifier(new URI(sanitized + "#" + name))
}
private def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier = private def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier =
ref match { ref match {
case ProjectRef(build, project) => case ProjectRef(build, project) =>
@ -733,4 +879,23 @@ object BuildServerProtocol {
} }
} }
} }
/** The regular targets for each scope and meta-targets for the SBT build. */
private[sbt] final case class BspFullWorkspace(
scopes: Map[BuildTargetIdentifier, Scope],
builds: Map[BuildTargetIdentifier, LoadedBuildUnit],
buildToScope: Map[BuildTargetIdentifier, Seq[BuildTargetIdentifier]]
) {
def filter(targets: Seq[BuildTargetIdentifier]): BspFullWorkspace = {
val set = targets.toSet
def filterMap[T](map: Map[BuildTargetIdentifier, T]) = map.filter(x => set.contains(x._1))
BspFullWorkspace(filterMap(scopes), filterMap(builds), buildToScope)
}
def warnIfBuildsNonEmpty(method: String, log: Logger): Unit = {
if (builds.nonEmpty)
log.warn(
s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}"
)
}
}
} }

View File

@ -114,7 +114,7 @@ object FakeState {
Nil Nil
) )
val pluginData = PluginData(Nil, Nil, None, None, Nil) val pluginData = PluginData(Nil, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
val builds: DetectedModules[BuildDef] = new DetectedModules[BuildDef](Nil) val builds: DetectedModules[BuildDef] = new DetectedModules[BuildDef](Nil)
val detectedAutoPlugins: Seq[DetectedAutoPlugin] = val detectedAutoPlugins: Seq[DetectedAutoPlugin] =

View File

@ -7,10 +7,18 @@
package testpkg package testpkg
import sbt.internal.bsp.SourcesResult
import java.io.File
import sbt.internal.bsp.WorkspaceBuildTargetsResult
import scala.concurrent.duration._ import scala.concurrent.duration._
// starts svr using server-test/buildserver and perform custom server tests // starts svr using server-test/buildserver and perform custom server tests
object BuildServerTest extends AbstractServerTest { object BuildServerTest extends AbstractServerTest {
import sbt.internal.bsp.codec.JsonProtocol._
override val testDirectory: String = "buildserver" override val testDirectory: String = "buildserver"
test("build/initialize") { _ => test("build/initialize") { _ =>
@ -26,12 +34,12 @@ object BuildServerTest extends AbstractServerTest {
"""{ "jsonrpc": "2.0", "id": "16", "method": "workspace/buildTargets", "params": {} }""" """{ "jsonrpc": "2.0", "id": "16", "method": "workspace/buildTargets", "params": {} }"""
) )
assert(processing("workspace/buildTargets")) assert(processing("workspace/buildTargets"))
assert { val result = svr.waitFor[WorkspaceBuildTargetsResult](10.seconds)
svr.waitForString(10.seconds) { s => val utilTarget = result.targets.find(_.displayName.contains("util")).get
(s contains """"id":"16"""") && assert(utilTarget.id.uri.toString.endsWith("#util/Compile"))
(s contains """"displayName":"util"""") val buildServerBuildTarget =
} result.targets.find(_.displayName.contains("buildserver-build")).get
} assert(buildServerBuildTarget.id.uri.toString.endsWith("#buildserver-build"))
} }
test("buildTarget/sources") { _ => test("buildTarget/sources") { _ =>
@ -42,10 +50,33 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin |} }""".stripMargin
) )
assert(processing("buildTarget/sources")) assert(processing("buildTarget/sources"))
assert(svr.waitForString(10.seconds) { s => val s = svr.waitFor[SourcesResult](10.seconds)
(s contains """"id":"24"""") && val sources = s.items.head.sources.map(_.uri)
(s contains "util/src/main/scala") assert(sources.contains(new File(svr.baseDirectory, "util/src/main/scala").toURI))
}) }
test("buildTarget/sources SBT") { _ =>
val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#buildserver-build"
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "25", "method": "buildTarget/sources", "params": {
| "targets": [{ "uri": "$x" }]
|} }""".stripMargin
)
assert(processing("buildTarget/sources"))
val s = svr.waitFor[SourcesResult](10.seconds)
val sources = s.items.head.sources.map(_.uri).sorted
val expectedSources = Vector(
"build.sbt",
"project/",
"project/A.scala",
"project/src/main/java",
"project/src/main/scala-2",
"project/src/main/scala-2.12",
"project/src/main/scala-sbt-1.0",
"project/src/main/scala/",
"project/src/main/scala/B.scala",
"project/target/scala-2.12/sbt-1.0/src_managed/main"
).map(rel => new File(svr.baseDirectory.getAbsoluteFile, rel).toURI).sorted
assert(sources == expectedSources)
} }
test("buildTarget/compile") { _ => test("buildTarget/compile") { _ =>

View File

@ -12,17 +12,18 @@ import java.net.Socket
import java.nio.file.{ Files, Path } import java.nio.file.{ Files, Path }
import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit } import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit }
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import verify._ import verify._
import sbt.{ ForkOptions, OutputStrategy, RunFromSourceMain } import sbt.{ ForkOptions, OutputStrategy, RunFromSourceMain }
import sbt.io.IO import sbt.io.IO
import sbt.io.syntax._ import sbt.io.syntax._
import sbt.protocol.ClientSocket import sbt.protocol.ClientSocket
import sjsonnew.JsonReader
import sjsonnew.support.scalajson.unsafe.{ Converter, Parser }
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.concurrent._ import scala.concurrent._
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.{ Success, Try } import scala.util.{ Failure, Success, Try }
trait AbstractServerTest extends TestSuite[Unit] { trait AbstractServerTest extends TestSuite[Unit] {
private var temp: File = _ private var temp: File = _
@ -293,6 +294,57 @@ case class TestServer(
} }
impl() impl()
} }
final def waitFor[T: JsonReader](duration: FiniteDuration): T = {
val deadline = duration.fromNow
var lastEx: Throwable = null
@tailrec def impl(): T =
lines.poll(deadline.timeLeft.toMillis, TimeUnit.MILLISECONDS) match {
case null =>
if (lastEx != null) throw lastEx
else throw new TimeoutException
case s =>
Parser
.parseFromString(s)
.flatMap(
jvalue =>
Converter.fromJson[T](
jvalue.toStandard
.asInstanceOf[sjsonnew.shaded.scalajson.ast.JObject]
.value("result")
.toUnsafe
)
) match {
case Success(value) =>
value
case Failure(exception) =>
if (deadline.isOverdue) {
val ex = new TimeoutException()
ex.initCause(exception)
throw ex
} else {
lastEx = exception
impl()
}
}
}
impl()
}
final def waitForResponse(duration: FiniteDuration, id: Int): String = {
val deadline = duration.fromNow
@tailrec def impl(): String =
lines.poll(deadline.timeLeft.toMillis, TimeUnit.MILLISECONDS) match {
case null =>
throw new TimeoutException()
case s =>
val s1 = s
val correctId = s1.contains("\"id\":\"" + id + "\"")
if (!correctId && !deadline.isOverdue) impl()
else if (deadline.isOverdue)
throw new TimeoutException()
else s
}
impl()
}
final def neverReceive(duration: FiniteDuration)(f: String => Boolean): Boolean = { final def neverReceive(duration: FiniteDuration)(f: String => Boolean): Boolean = {
val deadline = duration.fromNow val deadline = duration.fromNow