-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Fixes for sbt-dotty #2690
Changes from all commits
7f89bfa
9b086b6
ba33872
f4960d1
34b04d4
bd1df68
4d05747
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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 { | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is different from what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, but all compileForIDE does is call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK but then, why is |
||
|
||
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,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") | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.