Support the SBT extension in BSP import

This enables code assist in the .sbt and project/*.scala files
in IntelliJ and any other IDEs that implement this extension.
This commit is contained in:
Jason Zaugg 2021-06-17 13:35:33 +10:00
parent e4231ac039
commit 45e8e2f90d
2 changed files with 142 additions and 29 deletions

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.BspWorkspace
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._
@ -399,10 +399,11 @@ 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 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[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 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)
val bspSbtBuildTarget = taskKey[List[BuildTarget]]("Description of the BSP SBT build targets").withRank(DTask)
val bspBuildTargetSources = inputKey[Unit]("").withRank(DTask) val bspBuildTargetSources = inputKey[Unit]("").withRank(DTask)
val bspBuildTargetSourcesItem = taskKey[SourcesItem]("").withRank(DTask) val bspBuildTargetSourcesItem = taskKey[SourcesItem]("").withRank(DTask)
val bspBuildTargetResources = inputKey[Unit]("").withRank(DTask) val bspBuildTargetResources = inputKey[Unit]("").withRank(DTask)

View File

@ -10,6 +10,8 @@ package internal
package server package server
import java.net.URI import java.net.URI
import sbt.BasicCommandStrings.Shutdown
import sbt.BuildPaths.{ configurationSources, projectStandard }
import sbt.BuildSyntax._ import sbt.BuildSyntax._
import sbt.Def._ import sbt.Def._
import sbt.Keys._ import sbt.Keys._
@ -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 }
@ -94,26 +100,39 @@ object BuildServerProtocol {
}, },
bspEnabled := true, bspEnabled := true,
bspWorkspace := bspWorkspaceSetting.value, bspWorkspace := bspWorkspaceSetting.value,
bspSbtBuildTarget := sbtBuildTargetTask.value,
bspWorkspaceBuildTargets := Def.taskDyn { bspWorkspaceBuildTargets := Def.taskDyn {
val workspace = Keys.bspWorkspace.value val workspace = Keys.bspWorkspace.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)
Def.task { Def.task {
val sbtTargets = bspSbtBuildTarget.value
val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector
state.respondEvent(WorkspaceBuildTargetsResult(buildTargets)) val allBuildTargets = buildTargets ++ sbtTargets
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 = bspWorkspace.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 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) s.respondEvent(result)
} }
}.evaluated, }.evaluated,
@ -133,9 +152,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 = bspWorkspace.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 +166,9 @@ 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 = bspWorkspace.value.filter(targets)
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 +179,36 @@ 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 = bspWorkspace.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 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) 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 = bspWorkspace.value.filter(targets)
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 +217,9 @@ 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 = bspWorkspace.value.filter(targets)
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)
@ -250,7 +284,7 @@ object BuildServerProtocol {
def handler( def handler(
loadedBuild: LoadedBuild, loadedBuild: LoadedBuild,
workspace: Map[BuildTargetIdentifier, Scope], workspace: BspWorkspace,
sbtVersion: String, sbtVersion: String,
semanticdbEnabled: Boolean, semanticdbEnabled: Boolean,
semanticdbVersion: String semanticdbVersion: String
@ -312,7 +346,7 @@ object BuildServerProtocol {
case r if r.method == "buildTarget/run" => case r if r.method == "buildTarget/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,
@ -403,7 +437,7 @@ object BuildServerProtocol {
) )
@nowarn @nowarn
private def bspWorkspaceSetting: Def.Initialize[Map[BuildTargetIdentifier, Scope]] = private def bspWorkspaceSetting: Def.Initialize[BspWorkspace] =
Def.settingDyn { Def.settingDyn {
val loadedBuild = Keys.loadedBuild.value val loadedBuild = Keys.loadedBuild.value
@ -423,11 +457,24 @@ 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(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 { 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
@ -582,7 +673,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 +690,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 +735,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 = bspWorkspace.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 +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 = private def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier =
ref match { ref match {
case ProjectRef(build, project) => 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)
}
}
} }