diff --git a/build.sbt b/build.sbt index 8ca6d0ac6..55a26b6ab 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"), ), ) @@ -678,6 +681,8 @@ lazy val actionsProj = (project in file("main-actions")) exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.filesModifiedBytes"), exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.fileModifiedBytes"), exclude[DirectMissingMethodProblem]("sbt.Doc.$init$"), + // Added field in nested private[this] class + exclude[ReversedMissingMethodProblem]("sbt.compiler.Eval#EvalType.sourceName"), ), ) .configure( diff --git a/launch/src/main/input_resources/sbt/sbt.boot.properties b/launch/src/main/input_resources/sbt/sbt.boot.properties index f13961dcf..2aa284ef8 100644 --- a/launch/src/main/input_resources/sbt/sbt.boot.properties +++ b/launch/src/main/input_resources/sbt/sbt.boot.properties @@ -12,8 +12,6 @@ [repositories] local - local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext] - local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/} maven-central sbt-maven-releases: https://repo.scala-sbt.org/scalasbt/maven-releases/, bootOnly sbt-maven-snapshots: https://repo.scala-sbt.org/scalasbt/maven-snapshots/, bootOnly diff --git a/main-actions/src/main/scala/sbt/compiler/Eval.scala b/main-actions/src/main/scala/sbt/compiler/Eval.scala index 461a8c727..df0386262 100644 --- a/main-actions/src/main/scala/sbt/compiler/Eval.scala +++ b/main-actions/src/main/scala/sbt/compiler/Eval.scala @@ -12,7 +12,7 @@ import scala.collection.mutable.ListBuffer import scala.tools.nsc.{ ast, io, reporters, CompilerCommand, Global, Phase, Settings } import io.{ AbstractFile, PlainFile, VirtualDirectory } import ast.parser.Tokens -import reporters.{ ConsoleReporter, Reporter } +import reporters.Reporter import scala.reflect.internal.util.{ AbstractFileClassLoader, BatchSourceFile } import Tokens.{ EOF, NEWLINE, NEWLINES, SEMI } import java.io.{ File, FileNotFoundException } @@ -65,12 +65,12 @@ final class EvalException(msg: String) extends RuntimeException(msg) final class Eval( optionsNoncp: Seq[String], classpath: Seq[File], - mkReporter: Settings => Reporter, + mkReporter: Settings => EvalReporter, backing: Option[File] ) { - def this(mkReporter: Settings => Reporter, backing: Option[File]) = + def this(mkReporter: Settings => EvalReporter, backing: Option[File]) = this(Nil, IO.classLocationPath[Product].toFile :: Nil, mkReporter, backing) - def this() = this(s => new ConsoleReporter(s), None) + def this() = this(EvalReporter.console, None) backing.foreach(IO.createDirectory) val classpathString = Path.makeString(classpath ++ backing.toList) @@ -81,8 +81,8 @@ final class Eval( new CompilerCommand(options.toList, s) // this side-effects on Settings.. s } - lazy val reporter = mkReporter(settings) - + private lazy val evalReporter = mkReporter(settings) + def reporter: Reporter = evalReporter // kept for binary compatibility /** * Subclass of Global which allows us to mutate currentRun from outside. * See for rationale https://issues.scala-lang.org/browse/SI-8794 @@ -95,7 +95,7 @@ final class Eval( } var curRun: Run = null } - lazy val global: EvalGlobal = new EvalGlobal(settings, reporter) + lazy val global: EvalGlobal = new EvalGlobal(settings, evalReporter) import global._ private[sbt] def unlinkDeferred(): Unit = { @@ -114,6 +114,7 @@ final class Eval( line: Int = DefaultStartLine ): EvalResult = { val ev = new EvalType[String] { + def sourceName: String = srcName def makeUnit = mkUnit(srcName, line, expression) def unlink = true def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { @@ -142,6 +143,7 @@ final class Eval( require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.") val ev = new EvalType[Seq[String]] { lazy val (fullUnit, defUnits) = mkDefsUnit(srcName, definitions) + def sourceName: String = srcName def makeUnit = fullUnit def unlink = false def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { @@ -202,28 +204,19 @@ final class Eval( val hash = Hash.toHex(d) val moduleName = makeModuleName(hash) - lazy val unit = { - reporter.reset - ev.makeUnit - } - lazy val run = new Run { - override def units = (unit :: Nil).iterator - } - def unlinkAll(): Unit = - for ((sym, _) <- run.symSource) if (ev.unlink) unlink(sym) else toUnlinkLater ::= sym - - val (extra, loader) = backing match { - case Some(back) if classExists(back, moduleName) => - val loader = (parent: ClassLoader) => - (new URLClassLoader(Array(back.toURI.toURL), parent): ClassLoader) - val extra = ev.read(cacheFile(back, moduleName)) - (extra, loader) - case _ => - try { - compileAndLoad(run, unit, imports, backing, moduleName, ev) - } finally { - unlinkAll() - } + val (extra, loader) = try { + backing match { + case Some(back) if classExists(back, moduleName) => + val loader = (parent: ClassLoader) => + (new URLClassLoader(Array(back.toURI.toURL), parent): ClassLoader) + val extra = ev.read(cacheFile(back, moduleName)) + (extra, loader) + case _ => + compileAndLoad(imports, backing, moduleName, ev) + } + } finally { + // send a final report even if the class file was backed to reset preceding diagnostics + evalReporter.finalReport(ev.sourceName) } val generatedFiles = getGeneratedFiles(backing, moduleName) @@ -232,6 +225,25 @@ final class Eval( // location of the cached type or definition information private[this] def cacheFile(base: File, moduleName: String): File = new File(base, moduleName + ".cache") + + private def compileAndLoad[T]( + imports: EvalImports, + backing: Option[File], + moduleName: String, + ev: EvalType[T] + ): (T, ClassLoader => ClassLoader) = { + evalReporter.reset() + val unit = ev.makeUnit + val run = new Run { + override def units = (unit :: Nil).iterator + } + try { + compileAndLoad(run, unit, imports, backing, moduleName, ev) + } finally { + // unlink all + for ((sym, _) <- run.symSource) if (ev.unlink) unlink(sym) else toUnlinkLater ::= sym + } + } private[this] def compileAndLoad[T]( run: Run, unit: CompilationUnit, @@ -250,7 +262,7 @@ final class Eval( def compile(phase: Phase): Unit = { globalPhase = phase - if (phase == null || phase == phase.next || reporter.hasErrors) + if (phase == null || phase == phase.next || evalReporter.hasErrors) () else { enteringPhase(phase) { phase.run } @@ -457,6 +469,8 @@ final class Eval( /** Serializes the extra information to a cache file, where it can be `read` back if inputs haven't changed.*/ def write(value: T, file: File): Unit + def sourceName: String + /** * Constructs the full compilation unit for this evaluation. * This is used for error reporting during compilation. @@ -484,7 +498,7 @@ final class Eval( private[this] def mkUnit(srcName: String, firstLine: Int, s: String) = new CompilationUnit(new EvalSourceFile(srcName, firstLine, s)) private[this] def checkError(label: String) = - if (reporter.hasErrors) throw new EvalException(label) + if (evalReporter.hasErrors) throw new EvalException(label) private[this] final class EvalSourceFile(name: String, startLine: Int, contents: String) extends BatchSourceFile(name, contents) { diff --git a/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala b/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala new file mode 100644 index 000000000..7ae284231 --- /dev/null +++ b/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala @@ -0,0 +1,62 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.compiler + +import scala.reflect.internal.settings.MutableSettings +import scala.reflect.internal.util.Position +import scala.tools.nsc.Settings +import scala.tools.nsc.reporters.{ ConsoleReporter, FilteringReporter } + +/** + * Reporter used to compile *.sbt files that forwards compiler diagnostics to BSP clients + */ +abstract class EvalReporter extends FilteringReporter { + + /** + * Send a final report to clear out the outdated diagnostics. + * @param sourceName a *.sbt file + */ + def finalReport(sourceName: String): Unit +} + +object EvalReporter { + def console(s: Settings): EvalReporter = new ForwardingReporter(new ConsoleReporter(s)) +} + +class ForwardingReporter(delegate: FilteringReporter) extends EvalReporter { + def settings: Settings = delegate.settings + + def doReport(pos: Position, msg: String, severity: Severity): Unit = + delegate.doReport(pos, msg, severity) + + override def filter(pos: Position, msg: String, severity: Severity): Int = + delegate.filter(pos, msg, severity) + + override def increment(severity: Severity): Unit = delegate.increment(severity) + + override def errorCount: Int = delegate.errorCount + override def warningCount: Int = delegate.warningCount + + override def hasErrors: Boolean = delegate.hasErrors + override def hasWarnings: Boolean = delegate.hasWarnings + + override def comment(pos: Position, msg: String): Unit = delegate.comment(pos, msg) + + override def cancelled: Boolean = delegate.cancelled + override def cancelled_=(b: Boolean): Unit = delegate.cancelled_=(b) + + override def flush(): Unit = delegate.flush() + override def finish(): Unit = delegate.finish() + override def reset(): Unit = + delegate.reset() // super.reset not necessary, own state is never modified + + override def rerunWithDetails(setting: MutableSettings#Setting, name: String): String = + delegate.rerunWithDetails(setting, name) + + override def finalReport(sourceName: String): Unit = () +} diff --git a/main-actions/src/test/scala/sbt/compiler/EvalTest.scala b/main-actions/src/test/scala/sbt/compiler/EvalTest.scala index 21ffe61b9..4bea445b9 100644 --- a/main-actions/src/test/scala/sbt/compiler/EvalTest.scala +++ b/main-actions/src/test/scala/sbt/compiler/EvalTest.scala @@ -19,7 +19,7 @@ import sbt.io.IO class EvalTest extends Properties("eval") { private[this] lazy val reporter = new StoreReporter(new Settings()) import reporter.ERROR - private[this] lazy val eval = new Eval(_ => reporter, None) + private[this] lazy val eval = new Eval(_ => new ForwardingReporter(reporter), None) property("inferred integer") = forAll { (i: Int) => val result = eval.eval(i.toString) @@ -46,7 +46,7 @@ class EvalTest extends Properties("eval") { property("backed local class") = forAll { (i: Int) => IO.withTemporaryDirectory { dir => - val eval = new Eval(_ => reporter, backing = Some(dir)) + val eval = new Eval(_ => new ForwardingReporter(reporter), backing = Some(dir)) val result = eval.eval(local(i)) val v = value(result).asInstanceOf[{ def i: Int }].i (label("Value", v) |: (v == i)) && diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index d2f6709ec..1c83c4d13 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -330,6 +330,18 @@ class NetworkClient( val cmd = arguments.sbtLaunchJar match { case Some(lj) => + if (log) { + val sbtScript = if (Properties.isWin) "sbt.bat" else "sbt" + console.appendLog(Level.Warn, s"server is started using sbt-launch jar directly") + console.appendLog( + Level.Warn, + "this is not the recommended way: .sbtopts and .jvmopts files are not loaded and SBT_OPTS is ignored" + ) + console.appendLog( + Level.Warn, + s"either upgrade $sbtScript to its latest version or make sure it is accessible from $$PATH, and run 'sbt bspConfig'" + ) + } List("java") ++ arguments.sbtArguments ++ List("-jar", lj, DashDashDetachStdio, DashDashServer) case _ => @@ -1050,7 +1062,8 @@ object NetworkClient { private[client] val noStdErr = "--no-stderr" private[client] val sbtBase = "--sbt-base-directory" private[client] def parseArgs(args: Array[String]): Arguments = { - var sbtScript = if (Properties.isWin) "sbt.bat" else "sbt" + val defaultSbtScript = if (Properties.isWin) "sbt.bat" else "sbt" + var sbtScript = Properties.propOrNone("sbt.script") var launchJar: Option[String] = None var bsp = false val commandArgs = new mutable.ArrayBuffer[String] @@ -1072,11 +1085,10 @@ object NetworkClient { sbtScript = a .split("--sbt-script=") .lastOption - .map(_.replaceAllLiterally("%20", " ")) - .getOrElse(sbtScript) + .orElse(sbtScript) case "--sbt-script" if i + 1 < sanitized.length => i += 1 - sbtScript = sanitized(i).replaceAllLiterally("%20", " ") + sbtScript = Some(sanitized(i)) case a if a.startsWith("--sbt-launch-jar=") => launchJar = a .split("--sbt-launch-jar=") @@ -1096,12 +1108,17 @@ object NetworkClient { } val base = new File("").getCanonicalFile if (!sbtArguments.contains("-Dsbt.io.virtual=true")) sbtArguments += "-Dsbt.io.virtual=true" + if (!sbtArguments.exists(_.startsWith("-Dsbt.script"))) { + sbtScript.foreach { sbtScript => + sbtArguments += s"-Dsbt.script=$sbtScript" + } + } new Arguments( base, sbtArguments.toSeq, commandArgs.toSeq, completionArguments.toSeq, - sbtScript, + sbtScript.getOrElse(defaultSbtScript).replaceAllLiterally("%20", " "), bsp, launchJar ) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index f4f96f1f5..fc70be22b 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -332,13 +332,6 @@ object Defaults extends BuildCommon { turbo :== SysProp.turbo, usePipelining :== SysProp.pipelining, exportPipelining := usePipelining.value, - useScalaReplJLine :== false, - scalaInstanceTopLoader := { - // the JLineLoader contains the SbtInterfaceClassLoader - if (!useScalaReplJLine.value) - classOf[org.jline.terminal.Terminal].getClassLoader // the JLineLoader - else classOf[Compilers].getClassLoader // the SbtInterfaceClassLoader - }, useSuperShell := { if (insideCI.value) false else ITerminal.console.isSupershellEnabled }, superShellThreshold :== SysProp.supershellThreshold, superShellMaxTasks :== SysProp.supershellMaxTasks, @@ -429,7 +422,7 @@ object Defaults extends BuildCommon { LanguageServerProtocol.handler(fileConverter.value), BuildServerProtocol.handler( loadedBuild.value, - bspWorkspace.value, + bspFullWorkspace.value, sbtVersion.value, semanticdbEnabled.value, semanticdbVersion.value @@ -667,6 +660,23 @@ object Defaults extends BuildCommon { // This is included into JvmPlugin.projectSettings def compileBase = inTask(console)(compilersSetting :: Nil) ++ compileBaseGlobal ++ Seq( + useScalaReplJLine :== false, + scalaInstanceTopLoader := { + val topLoader = if (!useScalaReplJLine.value) { + // the JLineLoader contains the SbtInterfaceClassLoader + classOf[org.jline.terminal.Terminal].getClassLoader + } else classOf[Compilers].getClassLoader // the SbtInterfaceClassLoader + + // Scala 2.10 shades jline in the console so we need to make sure that it loads a compatible + // jansi version. Because of the shading, console does not work with the thin client for 2.10.x. + if (scalaVersion.value.startsWith("2.10.")) new ClassLoader(topLoader) { + override protected def loadClass(name: String, resolve: Boolean): Class[_] = { + if (name.startsWith("org.fusesource")) throw new ClassNotFoundException(name) + super.loadClass(name, resolve) + } + } + else topLoader + }, scalaInstance := scalaInstanceTask.value, crossVersion := (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled), pluginCrossBuild / sbtBinaryVersion := binarySbtVersion( @@ -1163,16 +1173,6 @@ object Defaults extends BuildCommon { state: State, topLoader: ClassLoader, ): ScalaInstance = { - // Scala 2.10 shades jline in the console so we need to make sure that it loads a compatible - // jansi version. Because of the shading, console does not work with the thin client for 2.10.x. - val jansiExclusionLoader = if (version.startsWith("2.10.")) new ClassLoader(topLoader) { - override protected def loadClass(name: String, resolve: Boolean): Class[_] = { - if (name.startsWith("org.fusesource")) throw new ClassNotFoundException(name) - super.loadClass(name, resolve) - } - } - else topLoader - val classLoaderCache = state.extendedClassLoaderCache val compilerJars = allCompilerJars.filterNot(libraryJars.contains).distinct.toArray val docJars = allDocJars @@ -1181,7 +1181,7 @@ object Defaults extends BuildCommon { .toArray val allJars = libraryJars ++ compilerJars ++ docJars - val libraryLoader = classLoaderCache(libraryJars.toList, jansiExclusionLoader) + val libraryLoader = classLoaderCache(libraryJars.toList, topLoader) val compilerLoader = classLoaderCache(compilerJars.toList, libraryLoader) val fullLoader = if (docJars.isEmpty) compilerLoader diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 3557b6b30..55da21c94 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -23,6 +23,7 @@ import sbt.librarymanagement.{ Resolver, UpdateReport } import sbt.std.Transform.DummyTaskMap import sbt.util.{ Logger, Show } import sbt.BuildSyntax._ +import sbt.internal.bsp.BuildTargetIdentifier import scala.annotation.nowarn import scala.Console.RED @@ -143,14 +144,19 @@ 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], + buildTarget: Option[BuildTargetIdentifier] ) { 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, None) } 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/Main.scala b/main/src/main/scala/sbt/Main.scala index 45ff32133..f9771b134 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -974,9 +974,6 @@ object BuiltinCommands { st => setupGlobalFileTreeRepository(addCacheStoreFactoryFactory(st)) ) val s4 = s3.put(Keys.useLog4J.key, Project.extract(s3).get(Keys.useLog4J)) - // This is a workaround for the console task in dotty which uses the classloader cache. - // We need to override the top loader in that case so that it gets the forked jline. - s4.extendedClassLoaderCache.setParent(Project.extract(s4).get(Keys.scalaInstanceTopLoader)) addSuperShellParams(CheckBuildSources.init(LintUnused.lintUnusedFunc(s4))) } diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index ef6e08149..08965b0ce 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -8,19 +8,17 @@ package sbt package internal -import java.io.File -import java.net.URI - import sbt.BuildPaths._ import sbt.Def.{ ScopeLocal, ScopedKey, Setting, isDummy } import sbt.Keys._ import sbt.Project.inScope import sbt.Scope.GlobalScope import sbt.SlashSyntax0._ -import sbt.compiler.Eval +import sbt.compiler.{ Eval, EvalReporter } import sbt.internal.BuildStreams._ import sbt.internal.inc.classpath.ClasspathUtil import sbt.internal.inc.{ ScalaInstance, ZincLmUtil, ZincUtil } +import sbt.internal.server.BuildServerEvalReporter import sbt.internal.util.Attributed.data import sbt.internal.util.Types.const import sbt.internal.util.{ Attributed, Settings, ~> } @@ -31,6 +29,8 @@ import sbt.nio.Settings import sbt.util.{ Logger, Show } import xsbti.compile.{ ClasspathOptionsUtil, Compilers } +import java.io.File +import java.net.URI import scala.annotation.{ nowarn, tailrec } import scala.collection.mutable import scala.tools.nsc.reporters.ConsoleReporter @@ -426,14 +426,21 @@ private[sbt] object Load { () => eval } - def mkEval(unit: BuildUnit): Eval = - mkEval(unit.definitions, unit.plugins, unit.plugins.pluginData.scalacOptions) - - def mkEval(defs: LoadedDefinitions, plugs: LoadedPlugins, options: Seq[String]): Eval = - mkEval(defs.target ++ plugs.classpath, defs.base, options) + def mkEval(unit: BuildUnit): Eval = { + val defs = unit.definitions + mkEval(defs.target ++ unit.plugins.classpath, defs.base, unit.plugins.pluginData.scalacOptions) + } def mkEval(classpath: Seq[File], base: File, options: Seq[String]): Eval = - new Eval(options, classpath, s => new ConsoleReporter(s), Some(evalOutputDirectory(base))) + mkEval(classpath, base, options, EvalReporter.console) + + def mkEval( + classpath: Seq[File], + base: File, + options: Seq[String], + mkReporter: scala.tools.nsc.Settings => EvalReporter + ): Eval = + new Eval(options, classpath, mkReporter, Some(evalOutputDirectory(base))) /** * This will clean up left-over files in the config-classes directory if they are no longer used. @@ -703,7 +710,13 @@ private[sbt] object Load { // NOTE - because we create an eval here, we need a clean-eval later for this URI. lazy val eval = timed("Load.loadUnit: mkEval", log) { - mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions) + def mkReporter(settings: scala.tools.nsc.Settings): EvalReporter = + plugs.pluginData.buildTarget match { + case None => EvalReporter.console(settings) + case Some(buildTarget) => + new BuildServerEvalReporter(buildTarget, new ConsoleReporter(settings)) + } + mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions, mkReporter) } val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase)) ++ buildLevelExtraProjects @@ -1164,12 +1177,22 @@ 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 + val buildTarget = (Configurations.Compile / bspTargetIdentifier).value PluginData( removeEntries(cp, prod), prod, Some(fullResolvers.value.toVector), Some(update.value), - opts + opts, + unmanagedSrcDirs, + unmanagedSrcs, + managedSrcDirs, + managedSrcs, + Some(buildTarget) ) }, scalacOptions += "-Wconf:cat=unused-nowarn:s", @@ -1225,7 +1248,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, None) ) def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins = @@ -1417,7 +1440,12 @@ final case class LoadBuildConfiguration( data.internalClasspath, Some(data.resolvers), Some(data.updateReport), - Nil + Nil, + Nil, + Nil, + Nil, + Nil, + None ) case None => PluginData(globalPluginClasspath) } diff --git a/main/src/main/scala/sbt/internal/server/BuildServerEvalReporter.scala b/main/src/main/scala/sbt/internal/server/BuildServerEvalReporter.scala new file mode 100644 index 000000000..cc4475e75 --- /dev/null +++ b/main/src/main/scala/sbt/internal/server/BuildServerEvalReporter.scala @@ -0,0 +1,93 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.server + +import sbt.StandardMain.exchange +import sbt.compiler.ForwardingReporter +import sbt.internal.bsp +import sbt.internal.bsp.{ + BuildTargetIdentifier, + Diagnostic, + DiagnosticSeverity, + PublishDiagnosticsParams, + Range, + TextDocumentIdentifier +} + +import java.nio.file.{ Files, Path, Paths } +import scala.collection.mutable +import scala.reflect.internal.Reporter +import scala.reflect.internal.util.{ DefinedPosition, Position } +import scala.tools.nsc.reporters.FilteringReporter +import sbt.internal.bsp.codec.JsonProtocol._ + +class BuildServerEvalReporter(buildTarget: BuildTargetIdentifier, delegate: FilteringReporter) + extends ForwardingReporter(delegate) { + private val problemsByFile = mutable.Map[Path, Vector[Diagnostic]]() + + override def doReport(pos: Position, msg: String, severity: Severity): Unit = { + for { + filePath <- if (pos.source.file.exists) Some(Paths.get(pos.source.file.path)) else None + range <- convertToRange(pos) + } { + val bspSeverity = convertToBsp(severity) + val diagnostic = Diagnostic(range, bspSeverity, None, Option("sbt"), msg) + problemsByFile(filePath) = problemsByFile.getOrElse(filePath, Vector()) :+ diagnostic + val params = PublishDiagnosticsParams( + TextDocumentIdentifier(filePath.toUri), + buildTarget, + originId = None, + Vector(diagnostic), + reset = false + ) + exchange.notifyEvent("build/publishDiagnostics", params) + } + super.doReport(pos, msg, severity) + } + + override def finalReport(sourceName: String): Unit = { + val filePath = Paths.get(sourceName) + if (Files.exists(filePath)) { + val diagnostics = problemsByFile.getOrElse(filePath, Vector()) + val params = PublishDiagnosticsParams( + textDocument = TextDocumentIdentifier(filePath.toUri), + buildTarget, + originId = None, + diagnostics, + reset = true + ) + exchange.notifyEvent("build/publishDiagnostics", params) + } + } + + private def convertToBsp(severity: Severity): Option[Long] = { + val result = severity match { + case Reporter.INFO => DiagnosticSeverity.Information + case Reporter.WARNING => DiagnosticSeverity.Warning + case Reporter.ERROR => DiagnosticSeverity.Error + } + Some(result) + } + + private def convertToRange(pos: Position): Option[Range] = { + pos match { + case _: DefinedPosition => + val startLine = pos.source.offsetToLine(pos.start) + val startChar = pos.start - pos.source.lineToOffset(startLine) + val endLine = pos.source.offsetToLine(pos.end) + val endChar = pos.end - pos.source.lineToOffset(endLine) + Some( + Range( + bsp.Position(startLine.toLong, startChar.toLong), + bsp.Position(endLine.toLong, endChar.toLong) + ) + ) + case _ => None + } + } +} diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index c8ff79bf7..d97e77960 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -10,25 +10,31 @@ 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._ import sbt.internal.langserver.ErrorCodes import sbt.internal.protocol.JsonRpcRequestMessage -import sbt.internal.util.Attributed +import sbt.internal.util.{ Attributed, ErrorHandling } 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 } @@ -47,35 +53,19 @@ object BuildServerProtocol { ) private val bspReload = "bspReload" - private val bspReloadFailed = "bspReloadFailed" - private val bspReloadSucceed = "bspReloadSucceed" lazy val commands: Seq[Command] = Seq( Command.single(bspReload) { (state, reqId) => - import sbt.BasicCommandStrings._ - import sbt.internal.CommandStrings._ - val result = List( - StashOnFailure, - s"$OnFailure $bspReloadFailed $reqId", - LoadProjectImpl, - s"$bspReloadSucceed $reqId", - PopOnFailure, - FailureWall - ) ::: state - result - }, - Command.single(bspReloadFailed) { (state, reqId) => - exchange.respondError( - ErrorCodes.InternalError, - "reload failed", - Some(reqId), - state.source - ) - state - }, - Command.single(bspReloadSucceed) { (state, reqId) => - exchange.respondEvent(JNull, Some(reqId), state.source) - state + try { + val newState = BuiltinCommands.doLoadProject(state, Project.LoadAction.Current) + exchange.respondEvent(JNull, Some(reqId), state.source) + newState + } catch { + case NonFatal(e) => + val msg = ErrorHandling.reducedToString(e) + exchange.respondError(ErrorCodes.InternalError, msg, Some(reqId), state.source) + state.fail + } } ) @@ -93,36 +83,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 +153,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 +167,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 +181,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 +223,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 +288,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 +322,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 +335,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 +386,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 +412,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 +461,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 +481,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 +548,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 +687,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 +698,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 +715,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 +760,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 +821,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 +863,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..eb8d9d593 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, None) val builds: DetectedModules[BuildDef] = new DetectedModules[BuildDef](Nil) val detectedAutoPlugins: Seq[DetectedAutoPlugin] = diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 76ed2003e..3d398e5bb 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -22,7 +22,7 @@ object Dependencies { private val libraryManagementCore = "org.scala-sbt" %% "librarymanagement-core" % lmVersion private val libraryManagementIvy = "org.scala-sbt" %% "librarymanagement-ivy" % lmVersion - val launcherVersion = "1.3.2" + val launcherVersion = "1.3.3" val launcherInterface = "org.scala-sbt" % "launcher-interface" % launcherVersion val rawLauncher = "org.scala-sbt" % "launcher" % launcherVersion val testInterface = "org.scala-sbt" % "test-interface" % "1.0" diff --git a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala index 9b21dd31e..85c2bd3cb 100644 --- a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -7,12 +7,14 @@ package sbt.internal.bsp -import java.io.File - -import sbt.internal.bsp +import sbt.internal.bsp.codec.JsonProtocol.BspConnectionDetailsFormat import sbt.io.IO import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter } +import java.io.File +import java.nio.file.{ Files, Paths } +import scala.util.Properties + object BuildServerConnection { final val name = "sbt" final val bspVersion = "2.0.0-M5" @@ -21,14 +23,25 @@ object BuildServerConnection { private final val SbtLaunchJar = "sbt-launch(-.*)?\\.jar".r private[sbt] def writeConnectionFile(sbtVersion: String, baseDir: File): Unit = { - import bsp.codec.JsonProtocol._ val bspConnectionFile = new File(baseDir, ".bsp/sbt.json") val javaHome = System.getProperty("java.home") val classPath = System.getProperty("java.class.path") + + val sbtScript = Option(System.getProperty("sbt.script")) + .orElse(sbtScriptInPath) + .map(script => s"-Dsbt.script=$script") + + // IntelliJ can start sbt even if the sbt script is not accessible from $PATH. + // To do so it uses its own bundled sbt-launch.jar. + // In that case, we must pass the path of the sbt-launch.jar to the BSP connection + // so that the server can be started. + // A known problem in that situation is that the .sbtopts and .jvmopts are not loaded. val sbtLaunchJar = classPath .split(File.pathSeparator) .find(jar => SbtLaunchJar.findFirstIn(jar).nonEmpty) .map(_.replaceAllLiterally(" ", "%20")) + .map(jar => s"--sbt-launch-jar=$jar") + val argv = Vector( s"$javaHome/bin/java", @@ -38,9 +51,21 @@ object BuildServerConnection { classPath, "xsbt.boot.Boot", "-bsp" - ) ++ sbtLaunchJar.map(jar => s"--sbt-launch-jar=$jar") + ) ++ sbtScript.orElse(sbtLaunchJar) val details = BspConnectionDetails(name, sbtVersion, bspVersion, languages, argv) val json = Converter.toJson(details).get IO.write(bspConnectionFile, CompactPrinter(json), append = false) } + + private def sbtScriptInPath: Option[String] = { + // For those who use an old sbt script, the -Dsbt.script is not set + // As a fallback we try to find the sbt script in $PATH + val fileName = if (Properties.isWin) "sbt.bat" else "sbt" + val envPath = Option(System.getenv("PATH")).getOrElse("") + val allPaths = envPath.split(File.pathSeparator).map(Paths.get(_)) + allPaths + .map(_.resolve(fileName)) + .find(file => Files.exists(file) && Files.isExecutable(file)) + .map(_.toString.replaceAllLiterally(" ", "%20")) + } } diff --git a/sbt b/sbt index 8adff2117..7fb865514 100755 --- a/sbt +++ b/sbt @@ -299,6 +299,16 @@ addDefaultMemory() { fi } +addSbtScriptProperty () { + if [[ "${java_args[@]}" == *-Dsbt.script=* ]]; then + : + else + sbt_script=$0 + sbt_script=${sbt_script/ /%20} + addJava "-Dsbt.script=$sbt_script" + fi +} + require_arg () { local type="$1" local opt="$2" @@ -769,6 +779,7 @@ else java_version="$(jdk_version)" vlog "[process_args] java_version = '$java_version'" addDefaultMemory + addSbtScriptProperty set -- "${residual_args[@]}" argumentCount=$# run 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..f9c31ecb7 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -7,10 +7,19 @@ package testpkg +import sbt.internal.bsp.SourcesResult +import sbt.internal.bsp.WorkspaceBuildTargetsResult +import sbt.internal.langserver.ErrorCodes +import sbt.IO + +import java.io.File 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,33 +35,56 @@ 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") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#util/Compile" + val buildTarget = buildTargetUri("util", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "24", "method": "buildTarget/sources", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".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") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#util/Compile" + val buildTarget = buildTargetUri("util", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "32", "method": "buildTarget/compile", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/compile")) @@ -63,10 +95,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/scalacOptions") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#util/Compile" + val buildTarget = buildTargetUri("util", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "40", "method": "buildTarget/scalacOptions", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/scalacOptions")) @@ -87,11 +119,66 @@ object BuildServerTest extends AbstractServerTest { }) } + test("workspace/reload: send diagnostic and respond with error") { _ => + // write an other-build.sbt file that does not compile + val otherBuildFile = new File(svr.baseDirectory, "other-build.sbt") + IO.write( + otherBuildFile, + """ + |val someSettings = Seq( + | scalacOptions ++= "-deprecation" + |) + |""".stripMargin + ) + // reload + svr.sendJsonRpc( + """{ "jsonrpc": "2.0", "id": "52", "method": "workspace/reload"}""" + ) + assert( + svr.waitForString(10.seconds) { s => + s.contains(s""""buildTarget":{"uri":"$metaBuildTarget"}""") && + s.contains(s""""textDocument":{"uri":"${otherBuildFile.toPath.toUri}"}""") && + s.contains(""""severity":1""") && + s.contains(""""reset":true""") + } + ) + assert( + svr.waitForString(10.seconds) { s => + s.contains(""""id":"52"""") && + s.contains(""""error"""") && + s.contains(s""""code":${ErrorCodes.InternalError}""") && + s.contains("Type error in expression") + } + ) + // fix the other-build.sbt file and reload again + IO.write( + otherBuildFile, + """ + |val someSettings = Seq( + | scalacOptions += "-deprecation" + |) + |""".stripMargin + ) + svr.sendJsonRpc( + """{ "jsonrpc": "2.0", "id": "52", "method": "workspace/reload"}""" + ) + // assert received an empty diagnostic + assert( + svr.waitForString(10.seconds) { s => + s.contains(s""""buildTarget":{"uri":"$metaBuildTarget"}""") && + s.contains(s""""textDocument":{"uri":"${otherBuildFile.toPath.toUri}"}""") && + s.contains(""""diagnostics":[]""") && + s.contains(""""reset":true""") + } + ) + IO.delete(otherBuildFile) + } + test("buildTarget/scalaMainClasses") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Compile" + val buildTarget = buildTargetUri("runAndTest", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "56", "method": "buildTarget/scalaMainClasses", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/scalaMainClasses")) @@ -102,10 +189,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/run") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Compile" + val buildTarget = buildTargetUri("runAndTest", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "64", "method": "buildTarget/run", "params": { - | "target": { "uri": "$x" }, + | "target": { "uri": "$buildTarget" }, | "dataKind": "scala-main-class", | "data": { "class": "main.Main" } |} }""".stripMargin @@ -122,10 +209,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/scalaTestClasses") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Test" + val buildTarget = buildTargetUri("runAndTest", "Test") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "72", "method": "buildTarget/scalaTestClasses", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/scalaTestClasses")) @@ -137,10 +224,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/test: run all tests") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Test" + val buildTarget = buildTargetUri("runAndTest", "Test") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "80", "method": "buildTarget/test", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/test")) @@ -151,15 +238,15 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/test: run one test class") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Test" + val buildTarget = buildTargetUri("runAndTest", "Test") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "84", "method": "buildTarget/test", "params": { - | "targets": [{ "uri": "$x" }], + | "targets": [{ "uri": "$buildTarget" }], | "dataKind": "scala-test", | "data": { | "testClasses": [ | { - | "target": { "uri": "$x" }, + | "target": { "uri": "$buildTarget" }, | "classes": ["tests.PassingTest"] | } | ] @@ -174,53 +261,53 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/compile: report error") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#reportError/Compile" + val buildTarget = buildTargetUri("reportError", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "88", "method": "buildTarget/compile", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(svr.waitForString(10.seconds) { s => - (s contains s""""buildTarget":{"uri":"$x"}""") && + (s contains s""""buildTarget":{"uri":"$buildTarget"}""") && (s contains """"severity":1""") && (s contains """"reset":true""") }) } test("buildTarget/compile: report warning") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#reportWarning/Compile" + val buildTarget = buildTargetUri("reportWarning", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "90", "method": "buildTarget/compile", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(svr.waitForString(10.seconds) { s => - (s contains s""""buildTarget":{"uri":"$x"}""") && + (s contains s""""buildTarget":{"uri":"$buildTarget"}""") && (s contains """"severity":2""") && (s contains """"reset":true""") }) } test("buildTarget/compile: respond error") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#respondError/Compile" + val buildTarget = buildTargetUri("respondError", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "92", "method": "buildTarget/compile", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(svr.waitForString(10.seconds) { s => s.contains(""""id":"92"""") && s.contains(""""error"""") && - s.contains(""""code":-32603""") && + s.contains(s""""code":${ErrorCodes.InternalError}""") && s.contains("custom message") }) } test("buildTarget/resources") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#util/Compile" + val buildTarget = buildTargetUri("util", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "96", "method": "buildTarget/resources", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/resources")) @@ -250,4 +337,10 @@ object BuildServerTest extends AbstractServerTest { msg.contains(s""""message":"Processing $method"""") } } + + private def buildTargetUri(project: String, config: String): String = + s"${svr.baseDirectory.getAbsoluteFile.toURI}#$project/$config" + + private def metaBuildTarget: String = + s"${svr.baseDirectory.getAbsoluteFile.toURI}project/#buildserver-build/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