Skip to content

Commit 9b086b6

Browse files
committed
Make configureIDE work with crossScalaVersions
So far, configureIDE detected the dotty-compiled projects by looking at `scalaVersion`, with this commit we now also look at `crossScalaVersions`. This is considerably harder to do since extracting the settings requires updating `scalaVersion` in all projects where it's not set to a dotty version but `crossScalaVersion` is, moreover this update needs to be temporary: once the `configureIDE` task is done, the state should revert to the original one.
1 parent 7f89bfa commit 9b086b6

File tree

1 file changed

+119
-37
lines changed

1 file changed

+119
-37
lines changed

sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala

Lines changed: 119 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import DottyPlugin.autoImport._
1414

1515
object DottyIDEPlugin extends AutoPlugin {
1616
// Adapted from scala-reflect
17-
private[this] def distinctBy[A, B](xs: Seq[A])(f: A => B): Seq[A] = {
17+
private def distinctBy[A, B](xs: Seq[A])(f: A => B): Seq[A] = {
1818
val buf = new mutable.ListBuffer[A]
1919
val seen = mutable.Set[B]()
2020
xs foreach { x =>
@@ -27,20 +27,102 @@ object DottyIDEPlugin extends AutoPlugin {
2727
buf.toList
2828
}
2929

30-
private def inAllDottyConfigurations[A](key: TaskKey[A], state: State): Task[Seq[A]] = {
31-
val struct = Project.structure(state)
32-
val settings = struct.data
33-
struct.allProjectRefs.flatMap { projRef =>
34-
val project = Project.getProjectForReference(projRef, struct).get
30+
private def isDottyVersion(version: String) =
31+
version.startsWith("0.")
32+
33+
34+
/** Return a new state derived from `state` such that scalaVersion returns `newScalaVersion` in all
35+
* projects in `projRefs` (`state` is returned if no setting needed to be updated).
36+
*/
37+
private def updateScalaVersion(state: State, projRefs: Seq[ProjectRef], newScalaVersion: String): State = {
38+
val extracted = Project.extract(state)
39+
val settings = extracted.structure.data
40+
41+
if (projRefs.forall(projRef => scalaVersion.in(projRef).get(settings).get == newScalaVersion))
42+
state
43+
else {
44+
def matchingSetting(setting: Setting[_]) =
45+
setting.key.key == scalaVersion.key &&
46+
setting.key.scope.project.fold(ref => projRefs.contains(ref), ifGlobal = true, ifThis = true)
47+
48+
val newSettings = extracted.session.mergeSettings.collect {
49+
case setting if matchingSetting(setting) =>
50+
scalaVersion in setting.key.scope := newScalaVersion
51+
}
52+
val newSession = extracted.session.appendRaw(newSettings)
53+
BuiltinCommands.reapply(newSession, extracted.structure, state)
54+
}
55+
}
56+
57+
/** Setup to run in all dotty projects.
58+
* Return a triplet of:
59+
* (1) A version of dotty
60+
* (2) A list of dotty projects
61+
* (3) A state where `scalaVersion` is set to (1) in all projects in (2)
62+
*/
63+
private def dottySetup(state: State): (String, Seq[ProjectRef], State) = {
64+
val structure = Project.structure(state)
65+
val settings = structure.data
66+
67+
// FIXME: this function uses `sorted` to order versions but this is incorrect,
68+
// we need an Ordering for version numbers, like the one in Coursier.
69+
70+
val (dottyVersions, dottyProjRefs) =
71+
structure.allProjectRefs.flatMap { projRef =>
72+
val version = scalaVersion.in(projRef).get(settings).get
73+
if (isDottyVersion(version))
74+
Some((version, projRef))
75+
else
76+
crossScalaVersions.in(projRef).get(settings).get.filter(isDottyVersion).sorted.lastOption match {
77+
case Some(v) =>
78+
Some((v, projRef))
79+
case _ =>
80+
None
81+
}
82+
}.unzip
83+
84+
if (dottyVersions.isEmpty)
85+
throw new FeedbackProvidedException {
86+
override def toString = "No Dotty project detected"
87+
}
88+
else {
89+
val dottyVersion = dottyVersions.sorted.last
90+
val dottyState = updateScalaVersion(state, dottyProjRefs, dottyVersion)
91+
(dottyVersion, dottyProjRefs, dottyState)
92+
}
93+
}
94+
95+
/** Run `task` in state `state` */
96+
private def runTask[T](task: Task[T], state: State): T = {
97+
val extracted = Project.extract(state)
98+
val structure = extracted.structure
99+
val (_, result) =
100+
EvaluateTask.withStreams(structure, state) { streams =>
101+
EvaluateTask.runTask(task, state, streams, structure.index.triggers,
102+
EvaluateTask.extractedTaskConfig(extracted, structure, state))(
103+
EvaluateTask.nodeView(state, streams, Nil)
104+
)
105+
}
106+
result match {
107+
case Value(v) =>
108+
v
109+
case Inc(i) =>
110+
throw i
111+
}
112+
}
113+
114+
/** Run task `key` in all configurations in all projects in `projRefs`, using state `state` */
115+
private def runInAllConfigurations[T](key: TaskKey[T], projRefs: Seq[ProjectRef], state: State): Seq[T] = {
116+
val structure = Project.structure(state)
117+
val settings = structure.data
118+
val joinedTask = projRefs.flatMap { projRef =>
119+
val project = Project.getProjectForReference(projRef, structure).get
35120
project.configurations.flatMap { config =>
36-
isDotty.in(projRef, config).get(settings) match {
37-
case Some(true) =>
38-
key.in(projRef, config).get(settings)
39-
case _ =>
40-
None
41-
}
121+
key.in(projRef, config).get(settings)
42122
}
43123
}.join
124+
125+
runTask(joinedTask, state)
44126
}
45127

46128
private val projectConfig = taskKey[Option[ProjectConfig]]("")
@@ -57,6 +139,7 @@ object DottyIDEPlugin extends AutoPlugin {
57139
override def requires: Plugins = plugins.JvmPlugin
58140
override def trigger = allRequirements
59141

142+
60143
override def projectSettings: Seq[Setting[_]] = Seq(
61144
// Use Def.derive so `projectConfig` is only defined in the configurations where the
62145
// tasks/settings it depends on are defined.
@@ -70,7 +153,6 @@ object DottyIDEPlugin extends AutoPlugin {
70153

71154
val id = s"${thisProject.value.id}/${configuration.value.name}"
72155
val compilerVersion = scalaVersion.value
73-
.replace("-nonbootstrapped", "") // The language server is only published bootstrapped
74156
val compilerArguments = scalacOptions.value
75157
val sourceDirectories = unmanagedSourceDirectories.value ++ managedSourceDirectories.value
76158
val depClasspath = Attributed.data(dependencyClasspath.value)
@@ -89,40 +171,40 @@ object DottyIDEPlugin extends AutoPlugin {
89171
)
90172

91173
override def buildSettings: Seq[Setting[_]] = Seq(
92-
configureIDE := {
93-
val log = streams.value.log
94-
95-
val configs0 = state.flatMap(s =>
96-
inAllDottyConfigurations(projectConfig, s)
97-
).value.flatten
98-
// Drop configurations who do not define their own sources, but just
99-
// inherit their sources from some other configuration.
100-
val configs = distinctBy(configs0)(_.sourceDirectories.deep)
101-
102-
if (configs.isEmpty) {
103-
log.error("No Dotty project detected")
104-
} else {
105-
// If different versions of Dotty are used by subprojects, choose the latest one
106-
// FIXME: use a proper version number Ordering that knows that "0.1.1-M1" < "0.1.1"
107-
val ideVersion = configs.map(_.compilerVersion).sorted.last
174+
configureIDE := Def.taskDyn {
175+
val origState = state.value
176+
Def.task {
177+
val (dottyVersion, projRefs, dottyState) = dottySetup(origState)
178+
val configs0 = runInAllConfigurations(projectConfig, projRefs, dottyState).flatten
179+
180+
// Drop configurations that do not define their own sources, but just
181+
// inherit their sources from some other configuration.
182+
val configs = distinctBy(configs0)(_.sourceDirectories.deep)
183+
108184
// Write the version of the Dotty Language Server to use in a file by itself.
109185
// This could be a field in the JSON config file, but that would require all
110186
// IDE plugins to parse JSON.
187+
val dlsVersion = dottyVersion
188+
.replace("-nonbootstrapped", "") // The language server is only published bootstrapped
189+
val dlsBinaryVersion = dlsVersion.split("\\.").take(2).mkString(".")
111190
val pwArtifact = new PrintWriter(".dotty-ide-artifact")
112-
pwArtifact.println(s"ch.epfl.lamp:dotty-language-server_0.1:${ideVersion}")
191+
pwArtifact.println(s"ch.epfl.lamp:dotty-language-server_${dlsBinaryVersion}:${dlsVersion}")
113192
pwArtifact.close()
114193

115194
val mapper = new ObjectMapper
116195
mapper.writerWithDefaultPrettyPrinter()
117196
.writeValue(new File(".dotty-ide.json"), configs.toArray)
118197
}
119-
},
120-
121-
compileForIDE := {
122-
val _ = state.flatMap(s =>
123-
inAllDottyConfigurations(compile, s)
124-
).value
125-
},
198+
}.value,
199+
200+
compileForIDE := Def.taskDyn {
201+
val origState = state.value
202+
Def.task {
203+
val (dottyVersion, projRefs, dottyState) = dottySetup(origState)
204+
runInAllConfigurations(compile, projRefs, dottyState)
205+
()
206+
}
207+
}.value,
126208

127209
runCode := {
128210
val exitCode = new ProcessBuilder("code", "--install-extension", "lampepfl.dotty")

0 commit comments

Comments
 (0)