Skip to content

Commit 5d39d24

Browse files
committed
sbt-dotty: Add IDE integration
This adds commands to the sbt-dotty plugin to generate the .dotty-ide.json config file used by the Dotty Language Server and to start Visual Studio Code with everything set up correctly. For end-users, using the IDE is as simple as: 1. Installing vscode (https://code.visualstudio.com) 2. In your project, run `sbt launchIDE`
1 parent cce3ae5 commit 5d39d24

File tree

6 files changed

+189
-5
lines changed

6 files changed

+189
-5
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ node_modules
3131
classes/
3232
*/bin/
3333

34+
# Dotty IDE
35+
/.dotty-ide-dev-port
36+
/.dotty-ide-artifact
37+
/.dotty-ide.json
38+
3439
# idea
3540
.idea
3641
.idea_modules

project/Build.scala

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import scala.reflect.io.Path
1010
import sbtassembly.AssemblyKeys.assembly
1111
import xerial.sbt.Pack._
1212

13-
import org.scalajs.sbtplugin.ScalaJSPlugin
14-
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
1513
import sbt.Package.ManifestAttributes
1614

1715
import com.typesafe.sbteclipse.plugin.EclipsePlugin._
16+
1817
import dotty.tools.sbtplugin.DottyPlugin.autoImport._
18+
import dotty.tools.sbtplugin.DottyIDEPlugin.autoImport._
19+
import org.scalajs.sbtplugin.ScalaJSPlugin
20+
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
1921

2022
/* In sbt 0.13 the Build trait would expose all vals to the shell, where you
2123
* can use them in "set a := b" like expressions. This re-exposes them.
@@ -83,7 +85,13 @@ object Build {
8385
// Used in build.sbt
8486
lazy val thisBuildSettings = Def.settings(
8587
// Change this to true if you want to bootstrap using a published non-bootstrapped compiler
86-
bootstrapFromPublishedJars := false
88+
bootstrapFromPublishedJars := false,
89+
90+
91+
// Override `launchIDE` from sbt-dotty to use the language-server and
92+
// vscode extension from the source repository of dotty instead of a
93+
// published version.
94+
launchIDE := (run in `dotty-language-server`).dependsOn(prepareIDE).toTask("").value
8795
)
8896

8997
// Only available in vscode-dotty
@@ -240,6 +248,7 @@ object Build {
240248
settings(
241249
triggeredMessage in ThisBuild := Watched.clearWhenTriggered,
242250
submoduleChecks,
251+
243252
addCommandAlias("run", "dotty-compiler/run") ++
244253
addCommandAlias("legacyTests", "dotty-compiler/testOnly dotc.tests")
245254
)
@@ -318,7 +327,7 @@ object Build {
318327
"com.vladsch.flexmark" % "flexmark-ext-emoji" % "0.11.1",
319328
"com.vladsch.flexmark" % "flexmark-ext-gfm-strikethrough" % "0.11.1",
320329
"com.vladsch.flexmark" % "flexmark-ext-yaml-front-matter" % "0.11.1",
321-
"com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % "2.8.6",
330+
Dependencies.`jackson-dataformat-yaml`,
322331
"nl.big-o" % "liqp" % "0.6.7"
323332
)
324333
)
@@ -907,8 +916,14 @@ object DottyInjectedPlugin extends AutoPlugin {
907916
lazy val `sbt-dotty` = project.in(file("sbt-dotty")).
908917
settings(commonSettings).
909918
settings(
919+
// Keep in sync with inject-sbt-dotty.sbt
920+
libraryDependencies += Dependencies.`jackson-databind`,
921+
unmanagedSourceDirectories in Compile +=
922+
baseDirectory.value / "../language-server/src/dotty/tools/languageserver/config",
923+
924+
910925
sbtPlugin := true,
911-
version := "0.1.0-RC4",
926+
version := "0.1.0-RC5",
912927
ScriptedPlugin.scriptedSettings,
913928
ScriptedPlugin.sbtTestDirectory := baseDirectory.value / "sbt-test",
914929
ScriptedPlugin.scriptedBufferLog := false,

project/Dependencies.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import sbt._
2+
3+
/** A dependency shared between multiple projects should be put here
4+
* to ensure the same version of the dependency is used in all projects
5+
*/
6+
object Dependencies {
7+
private val jacksonVersion = "2.8.8"
8+
val `jackson-databind` =
9+
"com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion
10+
val `jackson-dataformat-yaml` =
11+
"com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % jacksonVersion
12+
}

project/inject-sbt-dotty.sbt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
// in our build instead of a released version.
44

55
unmanagedSourceDirectories in Compile += baseDirectory.value / "../sbt-dotty/src"
6+
7+
// Keep in sync with `sbt-dotty` config in Build.scala
8+
libraryDependencies += Dependencies.`jackson-databind`
9+
unmanagedSourceDirectories in Compile +=
10+
baseDirectory.value / "../language-server/src/dotty/tools/languageserver/config"

project/project/build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Some dependencies are shared between the regular build and the meta-build
2+
sources in Compile += baseDirectory.value / "../Dependencies.scala"
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package dotty.tools.sbtplugin
2+
3+
import sbt._
4+
import sbt.Keys._
5+
import java.io._
6+
import java.lang.ProcessBuilder
7+
import scala.collection.mutable
8+
9+
import dotty.tools.languageserver.config.ProjectConfig
10+
11+
import com.fasterxml.jackson.databind.ObjectMapper
12+
import scala.collection.mutable.ListBuffer
13+
import DottyPlugin.autoImport._
14+
15+
object DottyIDEPlugin extends AutoPlugin {
16+
// Adapted from scala-reflect
17+
private[this] def distinctBy[A, B](xs: Seq[A])(f: A => B): Seq[A] = {
18+
val buf = new mutable.ListBuffer[A]
19+
val seen = mutable.Set[B]()
20+
xs foreach { x =>
21+
val y = f(x)
22+
if (!seen(y)) {
23+
buf += x
24+
seen += y
25+
}
26+
}
27+
buf.toList
28+
}
29+
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
35+
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+
}
42+
}
43+
}.join
44+
}
45+
46+
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]("")
50+
51+
object autoImport {
52+
val prepareIDE = taskKey[Unit]("Prepare for IDE launch")
53+
val launchIDE = taskKey[Unit]("Run Visual Studio Code on this project")
54+
}
55+
56+
import autoImport._
57+
58+
override def requires: Plugins = plugins.JvmPlugin
59+
override def trigger = allRequirements
60+
61+
override def projectSettings: Seq[Setting[_]] = Seq(
62+
// Use Def.derive so `projectConfig` is only defined in the configurations where the
63+
// tasks/settings it depends on are defined.
64+
Def.derive(projectConfig := {
65+
if (sources.value.isEmpty) None
66+
else {
67+
val id = s"${thisProject.value.id}/${configuration.value.name}"
68+
val compilerVersion = scalaVersion.value
69+
.replace("-nonbootstrapped", "") // The language server is only published bootstrapped
70+
val compilerArguments = scalacOptions.value
71+
val sourceDirectories = unmanagedSourceDirectories.value ++ managedSourceDirectories.value
72+
val depClasspath = Attributed.data(dependencyClasspath.value)
73+
val classDir = classDirectory.value
74+
75+
Some(new ProjectConfig(
76+
id,
77+
compilerVersion,
78+
compilerArguments.toArray,
79+
sourceDirectories.toArray,
80+
depClasspath.toArray,
81+
classDir
82+
))
83+
}
84+
})
85+
)
86+
87+
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+
},
122+
123+
runCode := {
124+
val exitCode = new ProcessBuilder("code", "--install-extension", "lampepfl.dotty")
125+
.inheritIO()
126+
.start()
127+
.waitFor()
128+
if (exitCode != 0)
129+
throw new FeedbackProvidedException {
130+
override def toString = "Installing the Dotty support for VSCode failed"
131+
}
132+
133+
new ProcessBuilder("code", baseDirectory.value.getAbsolutePath)
134+
.inheritIO()
135+
.start()
136+
},
137+
138+
prepareIDE := {
139+
val x1 = configureIDE.value
140+
val x2 = compileForIDE.value
141+
},
142+
143+
launchIDE := runCode.dependsOn(prepareIDE).value
144+
)
145+
}

0 commit comments

Comments
 (0)