Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for sbt-dotty #2690

Merged
merged 7 commits into from
Jun 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 10 additions & 24 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -920,19 +920,15 @@ 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)
.inheritIO()
.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
Expand All @@ -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
},
Expand All @@ -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"
},
Expand All @@ -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")
Expand All @@ -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("<arg>").parsed
Expand All @@ -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
)

Expand Down
203 changes: 139 additions & 64 deletions sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -27,46 +27,162 @@ 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I very much doubt sorted sorts in a meaningful way here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I had a FIXME before that we need a real ordering for version numbers, will add it back.

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._

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different from what prepareIDE did before. It called compileForIDE.value, not compile.value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but all compileForIDE does is call compile on each dotty configuration, here we already are in a task run in every dotty configuration, so compile is fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK but then, why is compileForIDE still necessary? It doesn't seem to be called by anything.


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)
Expand All @@ -85,61 +201,20 @@ 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")
.inheritIO()
.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")
}
4 changes: 2 additions & 2 deletions sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """ <version>(0.1\..*-bin.*)</version>""".r
val Version = """ <version>(0.2\..*-bin.*)</version>""".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
Expand Down