Skip to content

Commit

Permalink
Merge pull request #407 from spangaer/oss/jdt-settings
Browse files Browse the repository at this point in the history
Implement JDT compiler settings writing
  • Loading branch information
mkurz authored Dec 11, 2023
2 parents 9f2ee01 + 352bcce commit 2873967
Show file tree
Hide file tree
Showing 17 changed files with 344 additions and 1 deletion.
113 changes: 112 additions & 1 deletion src/main/scala/com/typesafe/sbteclipse/core/Eclipse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import EclipsePlugin.{
EclipseCreateSrc,
EclipseProjectFlavor,
EclipseExecutionEnvironment,
EclipseJDTMode,
EclipseKeys
}
import java.io.{ FileWriter, Writer }
Expand Down Expand Up @@ -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
Expand All @@ -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)] = {
Expand All @@ -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]],
Expand All @@ -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],
Expand All @@ -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))
Expand Down Expand Up @@ -184,6 +203,7 @@ private object Eclipse extends EclipseSDTConfig {

def handleProject(
jreContainer: String,
jdtMode: EclipseJDTMode.Value,
relativizeLibs: Boolean,
builderAndNatures: (String, Seq[String]),
state: State)(
Expand Down Expand Up @@ -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/<project>" -> "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
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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))

Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/com/typesafe/sbteclipse/core/EclipseOpts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ private object EclipseOpts {

val ExecutionEnvironment = "execution-environment"

val JDTMode = "jdt-mode"

val SkipParents = "skip-parents"

val WithSource = "with-source"
Expand Down
36 changes: 36 additions & 0 deletions src/main/scala/com/typesafe/sbteclipse/core/EclipsePlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ object EclipsePlugin {
withSource := true,
withJavadoc := true,
projectFlavor := EclipseProjectFlavor.ScalaIDE,
jdtMode := EclipseJDTMode.Ignore,
createSrc := EclipseCreateSrc.Default,
eclipseOutput := None,
relativizeLibs := true)
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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]] {
Expand Down
1 change: 1 addition & 0 deletions src/sbt-test/sbteclipse/08-jdt-settings/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!.settings
3 changes: 3 additions & 0 deletions src/sbt-test/sbteclipse/08-jdt-settings/b/expected
Original file line number Diff line number Diff line change
@@ -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
92 changes: 92 additions & 0 deletions src/sbt-test/sbteclipse/08-jdt-settings/build.sbt
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions src/sbt-test/sbteclipse/08-jdt-settings/c/expected
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions src/sbt-test/sbteclipse/08-jdt-settings/d/expected
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 2873967

Please sign in to comment.