@@ -14,7 +14,7 @@ import DottyPlugin.autoImport._
1414
1515object 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,46 +27,162 @@ 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 MessageOnlyException (" No Dotty project detected" )
86+ else {
87+ val dottyVersion = dottyVersions.sorted.last
88+ val dottyState = updateScalaVersion(state, dottyProjRefs, dottyVersion)
89+ (dottyVersion, dottyProjRefs, dottyState)
90+ }
91+ }
92+
93+ /** Run `task` in state `state` */
94+ private def runTask [T ](task : Task [T ], state : State ): T = {
95+ val extracted = Project .extract(state)
96+ val structure = extracted.structure
97+ val (_, result) =
98+ EvaluateTask .withStreams(structure, state) { streams =>
99+ EvaluateTask .runTask(task, state, streams, structure.index.triggers,
100+ EvaluateTask .extractedTaskConfig(extracted, structure, state))(
101+ EvaluateTask .nodeView(state, streams, Nil )
102+ )
103+ }
104+ result match {
105+ case Value (v) =>
106+ v
107+ case Inc (i) =>
108+ throw i
109+ }
110+ }
111+
112+ /** Run task `key` in all configurations in all projects in `projRefs`, using state `state` */
113+ private def runInAllConfigurations [T ](key : TaskKey [T ], projRefs : Seq [ProjectRef ], state : State ): Seq [T ] = {
114+ val structure = Project .structure(state)
115+ val settings = structure.data
116+ val joinedTask = projRefs.flatMap { projRef =>
117+ val project = Project .getProjectForReference(projRef, structure).get
35118 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- }
119+ key.in(projRef, config).get(settings)
42120 }
43121 }.join
122+
123+ runTask(joinedTask, state)
44124 }
45125
46126 private val projectConfig = taskKey[Option [ProjectConfig ]](" " )
47- private val configureIDE = taskKey[Unit ](" Generate IDE config files" )
48- private val compileForIDE = taskKey[Unit ](" Compile all projects supported by the IDE" )
49- private val runCode = taskKey[Unit ](" " )
50127
51128 object autoImport {
52- val prepareIDE = taskKey[Unit ](" Prepare for IDE launch " )
53- val launchIDE = taskKey[Unit ](" Run Visual Studio Code on this project" )
129+ val runCode = taskKey[Unit ](" Start VSCode, usually called from launchIDE " )
130+ val launchIDE = taskKey[Unit ](" Configure and run VSCode on this project" )
54131 }
55132
56133 import autoImport ._
57134
58135 override def requires : Plugins = plugins.JvmPlugin
59136 override def trigger = allRequirements
60137
138+ def configureIDE = Command .command(" configureIDE" ) { origState =>
139+ val (dottyVersion, projRefs, dottyState) = dottySetup(origState)
140+ val configs0 = runInAllConfigurations(projectConfig, projRefs, dottyState).flatten
141+
142+ // Drop configurations that do not define their own sources, but just
143+ // inherit their sources from some other configuration.
144+ val configs = distinctBy(configs0)(_.sourceDirectories.deep)
145+
146+ // Write the version of the Dotty Language Server to use in a file by itself.
147+ // This could be a field in the JSON config file, but that would require all
148+ // IDE plugins to parse JSON.
149+ val dlsVersion = dottyVersion
150+ .replace(" -nonbootstrapped" , " " ) // The language server is only published bootstrapped
151+ val dlsBinaryVersion = dlsVersion.split(" \\ ." ).take(2 ).mkString(" ." )
152+ val pwArtifact = new PrintWriter (" .dotty-ide-artifact" )
153+ try {
154+ pwArtifact.println(s " ch.epfl.lamp:dotty-language-server_ ${dlsBinaryVersion}: ${dlsVersion}" )
155+ } finally {
156+ pwArtifact.close()
157+ }
158+
159+ val mapper = new ObjectMapper
160+ mapper.writerWithDefaultPrettyPrinter()
161+ .writeValue(new File (" .dotty-ide.json" ), configs.toArray)
162+
163+ origState
164+ }
165+
166+ def compileForIDE = Command .command(" compileForIDE" ) { origState =>
167+ val (dottyVersion, projRefs, dottyState) = dottySetup(origState)
168+ runInAllConfigurations(compile, projRefs, dottyState)
169+
170+ origState
171+ }
172+
61173 override def projectSettings : Seq [Setting [_]] = Seq (
62174 // Use Def.derive so `projectConfig` is only defined in the configurations where the
63175 // tasks/settings it depends on are defined.
64176 Def .derive(projectConfig := {
65177 if (sources.value.isEmpty) None
66178 else {
179+ // Not needed to generate the config, but this guarantees that the
180+ // generated config is usable by an IDE without any extra compilation
181+ // step.
182+ val _ = compile.value
183+
67184 val id = s " ${thisProject.value.id}/ ${configuration.value.name}"
68185 val compilerVersion = scalaVersion.value
69- .replace(" -nonbootstrapped" , " " ) // The language server is only published bootstrapped
70186 val compilerArguments = scalacOptions.value
71187 val sourceDirectories = unmanagedSourceDirectories.value ++ managedSourceDirectories.value
72188 val depClasspath = Attributed .data(dependencyClasspath.value)
@@ -85,61 +201,20 @@ object DottyIDEPlugin extends AutoPlugin {
85201 )
86202
87203 override def buildSettings : Seq [Setting [_]] = Seq (
88- configureIDE := {
89- val log = streams.value.log
90-
91- val configs0 = state.flatMap(s =>
92- inAllDottyConfigurations(projectConfig, s)
93- ).value.flatten
94- // Drop configurations who do not define their own sources, but just
95- // inherit their sources from some other configuration.
96- val configs = distinctBy(configs0)(_.sourceDirectories.deep)
97-
98- if (configs.isEmpty) {
99- log.error(" No Dotty project detected" )
100- } else {
101- // If different versions of Dotty are used by subprojects, choose the latest one
102- // FIXME: use a proper version number Ordering that knows that "0.1.1-M1" < "0.1.1"
103- val ideVersion = configs.map(_.compilerVersion).sorted.last
104- // Write the version of the Dotty Language Server to use in a file by itself.
105- // This could be a field in the JSON config file, but that would require all
106- // IDE plugins to parse JSON.
107- val pwArtifact = new PrintWriter (" .dotty-ide-artifact" )
108- pwArtifact.println(s " ch.epfl.lamp:dotty-language-server_0.1: ${ideVersion}" )
109- pwArtifact.close()
110-
111- val mapper = new ObjectMapper
112- mapper.writerWithDefaultPrettyPrinter()
113- .writeValue(new File (" .dotty-ide.json" ), configs.toArray)
114- }
115- },
116-
117- compileForIDE := {
118- val _ = state.flatMap(s =>
119- inAllDottyConfigurations(compile, s)
120- ).value
121- },
204+ commands ++= Seq (configureIDE, compileForIDE),
122205
123206 runCode := {
124207 val exitCode = new ProcessBuilder (" code" , " --install-extension" , " lampepfl.dotty" )
125208 .inheritIO()
126209 .start()
127210 .waitFor()
128211 if (exitCode != 0 )
129- throw new FeedbackProvidedException {
130- override def toString = " Installing the Dotty support for VSCode failed"
131- }
212+ throw new MessageOnlyException (" Installing the Dotty support for VSCode failed" )
132213
133214 new ProcessBuilder (" code" , baseDirectory.value.getAbsolutePath)
134215 .inheritIO()
135216 .start()
136- },
137-
138- prepareIDE := {
139- val x1 = configureIDE.value
140- val x2 = compileForIDE.value
141- },
142-
143- launchIDE := runCode.dependsOn(prepareIDE).value
144- )
217+ }
218+
219+ ) ++ addCommandAlias(" launchIDE" , " ;configureIDE;runCode" )
145220}
0 commit comments