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