diff --git a/build.sbt b/build.sbt index 8ca6d0ac6..bc81d2bef 100644 --- a/build.sbt +++ b/build.sbt @@ -164,7 +164,10 @@ def mimaSettingsSince(versions: Seq[String]): Seq[Def.Setting[_]] = Def settings exclude[FinalClassProblem]("sbt.internal.*"), exclude[FinalMethodProblem]("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"), ), ) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index f4f96f1f5..27a19aab6 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -429,7 +429,7 @@ object Defaults extends BuildCommon { LanguageServerProtocol.handler(fileConverter.value), BuildServerProtocol.handler( loadedBuild.value, - bspWorkspace.value, + bspFullWorkspace.value, sbtVersion.value, semanticdbEnabled.value, semanticdbVersion.value diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 3557b6b30..0235e31d8 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -143,14 +143,18 @@ final case class PluginData( definitionClasspath: Seq[Attributed[File]], resolvers: Option[Vector[Resolver]], 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 } object 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 { diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 33f4d628d..b271fbc1e 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -10,7 +10,6 @@ package sbt import java.nio.file.{ Path => NioPath } import java.io.File import java.net.URL - import lmcoursier.definitions.{ CacheLogger, ModuleMatchers, Reconciliation } import lmcoursier.{ CoursierConfiguration, FallbackDependency } 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.librarymanagement.{ CompatibilityWarningOptions, IvySbt } import sbt.internal.remotecache.RemoteCacheArtifact +import sbt.internal.server.BuildServerProtocol.BspFullWorkspace import sbt.internal.server.{ BuildServerReporter, ServerHandler } import sbt.internal.util.{ AttributeKey, ProgressState, SourcePosition } import sbt.io._ @@ -398,8 +398,10 @@ object Keys { val bspConfig = taskKey[Unit]("Create or update the BSP connection files").withRank(DSetting) 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 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 bspWorkspaceBuildTargets = taskKey[Seq[BuildTarget]]("List all the BSP build targets").withRank(DTask) val bspBuildTarget = taskKey[BuildTarget]("Description of the BSP build targets").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index ef6e08149..684586f5a 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -1164,12 +1164,20 @@ private[sbt] object Load { val prod = (Configurations.Runtime / exportedProducts).value val cp = (Configurations.Runtime / fullClasspath).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( removeEntries(cp, prod), prod, Some(fullResolvers.value.toVector), Some(update.value), - opts + opts, + unmanagedSrcDirs, + unmanagedSrcs, + managedSrcDirs, + managedSrcs, ) }, scalacOptions += "-Wconf:cat=unused-nowarn:s", @@ -1225,7 +1233,7 @@ private[sbt] object Load { loadPluginDefinition( dir, 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 = @@ -1417,6 +1425,10 @@ final case class LoadBuildConfiguration( data.internalClasspath, Some(data.resolvers), Some(data.updateReport), + Nil, + Nil, + Nil, + Nil, Nil ) case None => PluginData(globalPluginClasspath) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index c8ff79bf7..7036a379b 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -10,11 +10,13 @@ package internal package server import java.net.URI +import sbt.BuildPaths.{ configurationSources, projectStandard } import sbt.BuildSyntax._ import sbt.Def._ import sbt.Keys._ import sbt.Project._ import sbt.ScopeFilter.Make._ +import sbt.Scoped.richTaskSeq import sbt.SlashSyntax0._ import sbt.StandardMain.exchange import sbt.internal.bsp._ @@ -22,13 +24,17 @@ import sbt.internal.langserver.ErrorCodes import sbt.internal.protocol.JsonRpcRequestMessage import sbt.internal.util.Attributed 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.util.Logger import sjsonnew.shaded.scalajson.ast.unsafe.{ JNull, JValue } import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser => JsonParser } import xsbti.CompileFailed +import java.io.File +import scala.collection.mutable + // import scala.annotation.nowarn import scala.util.control.NonFatal import scala.util.{ Failure, Success, Try } @@ -93,36 +99,66 @@ object BuildServerProtocol { } }, bspEnabled := true, - bspWorkspace := bspWorkspaceSetting.value, + bspSbtEnabled := true, + bspFullWorkspace := bspFullWorkspaceSetting.value, + bspWorkspace := bspFullWorkspace.value.scopes, bspWorkspaceBuildTargets := Def.taskDyn { - val workspace = Keys.bspWorkspace.value + val workspace = Keys.bspFullWorkspace.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 { val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector - state.respondEvent(WorkspaceBuildTargetsResult(buildTargets)) - buildTargets + val allBuildTargets = buildTargets ++ sbtTargets.join.value + state.respondEvent(WorkspaceBuildTargetsResult(allBuildTargets)) + allBuildTargets } }.value, // https://github.com/build-server-protocol/build-server-protocol/blob/master/docs/specification.md#build-target-sources-request bspBuildTargetSources := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value 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 Def.task { 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) } }.evaluated, bspBuildTargetSources / aggregate := false, bspBuildTargetResources := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value 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 Def.task { val items = bspBuildTargetResourcesItem.all(filter).value @@ -133,9 +169,9 @@ object BuildServerProtocol { bspBuildTargetResources / aggregate := false, bspBuildTargetDependencySources := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value 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 Def.task { import sbt.internal.bsp.codec.JsonProtocol._ @@ -147,9 +183,10 @@ object BuildServerProtocol { bspBuildTargetDependencySources / aggregate := false, bspBuildTargetCompile := Def.inputTaskDyn { val s: State = state.value - val workspace = bspWorkspace.value 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 { val statusCode = Keys.bspBuildTargetCompileItem.all(filter).value.max s.respondEvent(BspCompileResult(None, statusCode)) @@ -160,21 +197,40 @@ object BuildServerProtocol { bspBuildTargetTest / aggregate := false, bspBuildTargetScalacOptions := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value + 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 { 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) } }.evaluated, bspBuildTargetScalacOptions / aggregate := false, bspScalaTestClasses := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value 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 { val items = bspScalaTestClassesItem.all(filter).value val result = ScalaTestClassesResult(items.toVector, None) @@ -183,9 +239,10 @@ object BuildServerProtocol { }.evaluated, bspScalaMainClasses := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value 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 { val items = bspScalaMainClassesItem.all(filter).value 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( loadedBuild: LoadedBuild, - workspace: Map[BuildTargetIdentifier, Scope], + workspace: BspFullWorkspace, sbtVersion: String, semanticdbEnabled: Boolean, semanticdbVersion: String @@ -264,7 +338,7 @@ object BuildServerProtocol { ServerHandler { callback => ServerIntent( onRequest = { - case r if r.method == "build/initialize" => + case r if r.method == Method.Initialize => val params = Converter.fromJson[InitializeBuildParams](json(r)).get checkMetalsCompatibility(semanticdbEnabled, semanticdbVersion, params, callback.log) @@ -277,42 +351,42 @@ object BuildServerProtocol { ) 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)) - case r if r.method == "workspace/reload" => + case r if r.method == Method.Reload => 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)) - case r if r.method == "buildTarget/sources" => + case r if r.method == Method.Sources => val param = Converter.fromJson[SourcesParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetSources.key 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 targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetDependencySources.key 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 targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetCompile.key 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 paramStr = CompactPrinter(json(r)) 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 param = Converter.fromJson[RunParams](json(r)).get - val scope = workspace.getOrElse( + val scope = workspace.scopes.getOrElse( param.target, throw LangServerError( ErrorCodes.InvalidParams, @@ -328,25 +402,25 @@ object BuildServerProtocol { 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 targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetScalacOptions.key 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 targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspScalaTestClasses.key 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 targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspScalaMainClasses.key 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 targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetResources.key @@ -354,7 +428,7 @@ object BuildServerProtocol { }, onResponse = PartialFunction.empty, onNotification = { - case r if r.method == "build/exit" => + case r if r.method == Method.Exit => val _ = callback.appendExec(BasicCommandStrings.TerminateAction, None) }, ) @@ -403,7 +477,7 @@ object BuildServerProtocol { ) @nowarn - private def bspWorkspaceSetting: Def.Initialize[Map[BuildTargetIdentifier, Scope]] = + private def bspFullWorkspaceSetting: Def.Initialize[BspFullWorkspace] = Def.settingDyn { val loadedBuild = Keys.loadedBuild.value @@ -423,11 +497,32 @@ object BuildServerProtocol { .map(_ / Keys.bspEnabled) .join .value - val result = for { + val buildsMap = + mutable.HashMap[BuildTargetIdentifier, mutable.ListBuffer[BuildTargetIdentifier]]() + + val scopeMap = for { (targetId, scope, bspEnabled) <- (targetIds, scopes, bspEnabled).zipped if bspEnabled - } yield targetId -> scope - result.toMap + } yield { + 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 { val target = Keys.bspTargetIdentifier.value val scalacOptions = Keys.scalacOptions.value @@ -571,7 +703,7 @@ object BuildServerProtocol { .map(_.flatMap(json => Converter.fromJson[TestParams](json))) .parsed .get - val workspace = bspWorkspace.value + val workspace = bspFullWorkspace.value val resultTask: Def.Initialize[Task[Result[Seq[Unit]]]] = testParams.dataKind match { case Some("scala-test") => @@ -582,7 +714,7 @@ object BuildServerProtocol { case Success(value) => value.testClasses } 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 { case Nil => Def.task(()) case classes => @@ -599,7 +731,7 @@ object BuildServerProtocol { case None => // 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 } @@ -644,7 +776,7 @@ object BuildServerProtocol { @nowarn 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 .map { 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 = ref match { 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("[", ",", "]")}" + ) + } + } } diff --git a/main/src/test/scala/PluginCommandTest.scala b/main/src/test/scala/PluginCommandTest.scala index 46ec1c753..b8cf07768 100644 --- a/main/src/test/scala/PluginCommandTest.scala +++ b/main/src/test/scala/PluginCommandTest.scala @@ -114,7 +114,7 @@ object FakeState { 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 detectedAutoPlugins: Seq[DetectedAutoPlugin] = diff --git a/server-test/src/server-test/buildserver/project/A.scala b/server-test/src/server-test/buildserver/project/A.scala new file mode 100644 index 000000000..e69de29bb diff --git a/server-test/src/server-test/buildserver/project/src/main/scala/B.scala b/server-test/src/server-test/buildserver/project/src/main/scala/B.scala new file mode 100644 index 000000000..e69de29bb diff --git a/server-test/src/test/scala/testpkg/BuildServerTest.scala b/server-test/src/test/scala/testpkg/BuildServerTest.scala index 5509cf454..9d4e163a7 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -7,10 +7,18 @@ package testpkg +import sbt.internal.bsp.SourcesResult + +import java.io.File +import sbt.internal.bsp.WorkspaceBuildTargetsResult + import scala.concurrent.duration._ // starts svr using server-test/buildserver and perform custom server tests object BuildServerTest extends AbstractServerTest { + + import sbt.internal.bsp.codec.JsonProtocol._ + override val testDirectory: String = "buildserver" test("build/initialize") { _ => @@ -26,12 +34,12 @@ object BuildServerTest extends AbstractServerTest { """{ "jsonrpc": "2.0", "id": "16", "method": "workspace/buildTargets", "params": {} }""" ) assert(processing("workspace/buildTargets")) - assert { - svr.waitForString(10.seconds) { s => - (s contains """"id":"16"""") && - (s contains """"displayName":"util"""") - } - } + val result = svr.waitFor[WorkspaceBuildTargetsResult](10.seconds) + val utilTarget = result.targets.find(_.displayName.contains("util")).get + assert(utilTarget.id.uri.toString.endsWith("#util/Compile")) + val buildServerBuildTarget = + result.targets.find(_.displayName.contains("buildserver-build")).get + assert(buildServerBuildTarget.id.uri.toString.endsWith("#buildserver-build")) } test("buildTarget/sources") { _ => @@ -42,10 +50,33 @@ object BuildServerTest extends AbstractServerTest { |} }""".stripMargin ) assert(processing("buildTarget/sources")) - assert(svr.waitForString(10.seconds) { s => - (s contains """"id":"24"""") && - (s contains "util/src/main/scala") - }) + val s = svr.waitFor[SourcesResult](10.seconds) + val sources = s.items.head.sources.map(_.uri) + 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") { _ => diff --git a/server-test/src/test/scala/testpkg/TestServer.scala b/server-test/src/test/scala/testpkg/TestServer.scala index b2c6a2f62..dac89ff79 100644 --- a/server-test/src/test/scala/testpkg/TestServer.scala +++ b/server-test/src/test/scala/testpkg/TestServer.scala @@ -12,17 +12,18 @@ import java.net.Socket import java.nio.file.{ Files, Path } import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit } import java.util.concurrent.atomic.AtomicBoolean - import verify._ import sbt.{ ForkOptions, OutputStrategy, RunFromSourceMain } import sbt.io.IO import sbt.io.syntax._ import sbt.protocol.ClientSocket +import sjsonnew.JsonReader +import sjsonnew.support.scalajson.unsafe.{ Converter, Parser } import scala.annotation.tailrec import scala.concurrent._ import scala.concurrent.duration._ -import scala.util.{ Success, Try } +import scala.util.{ Failure, Success, Try } trait AbstractServerTest extends TestSuite[Unit] { private var temp: File = _ @@ -293,6 +294,57 @@ case class TestServer( } 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 = { val deadline = duration.fromNow