diff --git a/src/main/scala/com/typesafe/sbteclipse/core/Eclipse.scala b/src/main/scala/com/typesafe/sbteclipse/core/Eclipse.scala index 71355ee..11e8be8 100644 --- a/src/main/scala/com/typesafe/sbteclipse/core/Eclipse.scala +++ b/src/main/scala/com/typesafe/sbteclipse/core/Eclipse.scala @@ -25,6 +25,7 @@ import EclipsePlugin.{ EclipseCreateSrc, EclipseProjectFlavor, EclipseExecutionEnvironment, + EclipseJDTMode, EclipseKeys } import java.io.{ FileWriter, Writer } @@ -64,6 +65,8 @@ import scalaz.{ Equal, NonEmptyList } import scalaz.Scalaz._ import scalaz.effect.IO import com.typesafe.sbteclipse.core.util.ScalaVersion +import java.io.FileReader +import java.io.Reader private object Eclipse extends EclipseSDTConfig { val SettingFormat = """-([^:]*):?(.*)""".r @@ -82,12 +85,16 @@ private object Eclipse extends EclipseSDTConfig { val JavaNature = "org.eclipse.jdt.core.javanature" + val JreContainerVersionSelector = """.*/.*/.*-([0-9.]+)""".r + def eclipseCommand(commandName: String): Command = Command(commandName)(_ => parser)((state, args) => action(args.toMap, state)) def parser: Parser[Seq[(String, Any)]] = { import EclipseOpts._ - (executionEnvironmentOpt | boolOpt(SkipParents) | boolOpt(WithSource) | boolOpt(WithJavadoc) | boolOpt(WithBundledScalaContainers)).* + (executionEnvironmentOpt | jdtModeOpt | + boolOpt(SkipParents) | boolOpt(WithSource) | boolOpt(WithJavadoc) | + boolOpt(WithBundledScalaContainers)).* } def executionEnvironmentOpt: Parser[(String, EclipseExecutionEnvironment.Value)] = { @@ -99,11 +106,21 @@ private object Eclipse extends EclipseSDTConfig { (Space ~> ExecutionEnvironment ~ ("=" ~> executionEnvironments)) map { case (k, v) => k -> withName(v) } } + def jdtModeOpt: Parser[(String, EclipseJDTMode.Value)] = { + import EclipseJDTMode._ + import EclipseOpts._ + import sbt.complete.DefaultParsers._ + val (head :: tail) = valueSeq map (_.toString) + val jdtModes = tail.foldLeft(head: Parser[String])(_ | _) + (Space ~> JDTMode ~ ("=" ~> jdtModes)) map { case (k, v) => k -> withName(v) } + } + def action(args: Map[String, Any], state: State): State = { state.log.info("About to create Eclipse project files for your project(s).") import EclipseOpts._ handleProjects( (args get ExecutionEnvironment).asInstanceOf[Option[EclipseExecutionEnvironment.Value]], + (args get JDTMode).asInstanceOf[Option[EclipseJDTMode.Value]], (args get SkipParents).asInstanceOf[Option[Boolean]] getOrElse skipParents(ThisBuild, state), (args get WithSource).asInstanceOf[Option[Boolean]], (args get WithJavadoc).asInstanceOf[Option[Boolean]], @@ -112,6 +129,7 @@ private object Eclipse extends EclipseSDTConfig { def handleProjects( executionEnvironmentArg: Option[EclipseExecutionEnvironment.Value], + jdtModeArg: Option[EclipseJDTMode.Value], skipParents: Boolean, withSourceArg: Option[Boolean], withJavadocArg: Option[Boolean], @@ -138,6 +156,7 @@ private object Eclipse extends EclipseSDTConfig { applic( handleProject( jreContainer(executionEnvironmentArg orElse executionEnvironment(ref, state)), + jdtModeArg getOrElse jdtMode(ref, state), relativizeLibs(ref, state), builderAndNatures(projectFlavor(ref, state)), state)) @@ -184,6 +203,7 @@ private object Eclipse extends EclipseSDTConfig { def handleProject( jreContainer: String, + jdtMode: EclipseJDTMode.Value, relativizeLibs: Boolean, builderAndNatures: (String, Seq[String]), state: State)( @@ -217,6 +237,7 @@ private object Eclipse extends EclipseSDTConfig { _ <- saveXml(baseDirectory / ".classpath", new RuleTransformer(classpathTransformers: _*)(cp)) _ <- saveProperties(baseDirectory / ".settings" / "org.eclipse.core.resources.prefs", Seq(("encoding/" -> "UTF-8"))) _ <- saveProperties(baseDirectory / ".settings" / "org.scala-ide.sdt.core.prefs", scalacOptions ++: compileOrder.map { order => Seq(("compileorder" -> order)) }.getOrElse(Nil)) + _ <- handleJDTSettings(jdtMode, baseDirectory, jreContainer) } yield n } @@ -386,6 +407,45 @@ private object Eclipse extends EclipseSDTConfig { case None => JreContainer } + def jreContainerToJdtCompilerSettings(jreContainer: String): Seq[(String, String)] = { + jreContainer match { + case JreContainerVersionSelector(version) => + Seq( + "org.eclipse.jdt.core.compiler.codegen.targetPlatform" -> version, + "org.eclipse.jdt.core.compiler.compliance" -> version, + "org.eclipse.jdt.core.compiler.source" -> version + ) + case _ => + Nil + } + } + + def handleJDTSettings( + mode: EclipseJDTMode.Value, + baseDirectory: File, + jreContainer: String + ): IO[Unit] = { + val jdtPrefs = baseDirectory / ".settings" / "org.eclipse.jdt.core.prefs" + + mode match { + case EclipseJDTMode.Ignore => + io(()) + case EclipseJDTMode.Remove => + fileExists(jdtPrefs).flatMap { + case false => + io(()) + case true => + io { + jdtPrefs.delete() + } + } + case EclipseJDTMode.Update => + updateProperties(jdtPrefs, jreContainerToJdtCompilerSettings(jreContainer)) + case EclipseJDTMode.Overwrite => + saveProperties(jdtPrefs, jreContainerToJdtCompilerSettings(jreContainer)) + } + } + def builderAndNatures(projectFlavor: EclipseProjectFlavor.Value) = if (projectFlavor.id == EclipseProjectFlavor.ScalaIDE.id) ScalaBuilder -> Seq(ScalaNature, JavaNature) @@ -546,6 +606,9 @@ private object Eclipse extends EclipseSDTConfig { def executionEnvironment(ref: Reference, state: State): Option[EclipseExecutionEnvironment.Value] = setting((ref / EclipseKeys.executionEnvironment), state) + def jdtMode(ref: Reference, state: State): EclipseJDTMode.Value = + setting((ref / EclipseKeys.jdtMode), state) + def skipParents(ref: Reference, state: State): Boolean = setting((ref / EclipseKeys.skipParents), state) @@ -602,6 +665,45 @@ private object Eclipse extends EclipseSDTConfig { } else io(()) + def updateProperties(file: File, settings: Seq[(String, String)]): IO[Unit] = + if (!settings.isEmpty) { + fileExists(file).flatMap { + case false => + saveProperties(file, settings) + case true => + fileReader(file) + .bracket(closeReader) { reader => + io { + val properties = new Properties + properties.load(reader) + properties + } + } + .flatMap { properties => + // only write if updates were made + val write = (for { + (key, value) <- settings + update = { + if (properties.getProperty(key) != value) { + properties.setProperty(key, value) + true + } else + false + } + } yield update).fold(false)(_ || _) + + if (write) { + fileWriter(file).bracket(closeWriter) { writer => + io(properties.store(writer, "Updated by sbteclipse")) + } + } else { + io(()) + } + } + } + } else + io(()) + def fileWriter(file: File): IO[FileWriter] = io(new FileWriter(file)) @@ -614,6 +716,15 @@ private object Eclipse extends EclipseSDTConfig { def closeWriter(writer: Writer): IO[Unit] = io(writer.close()) + def fileExists(file: File): IO[Boolean] = + io(file.exists()) + + def fileReader(file: File): IO[FileReader] = + io(new FileReader(file)) + + def closeReader(reader: Reader): IO[Unit] = + io(reader.close()) + private def io[T](t: => T): IO[T] = scalaz.effect.IO(t) // Utilities diff --git a/src/main/scala/com/typesafe/sbteclipse/core/EclipseOpts.scala b/src/main/scala/com/typesafe/sbteclipse/core/EclipseOpts.scala index 8a9be5b..4364d4f 100644 --- a/src/main/scala/com/typesafe/sbteclipse/core/EclipseOpts.scala +++ b/src/main/scala/com/typesafe/sbteclipse/core/EclipseOpts.scala @@ -22,6 +22,8 @@ private object EclipseOpts { val ExecutionEnvironment = "execution-environment" + val JDTMode = "jdt-mode" + val SkipParents = "skip-parents" val WithSource = "with-source" diff --git a/src/main/scala/com/typesafe/sbteclipse/core/EclipsePlugin.scala b/src/main/scala/com/typesafe/sbteclipse/core/EclipsePlugin.scala index 9dca61b..47e039b 100644 --- a/src/main/scala/com/typesafe/sbteclipse/core/EclipsePlugin.scala +++ b/src/main/scala/com/typesafe/sbteclipse/core/EclipsePlugin.scala @@ -72,6 +72,7 @@ object EclipsePlugin { withSource := true, withJavadoc := true, projectFlavor := EclipseProjectFlavor.ScalaIDE, + jdtMode := EclipseJDTMode.Ignore, createSrc := EclipseCreateSrc.Default, eclipseOutput := None, relativizeLibs := true) @@ -175,6 +176,10 @@ object EclipsePlugin { prefix("project-flavor"), "The flavor of project (Scala or Java) to build.") + val jdtMode: SettingKey[EclipseJDTMode.Value] = SettingKey( + prefix("jdt-mode"), + "How to handle setting Java compiler target in org.eclipse.jdt.core.prefs (Ignore, Remove, Update, Overwrite).") + val eclipseOutput: SettingKey[Option[String]] = SettingKey( prefix("eclipse-output"), "The optional output for Eclipse.") @@ -333,6 +338,37 @@ object EclipsePlugin { def createTransformer(ref: ProjectRef, state: State): Validation[A] } + object EclipseJDTMode extends Enumeration { + + /** + * Do not touch the the .prefs file at all. + */ + val Ignore = Value + + /** + * If the file exists, remove it. + * Allows cleansing all JDT settings that got written by e.g. the LSP. + */ + val Remove = Value + + /** + * Write the Java compiler target settings, but maintain any other settings. + */ + val Update = Value + + /** + * Write a new file with only the Java compiler target settings. + * In a VSCode context, this makes the compiler settings work correctly but + * protects against e.g. outdated formatter settings (which the LSP injects) + * persisting. + * After LSP restart formatter settings will return, but are refreshed from + * the xml profile instead. + */ + val Overwrite = Value + + val valueSeq: Seq[Value] = Ignore :: Remove :: Update :: Overwrite :: Nil + } + object EclipseClasspathEntryTransformerFactory { object Identity extends EclipseTransformerFactory[Seq[EclipseClasspathEntry] => Seq[EclipseClasspathEntry]] { diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/.gitignore b/src/sbt-test/sbteclipse/08-jdt-settings/.gitignore new file mode 100644 index 0000000..5fcbd56 --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/.gitignore @@ -0,0 +1 @@ +!.settings diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/b/expected b/src/sbt-test/sbteclipse/08-jdt-settings/b/expected new file mode 100644 index 0000000..f736ce2 --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/b/expected @@ -0,0 +1,3 @@ +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.source=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/build.sbt b/src/sbt-test/sbteclipse/08-jdt-settings/build.sbt new file mode 100644 index 0000000..77bcea7 --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/build.sbt @@ -0,0 +1,92 @@ + +val check = TaskKey[Unit]("check") := { + import java.util.Properties + import java.io.FileInputStream + import scala.collection.JavaConverters._ + + val s: TaskStreams = streams.value + val expectedFile = baseDirectory.value / "expected" + val resultFile = baseDirectory.value / ".settings" / "org.eclipse.jdt.core.prefs" + + if (expectedFile.exists()) { + val expectedIn = new FileInputStream(expectedFile) + val expected = + try { + val prop = new Properties() + prop.load(expectedIn) + prop.asScala.toMap + } finally { + expectedIn.close() + } + + val resultIn = new FileInputStream(resultFile) + val result = + try { + val prop = new Properties() + prop.load(resultIn) + prop.asScala.toMap + } finally { + resultIn.close() + } + + if (expected == result) + s.log.info(s"correct data: ${resultFile}") + else + sys.error("Expected settings to be '%s', but was '%s'!".format(expected, result)) + } +} + +// ensure org.eclipse.core.resources.prefs will always be generated +ThisBuild / scalacOptions ++= Seq("-encoding", "utf-8") + +// check that no JDT file is generated (default ignore, no runtime defined) +lazy val projectA = (project in file("a")) + .settings( + check + ) + +// check that a new and correct JDT file is generated +lazy val projectB = (project in file("b")) + .settings( + EclipseKeys.executionEnvironment := Some(EclipseExecutionEnvironment.JavaSE18), + EclipseKeys.jdtMode := EclipseJDTMode.Update, + check + ) + +// check that a correct JDT file is is not updated +lazy val projectC = (project in file("c")) + .settings( + EclipseKeys.executionEnvironment := Some(EclipseExecutionEnvironment.JavaSE11), + EclipseKeys.jdtMode := EclipseJDTMode.Update, + check + ) + +// check that an outdated JDT file is selectively updated +lazy val projectD = (project in file("d")) + .settings( + EclipseKeys.executionEnvironment := Some(EclipseExecutionEnvironment.JavaSE_17), + EclipseKeys.jdtMode := EclipseJDTMode.Update, + check + ) + +// check that a JDT file is overwritten +lazy val projectE = (project in file("e")) + .settings( + EclipseKeys.executionEnvironment := Some(EclipseExecutionEnvironment.JavaSE11), + EclipseKeys.jdtMode := EclipseJDTMode.Overwrite, + check + ) + +// check that an JDT file is removed +lazy val projectF = (project in file("f")) + .settings( + EclipseKeys.jdtMode := EclipseJDTMode.Remove, + check + ) + +// check that an JDT file is default ignored, but written on command +lazy val projectG = (project in file("g")) + .settings( + EclipseKeys.executionEnvironment := Some(EclipseExecutionEnvironment.JavaSE18), + check + ) diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/c/.settings/org.eclipse.jdt.core.prefs b/src/sbt-test/sbteclipse/08-jdt-settings/c/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..7fc5c6f --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/c/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.compliance=11 +dummy.key=abc diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/c/expected b/src/sbt-test/sbteclipse/08-jdt-settings/c/expected new file mode 100644 index 0000000..7fc5c6f --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/c/expected @@ -0,0 +1,4 @@ +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.compliance=11 +dummy.key=abc diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/d/.settings/org.eclipse.jdt.core.prefs b/src/sbt-test/sbteclipse/08-jdt-settings/d/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..7fc5c6f --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/d/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.compliance=11 +dummy.key=abc diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/d/expected b/src/sbt-test/sbteclipse/08-jdt-settings/d/expected new file mode 100644 index 0000000..23a0c69 --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/d/expected @@ -0,0 +1,4 @@ +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.compiler.compliance=17 +dummy.key=abc diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/e/.settings/org.eclipse.jdt.core.prefs b/src/sbt-test/sbteclipse/08-jdt-settings/e/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..7fc5c6f --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/e/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.compliance=11 +dummy.key=abc diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/e/expected b/src/sbt-test/sbteclipse/08-jdt-settings/e/expected new file mode 100644 index 0000000..3bc34e2 --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/e/expected @@ -0,0 +1,3 @@ +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.compliance=11 diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/f/.settings/org.eclipse.jdt.core.prefs b/src/sbt-test/sbteclipse/08-jdt-settings/f/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..7fc5c6f --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/f/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.compliance=11 +dummy.key=abc diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/g/.settings/org.eclipse.jdt.core.prefs b/src/sbt-test/sbteclipse/08-jdt-settings/g/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..ccb0942 --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/g/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1 @@ +dummy.key=abc diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/g/expected b/src/sbt-test/sbteclipse/08-jdt-settings/g/expected new file mode 100644 index 0000000..ccb0942 --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/g/expected @@ -0,0 +1 @@ +dummy.key=abc diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/project/plugins.sbt b/src/sbt-test/sbteclipse/08-jdt-settings/project/plugins.sbt new file mode 100644 index 0000000..542d181 --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/project/plugins.sbt @@ -0,0 +1,7 @@ +{ + val pluginVersion = System.getProperty("plugin.version") + if(pluginVersion == null) + throw new RuntimeException("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) + else addSbtPlugin("com.github.sbt" % "sbt-eclipse" % pluginVersion) +} diff --git a/src/sbt-test/sbteclipse/08-jdt-settings/test b/src/sbt-test/sbteclipse/08-jdt-settings/test new file mode 100644 index 0000000..2e764ee --- /dev/null +++ b/src/sbt-test/sbteclipse/08-jdt-settings/test @@ -0,0 +1,62 @@ +# timestamp marker file +$ touch marker + +$ exists f/.settings/org.eclipse.jdt.core.prefs + +> eclipse + +> check + +$ exists a/.settings/org.eclipse.core.resources.prefs +-$ exists a/.settings/org.eclipse.jdt.core.prefs + +$ exists b/.settings/org.eclipse.jdt.core.prefs +$ newer b/.settings/org.eclipse.jdt.core.prefs marker + +$ exists c/.settings/org.eclipse.jdt.core.prefs +$ newer marker c/.settings/org.eclipse.jdt.core.prefs + +$ exists d/.settings/org.eclipse.jdt.core.prefs +$ newer d/.settings/org.eclipse.jdt.core.prefs marker + +$ exists e/.settings/org.eclipse.jdt.core.prefs +$ newer e/.settings/org.eclipse.jdt.core.prefs marker + +$ exists f/.settings/org.eclipse.core.resources.prefs +-$ exists f/.settings/org.eclipse.jdt.core.prefs + +$ exists g/.settings/org.eclipse.jdt.core.prefs +$ newer marker g/.settings/org.eclipse.jdt.core.prefs + +# test overwrite mode via command arg +$ touch marker +> eclipse jdt-mode=Overwrite +# no runtime defined for a +-$ exists a/.settings/org.eclipse.jdt.core.prefs +$ newer b/.settings/org.eclipse.jdt.core.prefs marker +$ newer c/.settings/org.eclipse.jdt.core.prefs marker +$ newer d/.settings/org.eclipse.jdt.core.prefs marker +$ newer e/.settings/org.eclipse.jdt.core.prefs marker +-$ exists f/.settings/org.eclipse.jdt.core.prefs +$ newer g/.settings/org.eclipse.jdt.core.prefs marker + +# test ignore mode via command arg +$ touch marker +> eclipse jdt-mode=Ignore +-$ exists a/.settings/org.eclipse.jdt.core.prefs +$ newer marker b/.settings/org.eclipse.jdt.core.prefs +$ newer marker c/.settings/org.eclipse.jdt.core.prefs +$ newer marker d/.settings/org.eclipse.jdt.core.prefs +$ newer marker e/.settings/org.eclipse.jdt.core.prefs +-$ exists f/.settings/org.eclipse.jdt.core.prefs +$ newer marker g/.settings/org.eclipse.jdt.core.prefs + +# test remove mode via command arg +> eclipse jdt-mode=Remove +-$ exists a/.settings/org.eclipse.jdt.core.prefs +-$ exists b/.settings/org.eclipse.jdt.core.prefs +-$ exists c/.settings/org.eclipse.jdt.core.prefs +-$ exists d/.settings/org.eclipse.jdt.core.prefs +-$ exists e/.settings/org.eclipse.jdt.core.prefs +-$ exists f/.settings/org.eclipse.jdt.core.prefs +-$ exists g/.settings/org.eclipse.jdt.core.prefs