diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 33f4d628d..284d24f36 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.BspWorkspace import sbt.internal.server.{ BuildServerReporter, ServerHandler } import sbt.internal.util.{ AttributeKey, ProgressState, SourcePosition } import sbt.io._ @@ -399,10 +399,11 @@ object Keys { val bspConfig = taskKey[Unit]("Create or update the BSP connection files").withRank(DSetting) val bspEnabled = SettingKey[Boolean](BasicKeys.bspEnabled) 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[BspWorkspace]("Mapping of BSP build targets to sbt scopes").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) + val bspSbtBuildTarget = taskKey[List[BuildTarget]]("Description of the BSP SBT build targets").withRank(DTask) val bspBuildTargetSources = inputKey[Unit]("").withRank(DTask) val bspBuildTargetSourcesItem = taskKey[SourcesItem]("").withRank(DTask) val bspBuildTargetResources = inputKey[Unit]("").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index c8ff79bf7..2329c837a 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -10,6 +10,8 @@ package internal package server import java.net.URI +import sbt.BasicCommandStrings.Shutdown +import sbt.BuildPaths.{ configurationSources, projectStandard } import sbt.BuildSyntax._ import sbt.Def._ import sbt.Keys._ @@ -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 } @@ -94,26 +100,39 @@ object BuildServerProtocol { }, bspEnabled := true, bspWorkspace := bspWorkspaceSetting.value, + bspSbtBuildTarget := sbtBuildTargetTask.value, bspWorkspaceBuildTargets := Def.taskDyn { val workspace = Keys.bspWorkspace.value val state = Keys.state.value - val allTargets = ScopeFilter.in(workspace.values.toSeq) + val allTargets = ScopeFilter.in(workspace.scopes.values.toSeq) Def.task { + val sbtTargets = bspSbtBuildTarget.value val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector - state.respondEvent(WorkspaceBuildTargetsResult(buildTargets)) - buildTargets + val allBuildTargets = buildTargets ++ sbtTargets + 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 = bspWorkspace.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 defDir = projectStandard(base) + val sbtFilesItems = + sbtFiles.map(f => SourceItem(f.toURI, SourceItemKind.File, generated = false)) + val defDirItem = SourceItem(defDir.toURI, SourceItemKind.Directory, generated = false) + SourcesItem(id, (defDirItem +: sbtFilesItems).toVector) + } + val result = SourcesResult((items ++ buildItems).toVector) s.respondEvent(result) } }.evaluated, @@ -133,9 +152,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 = bspWorkspace.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 +166,9 @@ 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 = bspWorkspace.value.filter(targets) + 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 +179,36 @@ 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 = bspWorkspace.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 pluginClassPath = build._2.unit.plugins.classpath + val classpath = (pluginClassPath ++ sbtJars).map(_.toURI).toVector + ScalacOptionsItem( + build._1, + Vector(), + 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 = bspWorkspace.value.filter(targets) + val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val items = bspScalaTestClassesItem.all(filter).value val result = ScalaTestClassesResult(items.toVector, None) @@ -183,9 +217,9 @@ 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 = bspWorkspace.value.filter(targets) + val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val items = bspScalaMainClassesItem.all(filter).value val result = ScalaMainClassesResult(items.toVector, None) @@ -250,7 +284,7 @@ object BuildServerProtocol { def handler( loadedBuild: LoadedBuild, - workspace: Map[BuildTargetIdentifier, Scope], + workspace: BspWorkspace, sbtVersion: String, semanticdbEnabled: Boolean, semanticdbVersion: String @@ -312,7 +346,7 @@ object BuildServerProtocol { case r if r.method == "buildTarget/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, @@ -403,7 +437,7 @@ object BuildServerProtocol { ) @nowarn - private def bspWorkspaceSetting: Def.Initialize[Map[BuildTargetIdentifier, Scope]] = + private def bspWorkspaceSetting: Def.Initialize[BspWorkspace] = Def.settingDyn { val loadedBuild = Keys.loadedBuild.value @@ -423,11 +457,24 @@ 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(toId(loadedBuildUnit), new mutable.ListBuffer) += targetId + } + targetId -> scope + } + val buildMap = for (loadedBuildUnit <- loadedBuild.units.values) yield { + toId(loadedBuildUnit) -> loadedBuildUnit + } + BspWorkspace(scopeMap.toMap, buildMap.toMap, buildsMap.mapValues(_.result()).toMap) } } @@ -469,6 +516,50 @@ object BuildServerProtocol { } } + private def sbtBuildTargetTask: Def.Initialize[Task[List[BuildTarget]]] = Def.taskDyn { + val structure = buildStructure.value + val loadedUnits = structure.units.values.toSeq.distinct + + 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 workspace = bspWorkspaceSetting.value + Def.task { + val sbtVersionValue = sbtVersion.value + loadedUnits.toList.map { loadedUnit => + val buildTargetIdentifier = toId(loadedUnit) + val sbtData = SbtBuildTarget( + sbtVersionValue, + loadedUnit.imports.toVector, + compileData, + None, + workspace.buildToScope.getOrElse(buildTargetIdentifier, Nil).toVector + ) + + BuildTarget( + buildTargetIdentifier, + // naming convention still seems like the only 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 + structure.rootProject(loadedUnit.unit.uri) + "-build", + 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 @@ -582,7 +673,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 +690,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 +735,7 @@ object BuildServerProtocol { @nowarn private def internalDependencyConfigurationsSetting = Def.settingDyn { - val allScopes = bspWorkspace.value.map { case (_, scope) => scope }.toSet + val allScopes = bspWorkspace.value.scopes.map { case (_, scope) => scope }.toSet val directDependencies = Keys.internalDependencyConfigurations.value .map { case (project, rawConfigs) => @@ -705,6 +796,15 @@ object BuildServerProtocol { ) } + private val SbtBuildSuffix = "#sbt-build" + private def toId(ref: LoadedBuildUnit): BuildTargetIdentifier = { + 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 + SbtBuildSuffix)) + } private def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier = ref match { case ProjectRef(build, project) => @@ -733,4 +833,16 @@ object BuildServerProtocol { } } } + + final case class BspWorkspace( + scopes: Map[BuildTargetIdentifier, Scope], + builds: Map[BuildTargetIdentifier, LoadedBuildUnit], + buildToScope: Map[BuildTargetIdentifier, Seq[BuildTargetIdentifier]] + ) { + def filter(targets: Seq[BuildTargetIdentifier]): BspWorkspace = { + val set = targets.toSet + def filterMap[T](map: Map[BuildTargetIdentifier, T]) = map.filter(x => set.contains(x._1)) + BspWorkspace(filterMap(scopes), filterMap(builds), buildToScope) + } + } }