diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 58f1e1740..0235e31d8 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -144,7 +144,9 @@ final case class PluginData( resolvers: Option[Vector[Resolver]], report: Option[UpdateReport], scalacOptions: Seq[String], + unmanagedSourceDirectories: Seq[File], unmanagedSources: Seq[File], + managedSourceDirectories: Seq[File], managedSources: Seq[File] ) { val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath @@ -152,7 +154,7 @@ final case class PluginData( object 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 { diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index 6830d6749..684586f5a 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -1164,16 +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 managedSrcs = (Configurations.Compile / managedSources).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, + unmanagedSrcDirs, unmanagedSrcs, - managedSrcs + managedSrcDirs, + managedSrcs, ) }, scalacOptions += "-Wconf:cat=unused-nowarn:s", @@ -1229,7 +1233,7 @@ private[sbt] object Load { loadPluginDefinition( dir, 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 = @@ -1423,6 +1427,8 @@ final case class LoadBuildConfiguration( 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 bbb5c2b65..c400fd9c3 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -132,15 +132,21 @@ object BuildServerProtocol { val base = loadedBuildUnit.localBase val sbtFiles = configurationSources(base) val pluginData = loadedBuildUnit.unit.plugins.pluginData - val unmanagedSources = pluginData.unmanagedSources.map( - f => SourceItem(f.toURI, SourceItemKind.File, generated = false) - ) - val managedSources = pluginData.managedSources.map( - f => SourceItem(f.toURI, SourceItemKind.File, generated = true) - ) - val sbtFilesItems = - sbtFiles.map(f => SourceItem(f.toURI, SourceItemKind.File, generated = false)) - SourcesItem(id, (unmanagedSources ++ managedSources ++ sbtFilesItems).toVector) + 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) @@ -499,13 +505,17 @@ object BuildServerProtocol { scope.project.toOption match { case Some(ProjectRef(buildUri, _)) => val loadedBuildUnit = loadedBuild.units(buildUri) - buildsMap.getOrElseUpdate(toId(loadedBuildUnit), new mutable.ListBuffer) += targetId + buildsMap.getOrElseUpdate( + toSbtTargetId(loadedBuildUnit), + new mutable.ListBuffer + ) += targetId } targetId -> scope } val buildMap = if (bspSbtEnabled.value) { for (loadedBuildUnit <- loadedBuild.units.values) yield { - toId(loadedBuildUnit) -> loadedBuildUnit + val rootProjectId = loadedBuildUnit.root + toSbtTargetId(loadedBuildUnit) -> loadedBuildUnit } } else { Nil @@ -557,7 +567,6 @@ object BuildServerProtocol { buildTargetIdentifier: BuildTargetIdentifier, buildFor: Seq[BuildTargetIdentifier] ): Def.Initialize[Task[BuildTarget]] = Def.task { - val structure = buildStructure.value val scalaProvider = appConfiguration.value.provider().scalaProvider() appConfiguration.value.provider().mainClasspath() val scalaJars = scalaProvider.jars() @@ -579,9 +588,7 @@ object BuildServerProtocol { 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", + toSbtTargetIdName(loadedUnit), projectStandard(loadedUnit.unit.localBase).toURI, Vector(), BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false), @@ -828,14 +835,19 @@ object BuildServerProtocol { ) } - private val SbtBuildSuffix = "#sbt-build" - private def toId(ref: LoadedBuildUnit): BuildTargetIdentifier = { + // 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 + SbtBuildSuffix)) + BuildTargetIdentifier(new URI(sanitized + "#" + name)) } private def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier = ref match { @@ -879,7 +891,9 @@ object BuildServerProtocol { } 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("[", ",", "]")}") + log.warn( + s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}" + ) } } } 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