diff --git a/project/Build.scala b/project/Build.scala index 2dc8ea68b101..fea9b5496547 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -91,10 +91,10 @@ object Build { bootstrapFromPublishedJars := false, - // Override `launchIDE` from sbt-dotty to use the language-server and + // Override `runCode` from sbt-dotty to use the language-server and // vscode extension from the source repository of dotty instead of a // published version. - launchIDE := (run in `dotty-language-server`).dependsOn(prepareIDE).toTask("").value + runCode := (run in `dotty-language-server`).toTask("").value ) // Only available in vscode-dotty @@ -882,7 +882,7 @@ object Build { sbtPlugin := true, - version := "0.1.1", + version := "0.1.2", ScriptedPlugin.scriptedSettings, ScriptedPlugin.sbtTestDirectory := baseDirectory.value / "sbt-test", ScriptedPlugin.scriptedBufferLog := false, @@ -920,9 +920,7 @@ object Build { .start() .waitFor() if (exitCode != 0) - throw new FeedbackProvidedException { - override def toString = "'npm run update-all' in vscode-dotty failed" - } + throw new MessageOnlyException("'npm run update-all' in vscode-dotty failed") } val tsc = baseDirectory.value / "node_modules" / ".bin" / "tsc" val exitCodeTsc = new java.lang.ProcessBuilder(tsc.getAbsolutePath, "--pretty", "--project", baseDirectory.value.getAbsolutePath) @@ -930,9 +928,7 @@ object Build { .start() .waitFor() if (exitCodeTsc != 0) - throw new FeedbackProvidedException { - override def toString = "tsc in vscode-dotty failed" - } + throw new MessageOnlyException("tsc in vscode-dotty failed") // Currently, vscode-dotty depends on daltonjorge.scala for syntax highlighting, // this is not automatically installed when starting the extension in development mode @@ -942,9 +938,7 @@ object Build { .start() .waitFor() if (exitCodeInstall != 0) - throw new FeedbackProvidedException { - override def toString = "Installing dependency daltonjorge.scala failed" - } + throw new MessageOnlyException("Installing dependency daltonjorge.scala failed") sbt.inc.Analysis.Empty }, @@ -955,9 +949,7 @@ object Build { .start() .waitFor() if (exitCode != 0) - throw new FeedbackProvidedException { - override def toString = "vsce package failed" - } + throw new MessageOnlyException("vsce package failed") baseDirectory.value / s"dotty-${version.value}.vsix" }, @@ -968,9 +960,7 @@ object Build { .start() .waitFor() if (exitCode != 0) - throw new FeedbackProvidedException { - override def toString = "vsce unpublish failed" - } + throw new MessageOnlyException("vsce unpublish failed") }, publish := { val exitCode = new java.lang.ProcessBuilder("vsce", "publish") @@ -979,9 +969,7 @@ object Build { .start() .waitFor() if (exitCode != 0) - throw new FeedbackProvidedException { - override def toString = "vsce publish failed" - } + throw new MessageOnlyException("vsce publish failed") }, run := Def.inputTask { val inputArgs = spaceDelimited("").parsed @@ -994,9 +982,7 @@ object Build { .start() .waitFor() if (exitCode != 0) - throw new FeedbackProvidedException { - override def toString = "Running Visual Studio Code failed" - } + throw new MessageOnlyException("Running Visual Studio Code failed") }.dependsOn(compile in Compile).evaluated ) diff --git a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala index bb2a1227f97b..1e4226d48aa3 100644 --- a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala +++ b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala @@ -14,7 +14,7 @@ import DottyPlugin.autoImport._ object DottyIDEPlugin extends AutoPlugin { // Adapted from scala-reflect - private[this] def distinctBy[A, B](xs: Seq[A])(f: A => B): Seq[A] = { + private def distinctBy[A, B](xs: Seq[A])(f: A => B): Seq[A] = { val buf = new mutable.ListBuffer[A] val seen = mutable.Set[B]() xs foreach { x => @@ -27,30 +27,107 @@ object DottyIDEPlugin extends AutoPlugin { buf.toList } - private def inAllDottyConfigurations[A](key: TaskKey[A], state: State): Task[Seq[A]] = { - val struct = Project.structure(state) - val settings = struct.data - struct.allProjectRefs.flatMap { projRef => - val project = Project.getProjectForReference(projRef, struct).get + private def isDottyVersion(version: String) = + version.startsWith("0.") + + + /** Return a new state derived from `state` such that scalaVersion returns `newScalaVersion` in all + * projects in `projRefs` (`state` is returned if no setting needed to be updated). + */ + private def updateScalaVersion(state: State, projRefs: Seq[ProjectRef], newScalaVersion: String): State = { + val extracted = Project.extract(state) + val settings = extracted.structure.data + + if (projRefs.forall(projRef => scalaVersion.in(projRef).get(settings).get == newScalaVersion)) + state + else { + def matchingSetting(setting: Setting[_]) = + setting.key.key == scalaVersion.key && + setting.key.scope.project.fold(ref => projRefs.contains(ref), ifGlobal = true, ifThis = true) + + val newSettings = extracted.session.mergeSettings.collect { + case setting if matchingSetting(setting) => + scalaVersion in setting.key.scope := newScalaVersion + } + val newSession = extracted.session.appendRaw(newSettings) + BuiltinCommands.reapply(newSession, extracted.structure, state) + } + } + + /** Setup to run in all dotty projects. + * Return a triplet of: + * (1) A version of dotty + * (2) A list of dotty projects + * (3) A state where `scalaVersion` is set to (1) in all projects in (2) + */ + private def dottySetup(state: State): (String, Seq[ProjectRef], State) = { + val structure = Project.structure(state) + val settings = structure.data + + // FIXME: this function uses `sorted` to order versions but this is incorrect, + // we need an Ordering for version numbers, like the one in Coursier. + + val (dottyVersions, dottyProjRefs) = + structure.allProjectRefs.flatMap { projRef => + val version = scalaVersion.in(projRef).get(settings).get + if (isDottyVersion(version)) + Some((version, projRef)) + else + crossScalaVersions.in(projRef).get(settings).get.filter(isDottyVersion).sorted.lastOption match { + case Some(v) => + Some((v, projRef)) + case _ => + None + } + }.unzip + + if (dottyVersions.isEmpty) + throw new MessageOnlyException("No Dotty project detected") + else { + val dottyVersion = dottyVersions.sorted.last + val dottyState = updateScalaVersion(state, dottyProjRefs, dottyVersion) + (dottyVersion, dottyProjRefs, dottyState) + } + } + + /** Run `task` in state `state` */ + private def runTask[T](task: Task[T], state: State): T = { + val extracted = Project.extract(state) + val structure = extracted.structure + val (_, result) = + EvaluateTask.withStreams(structure, state) { streams => + EvaluateTask.runTask(task, state, streams, structure.index.triggers, + EvaluateTask.extractedTaskConfig(extracted, structure, state))( + EvaluateTask.nodeView(state, streams, Nil) + ) + } + result match { + case Value(v) => + v + case Inc(i) => + throw i + } + } + + /** Run task `key` in all configurations in all projects in `projRefs`, using state `state` */ + private def runInAllConfigurations[T](key: TaskKey[T], projRefs: Seq[ProjectRef], state: State): Seq[T] = { + val structure = Project.structure(state) + val settings = structure.data + val joinedTask = projRefs.flatMap { projRef => + val project = Project.getProjectForReference(projRef, structure).get project.configurations.flatMap { config => - isDotty.in(projRef, config).get(settings) match { - case Some(true) => - key.in(projRef, config).get(settings) - case _ => - None - } + key.in(projRef, config).get(settings) } }.join + + runTask(joinedTask, state) } private val projectConfig = taskKey[Option[ProjectConfig]]("") - private val configureIDE = taskKey[Unit]("Generate IDE config files") - private val compileForIDE = taskKey[Unit]("Compile all projects supported by the IDE") - private val runCode = taskKey[Unit]("") object autoImport { - val prepareIDE = taskKey[Unit]("Prepare for IDE launch") - val launchIDE = taskKey[Unit]("Run Visual Studio Code on this project") + val runCode = taskKey[Unit]("Start VSCode, usually called from launchIDE") + val launchIDE = taskKey[Unit]("Configure and run VSCode on this project") } import autoImport._ @@ -58,15 +135,54 @@ object DottyIDEPlugin extends AutoPlugin { override def requires: Plugins = plugins.JvmPlugin override def trigger = allRequirements + def configureIDE = Command.command("configureIDE") { origState => + val (dottyVersion, projRefs, dottyState) = dottySetup(origState) + val configs0 = runInAllConfigurations(projectConfig, projRefs, dottyState).flatten + + // Drop configurations that do not define their own sources, but just + // inherit their sources from some other configuration. + val configs = distinctBy(configs0)(_.sourceDirectories.deep) + + // Write the version of the Dotty Language Server to use in a file by itself. + // This could be a field in the JSON config file, but that would require all + // IDE plugins to parse JSON. + val dlsVersion = dottyVersion + .replace("-nonbootstrapped", "") // The language server is only published bootstrapped + val dlsBinaryVersion = dlsVersion.split("\\.").take(2).mkString(".") + val pwArtifact = new PrintWriter(".dotty-ide-artifact") + try { + pwArtifact.println(s"ch.epfl.lamp:dotty-language-server_${dlsBinaryVersion}:${dlsVersion}") + } finally { + pwArtifact.close() + } + + val mapper = new ObjectMapper + mapper.writerWithDefaultPrettyPrinter() + .writeValue(new File(".dotty-ide.json"), configs.toArray) + + origState + } + + def compileForIDE = Command.command("compileForIDE") { origState => + val (dottyVersion, projRefs, dottyState) = dottySetup(origState) + runInAllConfigurations(compile, projRefs, dottyState) + + origState + } + override def projectSettings: Seq[Setting[_]] = Seq( // Use Def.derive so `projectConfig` is only defined in the configurations where the // tasks/settings it depends on are defined. Def.derive(projectConfig := { if (sources.value.isEmpty) None else { + // Not needed to generate the config, but this guarantees that the + // generated config is usable by an IDE without any extra compilation + // step. + val _ = compile.value + val id = s"${thisProject.value.id}/${configuration.value.name}" val compilerVersion = scalaVersion.value - .replace("-nonbootstrapped", "") // The language server is only published bootstrapped val compilerArguments = scalacOptions.value val sourceDirectories = unmanagedSourceDirectories.value ++ managedSourceDirectories.value val depClasspath = Attributed.data(dependencyClasspath.value) @@ -85,40 +201,7 @@ object DottyIDEPlugin extends AutoPlugin { ) override def buildSettings: Seq[Setting[_]] = Seq( - configureIDE := { - val log = streams.value.log - - val configs0 = state.flatMap(s => - inAllDottyConfigurations(projectConfig, s) - ).value.flatten - // Drop configurations who do not define their own sources, but just - // inherit their sources from some other configuration. - val configs = distinctBy(configs0)(_.sourceDirectories.deep) - - if (configs.isEmpty) { - log.error("No Dotty project detected") - } else { - // If different versions of Dotty are used by subprojects, choose the latest one - // FIXME: use a proper version number Ordering that knows that "0.1.1-M1" < "0.1.1" - val ideVersion = configs.map(_.compilerVersion).sorted.last - // Write the version of the Dotty Language Server to use in a file by itself. - // This could be a field in the JSON config file, but that would require all - // IDE plugins to parse JSON. - val pwArtifact = new PrintWriter(".dotty-ide-artifact") - pwArtifact.println(s"ch.epfl.lamp:dotty-language-server_0.1:${ideVersion}") - pwArtifact.close() - - val mapper = new ObjectMapper - mapper.writerWithDefaultPrettyPrinter() - .writeValue(new File(".dotty-ide.json"), configs.toArray) - } - }, - - compileForIDE := { - val _ = state.flatMap(s => - inAllDottyConfigurations(compile, s) - ).value - }, + commands ++= Seq(configureIDE, compileForIDE), runCode := { val exitCode = new ProcessBuilder("code", "--install-extension", "lampepfl.dotty") @@ -126,20 +209,12 @@ object DottyIDEPlugin extends AutoPlugin { .start() .waitFor() if (exitCode != 0) - throw new FeedbackProvidedException { - override def toString = "Installing the Dotty support for VSCode failed" - } + throw new MessageOnlyException("Installing the Dotty support for VSCode failed") new ProcessBuilder("code", baseDirectory.value.getAbsolutePath) .inheritIO() .start() - }, - - prepareIDE := { - val x1 = configureIDE.value - val x2 = compileForIDE.value - }, - - launchIDE := runCode.dependsOn(prepareIDE).value - ) + } + + ) ++ addCommandAlias("launchIDE", ";configureIDE;runCode") } diff --git a/sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala b/sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala index ed53d537c43e..ce916a6e6ba0 100644 --- a/sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala +++ b/sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala @@ -14,10 +14,10 @@ object DottyPlugin extends AutoPlugin { // - if this was a settingKey, then this would evaluate even if you don't use it. def dottyLatestNightlyBuild: Option[String] = { println("Fetching latest Dotty nightly version (requires an internet connection)...") - val Version = """ (0.1\..*-bin.*)""".r + val Version = """ (0.2\..*-bin.*)""".r val latest = scala.io.Source .fromURL( - "http://repo1.maven.org/maven2/ch/epfl/lamp/dotty_0.1/maven-metadata.xml") + "http://repo1.maven.org/maven2/ch/epfl/lamp/dotty_0.2/maven-metadata.xml") .getLines() .collect { case Version(version) => version } .toSeq