Refine and test SBT BSP target

- Pass source dirs and current list of files
  - Align display name and URI
This commit is contained in:
Jason Zaugg 2021-06-30 11:19:24 +10:00
parent 0bd736be2a
commit fe046476b1
7 changed files with 140 additions and 35 deletions

View File

@ -144,7 +144,9 @@ final case class PluginData(
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], unmanagedSources: Seq[File],
managedSourceDirectories: Seq[File],
managedSources: Seq[File] managedSources: Seq[File]
) { ) {
val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath
@ -152,7 +154,7 @@ final case class PluginData(
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, Nil, Nil) PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
} }
object EvaluateTask { object EvaluateTask {

View File

@ -1164,16 +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 managedSrcs = (Configurations.Compile / managedSources).value val unmanagedSrcDirs = (Configurations.Compile / unmanagedSourceDirectories).value
val unmanagedSrcs = (Configurations.Compile / unmanagedSources).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, unmanagedSrcs,
managedSrcs managedSrcDirs,
managedSrcs,
) )
}, },
scalacOptions += "-Wconf:cat=unused-nowarn:s", scalacOptions += "-Wconf:cat=unused-nowarn:s",
@ -1229,7 +1233,7 @@ private[sbt] object Load {
loadPluginDefinition( loadPluginDefinition(
dir, dir,
config, config,
PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, 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 =
@ -1423,6 +1427,8 @@ final case class LoadBuildConfiguration(
Some(data.updateReport), Some(data.updateReport),
Nil, Nil,
Nil, Nil,
Nil,
Nil,
Nil Nil
) )
case None => PluginData(globalPluginClasspath) case None => PluginData(globalPluginClasspath)

View File

@ -132,15 +132,21 @@ object BuildServerProtocol {
val base = loadedBuildUnit.localBase val base = loadedBuildUnit.localBase
val sbtFiles = configurationSources(base) val sbtFiles = configurationSources(base)
val pluginData = loadedBuildUnit.unit.plugins.pluginData val pluginData = loadedBuildUnit.unit.plugins.pluginData
val unmanagedSources = pluginData.unmanagedSources.map( val all = Vector.newBuilder[SourceItem]
f => SourceItem(f.toURI, SourceItemKind.File, generated = false) def add(fs: Seq[File], sourceItemKind: Int, generated: Boolean): Unit = {
) fs.foreach(f => all += (SourceItem(f.toURI, sourceItemKind, generated = generated)))
val managedSources = pluginData.managedSources.map( }
f => SourceItem(f.toURI, SourceItemKind.File, generated = true) all += (SourceItem(
) loadedBuildUnit.unit.plugins.base.toURI,
val sbtFilesItems = SourceItemKind.Directory,
sbtFiles.map(f => SourceItem(f.toURI, SourceItemKind.File, generated = false)) generated = false
SourcesItem(id, (unmanagedSources ++ managedSources ++ sbtFilesItems).toVector) ))
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) val result = SourcesResult((items ++ buildItems).toVector)
s.respondEvent(result) s.respondEvent(result)
@ -499,13 +505,17 @@ object BuildServerProtocol {
scope.project.toOption match { scope.project.toOption match {
case Some(ProjectRef(buildUri, _)) => case Some(ProjectRef(buildUri, _)) =>
val loadedBuildUnit = loadedBuild.units(buildUri) val loadedBuildUnit = loadedBuild.units(buildUri)
buildsMap.getOrElseUpdate(toId(loadedBuildUnit), new mutable.ListBuffer) += targetId buildsMap.getOrElseUpdate(
toSbtTargetId(loadedBuildUnit),
new mutable.ListBuffer
) += targetId
} }
targetId -> scope targetId -> scope
} }
val buildMap = if (bspSbtEnabled.value) { val buildMap = if (bspSbtEnabled.value) {
for (loadedBuildUnit <- loadedBuild.units.values) yield { for (loadedBuildUnit <- loadedBuild.units.values) yield {
toId(loadedBuildUnit) -> loadedBuildUnit val rootProjectId = loadedBuildUnit.root
toSbtTargetId(loadedBuildUnit) -> loadedBuildUnit
} }
} else { } else {
Nil Nil
@ -557,7 +567,6 @@ object BuildServerProtocol {
buildTargetIdentifier: BuildTargetIdentifier, buildTargetIdentifier: BuildTargetIdentifier,
buildFor: Seq[BuildTargetIdentifier] buildFor: Seq[BuildTargetIdentifier]
): Def.Initialize[Task[BuildTarget]] = Def.task { ): Def.Initialize[Task[BuildTarget]] = Def.task {
val structure = buildStructure.value
val scalaProvider = appConfiguration.value.provider().scalaProvider() val scalaProvider = appConfiguration.value.provider().scalaProvider()
appConfiguration.value.provider().mainClasspath() appConfiguration.value.provider().mainClasspath()
val scalaJars = scalaProvider.jars() val scalaJars = scalaProvider.jars()
@ -579,9 +588,7 @@ object BuildServerProtocol {
BuildTarget( BuildTarget(
buildTargetIdentifier, buildTargetIdentifier,
// naming convention still seems like the only way to get IntelliJ to import this correctly toSbtTargetIdName(loadedUnit),
// 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, projectStandard(loadedUnit.unit.localBase).toURI,
Vector(), Vector(),
BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false), BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false),
@ -828,14 +835,19 @@ object BuildServerProtocol {
) )
} }
private val SbtBuildSuffix = "#sbt-build" // naming convention still seems like the only reliable way to get IntelliJ to import this correctly
private def toId(ref: LoadedBuildUnit): BuildTargetIdentifier = { // 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 build = ref.unit.uri
val sanitized = build.toString.indexOf("#") match { val sanitized = build.toString.indexOf("#") match {
case i if i > 0 => build.toString.take(i) case i if i > 0 => build.toString.take(i)
case _ => build.toString case _ => build.toString
} }
BuildTargetIdentifier(new URI(sanitized + SbtBuildSuffix)) 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 {
@ -879,7 +891,9 @@ object BuildServerProtocol {
} }
def warnIfBuildsNonEmpty(method: String, log: Logger): Unit = { def warnIfBuildsNonEmpty(method: String, log: Logger): Unit = {
if (builds.nonEmpty) if (builds.nonEmpty)
log.warn(s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}") log.warn(
s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}"
)
} }
} }
} }

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