From 4bf5efe44a490b6ec5536f3443738e22f9f87e43 Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Thu, 2 Sep 2021 12:24:50 +0100 Subject: [PATCH 1/8] Initial Java support --- build.sbt | 4 + .../scala/meta/internal/builds/Digest.scala | 4 +- .../implementation/GlobalClassTable.scala | 4 +- .../metals/BuildServerConnection.scala | 39 ++- .../meta/internal/metals/BuildTargets.scala | 149 ++++++++++-- .../meta/internal/metals/CommonTarget.scala | 17 ++ .../meta/internal/metals/Compilers.scala | 2 +- .../scala/meta/internal/metals/Doctor.scala | 99 +++++--- .../meta/internal/metals/DoctorResults.scala | 4 +- .../internal/metals/FileDecoderProvider.scala | 62 ++++- .../metals/FileSystemSemanticdbs.scala | 11 +- .../internal/metals/FormattingProvider.scala | 4 +- .../meta/internal/metals/ImportedBuild.scala | 10 + .../metals/JavaFormattingProvider.scala | 230 ++++++++++++++++++ .../meta/internal/metals/JavaTarget.scala | 34 +++ .../internal/metals/MetalsBuildServer.scala | 5 +- .../internal/metals/MetalsEnrichments.scala | 96 +++++++- .../metals/MetalsLanguageServer.scala | 62 +++-- .../internal/metals/PackageProvider.scala | 7 +- .../meta/internal/metals/ScalaTarget.scala | 49 ++-- .../metals/ScalaVersionSelector.scala | 2 +- .../internal/metals/ScalafixProvider.scala | 10 +- .../internal/metals/SemanticdbIndexer.scala | 10 + .../meta/internal/metals/ServerCommands.scala | 14 ++ .../internal/metals/UserConfiguration.scala | 42 +++- .../scala/meta/internal/metals/Warnings.scala | 8 +- .../internal/metals/debug/DebugProvider.scala | 6 +- .../metals/newScalaFile/NewFileProvider.scala | 89 +++++-- .../metals/newScalaFile/NewFileTypes.scala | 53 +++- .../internal/metals/watcher/FileWatcher.scala | 10 +- .../internal/troubleshoot/JavaProblem.scala | 44 ++++ .../troubleshoot/ProblemResolver.scala | 58 ++++- .../internal/tvp/MetalsTreeViewProvider.scala | 24 +- .../mtags/CommonMtagsEnrichments.scala | 7 + .../internal/mtags/SemanticdbClasspath.scala | 24 +- .../meta/internal/mtags/Semanticdbs.scala | 14 +- .../test/scala/tests/sbt/SbtServerSuite.scala | 2 +- .../scala/tests/MetalsTestEnrichments.scala | 1 + .../src/main/scala/tests/TestingServer.scala | 2 +- .../tests/DefinitionDirectorySuite.scala | 18 +- .../test/scala/tests/NewFileLspSuite.scala | 175 ++++++++++++- .../test/scala/tests/TreeViewLspSuite.scala | 2 +- 42 files changed, 1265 insertions(+), 242 deletions(-) create mode 100644 metals/src/main/scala/scala/meta/internal/metals/CommonTarget.scala create mode 100644 metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala create mode 100644 metals/src/main/scala/scala/meta/internal/metals/JavaTarget.scala create mode 100644 metals/src/main/scala/scala/meta/internal/troubleshoot/JavaProblem.scala diff --git a/build.sbt b/build.sbt index 8a96e6c0e8a..16623578417 100644 --- a/build.sbt +++ b/build.sbt @@ -270,6 +270,7 @@ lazy val V = new { val bloop = "1.4.11-19-93ebe2c6" val scala3 = "3.1.0" val nextScala3RC = "3.1.1-RC1" + val javaSemanticdb = "0.7.2" val bloopNightly = bloop val sbtBloop = bloop val gradleBloop = bloop @@ -538,6 +539,8 @@ lazy val metals = project "com.thoughtworks.qdox" % "qdox" % "2.0.1", // for finding paths of global log/cache directories "dev.dirs" % "directories" % "26", + // for Java formatting + "org.eclipse.jdt" % "org.eclipse.jdt.core" % "3.26.0", // ================== // Scala dependencies // ================== @@ -578,6 +581,7 @@ lazy val metals = project "mavenBloopVersion" -> V.mavenBloop, "scalametaVersion" -> V.scalameta, "semanticdbVersion" -> V.semanticdb, + "javaSemanticdbVersion" -> V.javaSemanticdb, "scalafmtVersion" -> V.scalafmt, "ammoniteVersion" -> V.ammonite, "organizeImportVersion" -> V.organizeImportRule, diff --git a/metals/src/main/scala/scala/meta/internal/builds/Digest.scala b/metals/src/main/scala/scala/meta/internal/builds/Digest.scala index 833c3fe6beb..85f8ebe3b2b 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/Digest.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/Digest.scala @@ -84,7 +84,7 @@ object Digest { val ext = PathIO.extension(path.toNIO) val isScala = Set("sbt", "scala", "sc")(ext) // we can have both gradle and gradle.kts and build plugins can be written in any of three languages - val isGradle = + val isGeneralJVM = Set("gradle", "groovy", "gradle.kts", "java", "kts").exists( path.toString().endsWith(_) ) @@ -92,7 +92,7 @@ object Digest { if (isScala && path.isFile) { digestScala(path, digest) - } else if (isGradle && path.isFile) { + } else if (isGeneralJVM && path.isFile) { digestGeneralJvm(path, digest) } else if (isXml) { digestXml(path, digest) diff --git a/metals/src/main/scala/scala/meta/internal/implementation/GlobalClassTable.scala b/metals/src/main/scala/scala/meta/internal/implementation/GlobalClassTable.scala index b579508c073..35e5e72382d 100644 --- a/metals/src/main/scala/scala/meta/internal/implementation/GlobalClassTable.scala +++ b/metals/src/main/scala/scala/meta/internal/implementation/GlobalClassTable.scala @@ -40,8 +40,8 @@ final class GlobalClassTable( synchronized { for { buildTargetId <- buildTargets.inverseSources(source) - scalaTarget <- buildTargets.scalaTarget(buildTargetId) - classpath = new Classpath(scalaTarget.jarClasspath) + jarClasspath <- buildTargets.targetJarClasspath(buildTargetId) + classpath = new Classpath(jarClasspath) } yield { buildTargetsIndexes.getOrElseUpdate( buildTargetId, diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala index a54d8a2959f..d067ccbe9dd 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala @@ -3,7 +3,6 @@ package scala.meta.internal.metals import java.io.IOException import java.io.InputStream import java.net.URI -import java.util.Collections import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException @@ -160,10 +159,28 @@ class BuildServerConnection private ( register(server => server.workspaceBuildTargets()).asScala } + def buildTargetJavacOptions( + params: JavacOptionsParams + ): Future[JavacOptionsResult] = { + val resultOnJavacOptionsUnsupported = new JavacOptionsResult( + List.empty[JavacOptionsItem].asJava + ) + val onFail = Some( + (resultOnJavacOptionsUnsupported, "Java targets not supported by server") + ) + register(server => server.buildTargetJavacOptions(params), onFail).asScala + } + def buildTargetScalacOptions( params: ScalacOptionsParams ): Future[ScalacOptionsResult] = { - register(server => server.buildTargetScalacOptions(params)).asScala + val resultOnScalaOptionsUnsupported = new ScalacOptionsResult( + List.empty[ScalacOptionsItem].asJava + ) + val onFail = Some( + (resultOnScalaOptionsUnsupported, "Scala targets not supported by server") + ) + register(server => server.buildTargetScalacOptions(params), onFail).asScala } def buildTargetSources(params: SourcesParams): Future[SourcesResult] = { @@ -228,7 +245,8 @@ class BuildServerConnection private ( } private def register[T: ClassTag]( - action: MetalsBuildServer => CompletableFuture[T] + action: MetalsBuildServer => CompletableFuture[T], + onFail: => Option[(T, String)] = None ): CompletableFuture[T] = { val original = connection val actionFuture = original @@ -248,8 +266,15 @@ class BuildServerConnection private ( } case t if implicitly[ClassTag[T]].runtimeClass.getSimpleName != "Object" => - val name = implicitly[ClassTag[T]].runtimeClass.getSimpleName - Future.failed(MetalsBspException(name, t.getMessage)) + onFail + .map { case (defaultResult, message) => + scribe.info(message) + Future.successful(defaultResult) + } + .getOrElse({ + val name = implicitly[ClassTag[T]].runtimeClass.getSimpleName + Future.failed(MetalsBspException(name, t.getMessage)) + }) } CancelTokens.future(_ => actionFuture) } @@ -345,6 +370,7 @@ object BuildServerConnection { } final case class BspExtraBuildParams( + javaSemanticdbVersion: String, semanticdbVersion: String, supportedScalaVersions: java.util.List[String] ) @@ -358,6 +384,7 @@ object BuildServerConnection { serverName: String ): InitializeBuildResult = { val extraParams = BspExtraBuildParams( + BuildInfo.javaSemanticdbVersion, BuildInfo.scalametaVersion, BuildInfo.supportedScala2Versions.asJava ) @@ -369,7 +396,7 @@ object BuildServerConnection { BuildInfo.bspVersion, workspace.toURI.toString, new BuildClientCapabilities( - Collections.singletonList("scala") + List("scala", "java").asJava ) ) val gson = new Gson diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala index 684aba28f6f..8cdc570a367 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala @@ -20,6 +20,8 @@ import scala.meta.io.AbsolutePath import ch.epfl.scala.bsp4j.BuildTarget import ch.epfl.scala.bsp4j.BuildTargetIdentifier +import ch.epfl.scala.bsp4j.JavacOptionsItem +import ch.epfl.scala.bsp4j.JavacOptionsResult import ch.epfl.scala.bsp4j.ScalaBuildTarget import ch.epfl.scala.bsp4j.ScalacOptionsItem import ch.epfl.scala.bsp4j.ScalacOptionsResult @@ -40,6 +42,8 @@ final class BuildTargets( TrieMap.empty[AbsolutePath, ConcurrentLinkedQueue[BuildTargetIdentifier]] private val buildTargetInfo = TrieMap.empty[BuildTargetIdentifier, BuildTarget] + private val javacTargetInfo = + TrieMap.empty[BuildTargetIdentifier, JavacOptionsItem] private val scalacTargetInfo = TrieMap.empty[BuildTargetIdentifier, ScalacOptionsItem] private val inverseDependencies = @@ -68,6 +72,9 @@ final class BuildTargets( ) if (isSupportedScalaVersion) score <<= 2 + val usesJavac = javacOptions(t).nonEmpty + if (usesJavac) score <<= 1 + val isJVM = scalacOptions(t).exists(_.isJVM) if (isJVM) score <<= 1 @@ -88,6 +95,7 @@ final class BuildTargets( sourceItemsToBuildTarget.values.foreach(_.clear()) sourceItemsToBuildTarget.clear() buildTargetInfo.clear() + javacTargetInfo.clear() scalacTargetInfo.clear() inverseDependencies.clear() buildTargetSources.clear() @@ -95,27 +103,74 @@ final class BuildTargets( sourceJarNameToJarFile.clear() isSourceRoot.clear() } - def sourceItems: Iterable[AbsolutePath] = - sourceItemsToBuildTarget.keys + def sourceItems: Iterable[AbsolutePath] = sourceItemsToBuildTarget.keys def sourceItemsToBuildTargets : Iterator[(AbsolutePath, JIterable[BuildTargetIdentifier])] = sourceItemsToBuildTarget.iterator - def scalacOptions: Iterable[ScalacOptionsItem] = + + private def scalacOptions: Iterable[ScalacOptionsItem] = scalacTargetInfo.values + private def javacOptions: Iterable[JavacOptionsItem] = javacTargetInfo.values - def allBuildTargetIds: Seq[BuildTargetIdentifier] = - all.toSeq.map(_.info.getId()) - def all: Iterator[ScalaTarget] = - for { - (_, target) <- buildTargetInfo.iterator - scalaTarget <- toScalaTarget(target) - } yield scalaTarget + def allTargets: Iterator[BuildTarget] = buildTargetInfo.values.iterator + + def allBuildTargetIds: Seq[BuildTargetIdentifier] = buildTargetInfo.keys.toSeq + + def allTargetRoots: Iterator[AbsolutePath] = { + val scalaTargetRoots = for { + item <- scalacOptions + scalaInfo <- scalaInfo(item.getTarget) + } yield (item.targetroot(scalaInfo.getScalaVersion)) + val javaTargetRoots = javacOptions.map(_.targetroot) + val allTargetRoots = scalaTargetRoots.toSet ++ javaTargetRoots.toSet + allTargetRoots.iterator + } + + def allCommon: Iterator[CommonTarget] = + allTargets.map(CommonTarget.apply) + + def allScala: Iterator[ScalaTarget] = + allTargets.flatMap(toScalaTarget) + + def allJava: Iterator[JavaTarget] = + allTargets.flatMap(toJavaTarget) + + def commonTarget(id: BuildTargetIdentifier): Option[CommonTarget] = + buildTargetInfo.get(id).map(CommonTarget.apply) def scalaTarget(id: BuildTargetIdentifier): Option[ScalaTarget] = - for { - target <- buildTargetInfo.get(id) - scalaTarget <- toScalaTarget(target) - } yield scalaTarget + buildTargetInfo.get(id).flatMap(toScalaTarget) + + def javaTarget(id: BuildTargetIdentifier): Option[JavaTarget] = + buildTargetInfo.get(id).flatMap(toJavaTarget) + + def targetJarClasspath( + id: BuildTargetIdentifier + ): Option[List[AbsolutePath]] = { + val scalacData = scalacTargetInfo.get(id).map(_.jarClasspath) + val javacData = javacTargetInfo.get(id).map(_.jarClasspath) + scalacData + .flatMap(s => javacData.map(j => (s ::: j).distinct).orElse(scalacData)) + .orElse(javacData) + } + + private def targetClasspath( + id: BuildTargetIdentifier + ): Option[List[String]] = { + val scalacData = scalacTargetInfo.get(id).map(_.classpath) + val javacData = javacTargetInfo.get(id).map(_.classpath) + scalacData + .flatMap(s => javacData.map(j => (s ::: j).distinct).orElse(scalacData)) + .orElse(javacData) + } + + def targetClassDirectories( + id: BuildTargetIdentifier + ): List[String] = { + val scalacData = scalacTargetInfo.get(id).map(_.getClassDirectory).toList + val javacData = javacTargetInfo.get(id).map(_.getClassDirectory).toList + (scalacData ++ javacData).distinct + } private def toScalaTarget(target: BuildTarget): Option[ScalaTarget] = { for { @@ -128,18 +183,25 @@ final class BuildTargets( scalaTarget, scalac, autoImports, - target.getDataKind() == "sbt" + target.isSbtBuild ) } } + private def toJavaTarget(target: BuildTarget): Option[JavaTarget] = { + for { + javac <- javacTargetInfo.get(target.getId) + } yield JavaTarget(target, javac) + } + def allWorkspaceJars: Iterator[AbsolutePath] = { val isVisited = new ju.HashSet[AbsolutePath]() + Iterator( for { - target <- all - classpathEntry <- target.scalac.classpath - if classpathEntry.isJar + targetId <- allBuildTargetIds + classpathEntries <- targetJarClasspath(targetId).toList + classpathEntry <- classpathEntries if isVisited.add(classpathEntry) } yield classpathEntry, PackageIndex.bootClasspath.iterator @@ -234,6 +296,12 @@ final class BuildTargets( } } + def addJavacOptions(result: JavacOptionsResult): Unit = { + result.getItems.asScala.foreach { item => + javacTargetInfo(item.getTarget) = item + } + } + def info( buildTarget: BuildTargetIdentifier ): Option[BuildTarget] = @@ -243,11 +311,40 @@ final class BuildTargets( ): Option[ScalaBuildTarget] = info(buildTarget).flatMap(_.asScalaBuildTarget) + def targetRoots( + buildTarget: BuildTargetIdentifier + ): List[AbsolutePath] = { + val javaRoot = javaTargetRoot(buildTarget).toList + val scalaRoot = scalaTargetRoot(buildTarget).toList + (javaRoot ++ scalaRoot).distinct + } + + def javaTargetRoot( + buildTarget: BuildTargetIdentifier + ): Option[AbsolutePath] = + javacTargetInfo.get(buildTarget).map(_.targetroot) + + def scalaTargetRoot( + buildTarget: BuildTargetIdentifier + ): Option[AbsolutePath] = + scalacTargetInfo + .get(buildTarget) + .flatMap(scalacOptions => { + scalaInfo(scalacOptions.getTarget).map(scalaBuildTarget => + scalacOptions.targetroot(scalaBuildTarget.getScalaVersion) + ) + }) + def scalacOptions( buildTarget: BuildTargetIdentifier ): Option[ScalacOptionsItem] = scalacTargetInfo.get(buildTarget) + def javacOptions( + buildTarget: BuildTargetIdentifier + ): Option[JavacOptionsItem] = + javacTargetInfo.get(buildTarget) + def workspaceDirectory( buildTarget: BuildTargetIdentifier ): Option[AbsolutePath] = @@ -328,11 +425,10 @@ final class BuildTargets( // else it can be a source file inside a jar val fromJar = jarPath(source) .flatMap { jar => - all.find { scalaTarget => - scalaTarget.jarClasspath.contains(jar) + allBuildTargetIds.find { id => + targetJarClasspath(id).exists(_.contains(jar)) } } - .map(_.id) fromJar.foreach(addSourceItem(source, _)) fromJar } @@ -362,7 +458,7 @@ final class BuildTargets( if (file.isSbt) file.parent.resolve("project") else file.parent buildTargetInfo.values .find { target => - val isMetaBuild = target.getDataKind == "sbt" + val isMetaBuild = target.isSbtBuild if (isMetaBuild) { workspaceDirectory(target.getId) .map(_ == targetMetaBuildDir) @@ -386,8 +482,13 @@ final class BuildTargets( allWorkspaceJars.map(_.toNIO.toUri().toURL()).toArray, null ) - lazy val classpaths = - all.map(i => i.id -> i.scalac.classpath.toSeq).toSeq + lazy val classpaths: Seq[(BuildTargetIdentifier, Seq[AbsolutePath])] = + allBuildTargetIds.map(id => + id -> targetClasspath(id) + .map(_.toAbsoluteClasspath.toSeq) + .getOrElse(Seq.empty) + ) + try { toplevels.foldLeft(Option.empty[InferredBuildTarget]) { case (Some(x), _) => Some(x) diff --git a/metals/src/main/scala/scala/meta/internal/metals/CommonTarget.scala b/metals/src/main/scala/scala/meta/internal/metals/CommonTarget.scala new file mode 100644 index 00000000000..61d36889fac --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/CommonTarget.scala @@ -0,0 +1,17 @@ +package scala.meta.internal.metals + +import scala.meta.internal.metals.MetalsEnrichments._ + +import ch.epfl.scala.bsp4j.BuildTarget +import ch.epfl.scala.bsp4j.BuildTargetIdentifier + +case class CommonTarget(val info: BuildTarget) { + + def id: BuildTargetIdentifier = info.getId() + + def dataKind: String = info.dataKind + + def baseDirectory: String = info.baseDirectory + + def displayName: String = info.getDisplayName() +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala index 32a204f3b81..2710a115779 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala @@ -702,7 +702,7 @@ class Compilers( mtags: MtagsBinaries, search: SymbolSearch ): PresentationCompiler = { - val classpath = scalac.classpath.map(_.toNIO).toSeq + val classpath = scalac.classpath.toAbsoluteClasspath.map(_.toNIO).toSeq newCompiler(scalac, target, mtags, classpath, search) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/Doctor.scala b/metals/src/main/scala/scala/meta/internal/metals/Doctor.scala index b9081f3d119..f553e05e800 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Doctor.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Doctor.scala @@ -19,6 +19,8 @@ import scala.meta.internal.metals.config.DoctorFormat import scala.meta.internal.troubleshoot.ProblemResolver import scala.meta.io.AbsolutePath +import ch.epfl.scala.bsp4j.BuildTargetIdentifier + /** * Helps the user figure out what is mis-configured in the build through the "Run doctor" command. * @@ -124,7 +126,9 @@ final class Doctor( * Checks if there are any potential problems and if any, notifies the user. */ def check(): Unit = { - val summary = problemResolver.problemMessage(allTargets()) + val scalaTargets = buildTargets.allScala.toList + val javaTargets = buildTargets.allJava.toList + val summary = problemResolver.problemMessage(scalaTargets, javaTargets) executeReloadDoctor(summary) summary match { case Some(problem) => @@ -147,7 +151,8 @@ final class Doctor( } } - def allTargets(): List[ScalaTarget] = buildTargets.all.toList + private def allTargetIds(): Seq[BuildTargetIdentifier] = + buildTargets.allBuildTargetIds private def selectedBuildToolMessage(): Option[String] = { tables.buildTool.selectedBuildTool().map { value => @@ -213,7 +218,7 @@ final class Doctor( } private def buildTargetsJson(): String = { - val targets = allTargets() + val targetIds = allTargetIds() val buildToolHeading = selectedBuildToolMessage() val (buildServerHeading, _) = selectedBuildServerMessage() val importBuildHeading = selectedImportBuildMessage() @@ -226,7 +231,7 @@ final class Doctor( ).flatten .mkString("\n\n") - val results = if (targets.isEmpty) { + val results = if (targetIds.isEmpty) { DoctorResults( doctorTitle, heading, @@ -240,10 +245,10 @@ final class Doctor( ), None ).toJson - } else { - val targetResults = - targets.sortBy(_.baseDirectory).map(extractTargetInfo) + val targetResults = targetIds + .flatMap(extractTargetInfo) + .sortBy(f => (f.baseDirectory, f.name, f.dataKind)) DoctorResults(doctorTitle, heading, None, Some(targetResults)).toJson } ujson.write(results) @@ -299,8 +304,8 @@ final class Doctor( _.text(doctorHeading) ) - val targets = allTargets() - if (targets.isEmpty) { + val targetIds = allTargetIds() + if (targetIds.isEmpty) { html .element("p")( _.text(noBuildTargetsTitle) @@ -325,33 +330,43 @@ final class Doctor( .element("th")(_.text("Find references")) .element("th")(_.text("Recommendation")) ) - ).element("tbody")(html => buildTargetRows(html, targets)) + ).element("tbody")(html => buildTargetRows(html, targetIds)) ) } } private def buildTargetRows( html: HtmlBuilder, - targets: List[ScalaTarget] + targetIds: Seq[BuildTargetIdentifier] ): Unit = { - targets.sortBy(_.baseDirectory).foreach { target => - val targetInfo = extractTargetInfo(target) - val center = "style='text-align: center'" - html.element("tr")( - _.element("td")(_.text(targetInfo.name)) - .element("td")(_.text(targetInfo.scalaVersion)) - .element("td", center)(_.text(Icons.unicode.check)) - .element("td", center)(_.text(targetInfo.definitionStatus)) - .element("td", center)(_.text(targetInfo.completionsStatus)) - .element("td", center)(_.text(targetInfo.referencesStatus)) - .element("td")( - _.raw(targetInfo.recommenedFix) - ) - ) - } + targetIds + .flatMap(extractTargetInfo) + .sortBy(f => (f.baseDirectory, f.name, f.dataKind)) + .foreach { targetInfo => + val center = "style='text-align: center'" + html.element("tr")( + _.element("td")(_.text(targetInfo.name)) + .element("td")(_.text(targetInfo.scalaVersion)) + .element("td", center)(_.text(Icons.unicode.check)) + .element("td", center)(_.text(targetInfo.definitionStatus)) + .element("td", center)(_.text(targetInfo.completionsStatus)) + .element("td", center)(_.text(targetInfo.referencesStatus)) + .element("td")(_.raw(targetInfo.recommenedFix)) + ) + } } - private def extractTargetInfo(target: ScalaTarget) = { + private def extractTargetInfo( + targetId: BuildTargetIdentifier + ): List[DoctorTargetInfo] = { + val scalaDoctorInfo = + buildTargets.scalaTarget(targetId).map(extractScalaTargetInfo).toList + val javaDoctorInfo = + buildTargets.javaTarget(targetId).map(extractJavaTargetInfo).toList + scalaDoctorInfo ::: javaDoctorInfo + } + + private def extractScalaTargetInfo(target: ScalaTarget) = { val scalaVersion = target.scalaVersion val definition: String = if (mtagsResolver.isSupportedScalaVersion(scalaVersion)) { @@ -367,14 +382,40 @@ final class Doctor( } else { Icons.unicode.check } - val recommenedFix = problemResolver.recommendation(target) + val recommendedFix = problemResolver.recommendation(target) + DoctorTargetInfo( + target.displayName, + target.dataKind, + target.baseDirectory, + scalaVersion, + definition, + completions, + references, + recommendedFix + ) + } + + private def extractJavaTargetInfo(target: JavaTarget) = { + val scalaVersion = "Java" + val definition: String = Icons.unicode.check + val completions: String = Icons.unicode.alert + val isSemanticdbNeeded = !target.isSemanticdbEnabled + val references: String = + if (isSemanticdbNeeded) { + Icons.unicode.alert + } else { + Icons.unicode.check + } + val recommendedFix = problemResolver.recommendation(target) DoctorTargetInfo( target.displayName, + target.dataKind, + target.baseDirectory, scalaVersion, definition, completions, references, - recommenedFix + recommendedFix ) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/DoctorResults.scala b/metals/src/main/scala/scala/meta/internal/metals/DoctorResults.scala index 24b9eb4791b..031b84897b3 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/DoctorResults.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/DoctorResults.scala @@ -6,7 +6,7 @@ final case class DoctorResults( title: String, headerText: String, messages: Option[List[DoctorMessage]], - targets: Option[List[DoctorTargetInfo]] + targets: Option[Seq[DoctorTargetInfo]] ) { def toJson: Obj = { val json = ujson.Obj( @@ -31,6 +31,8 @@ final case class DoctorMessage(title: String, recommendations: List[String]) { final case class DoctorTargetInfo( name: String, + dataKind: String, + baseDirectory: String, scalaVersion: String, definitionStatus: String, completionsStatus: String, diff --git a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala index 78b999c4ae1..fc358213197 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala @@ -283,8 +283,7 @@ final class FileDecoderProvider( newExtension: String ): Either[String, PathInfo] = findBuildTargetMetadata(sourceFile) - .map { case (targetId, target, _, sourceRoot) => - val classDir = target.classDirectory.toAbsolutePath + .map { case (targetId, classDir, _, _, sourceRoot) => val oldExtension = sourceFile.extension val relativePath = sourceFile .toRelative(sourceRoot) @@ -296,10 +295,11 @@ final class FileDecoderProvider( path: AbsolutePath ): Option[PathInfo] = { val pathInfos = for { - scalaTarget <- buildTargets.all - classPath = scalaTarget.classDirectory.toAbsolutePath + targetId <- buildTargets.allBuildTargetIds + classDir <- buildTargets.targetClassDirectories(targetId) + classPath = classDir.toAbsolutePath if (path.isInside(classPath)) - } yield PathInfo(scalaTarget.id, path) + } yield PathInfo(targetId, path) pathInfos.toList.headOption } @@ -329,8 +329,7 @@ final class FileDecoderProvider( DecoderResponse.failed(requestedURI, _) ) } yield { - val (targetId, target, _, _) = buildMetadata - val classDir = target.classDirectory.toAbsolutePath + val (targetId, classDir, _, _, _) = buildMetadata val pathToResource = classDir.resolve(resourcePath) PathInfo(targetId, pathToResource) } @@ -369,10 +368,9 @@ final class FileDecoderProvider( ): Either[String, AbsolutePath] = for { metadata <- findBuildTargetMetadata(sourceFile) - (targetId, target, workspaceDirectory, _) = metadata + (_, _, targetRoot, workspaceDirectory, _) = metadata foundSemanticDbPath <- { - val targetRoot = target.targetroot - val relativePath = SemanticdbClasspath.fromScala( + val relativePath = SemanticdbClasspath.fromScalaOrJava( sourceFile.toRelative(workspaceDirectory.dealias) ) fileSystemSemanticdbs @@ -388,18 +386,56 @@ final class FileDecoderProvider( } } yield foundSemanticDbPath.path + private def findJavaBuildTargetMetadata( + targetId: BuildTargetIdentifier, + sourceFile: AbsolutePath + ): Option[(String, AbsolutePath)] = { + for { + javaTarget <- buildTargets.javaTarget(targetId) + classDir = javaTarget.classDirectory + targetroot = javaTarget.targetroot + } yield (classDir, targetroot) + } + + private def findScalaBuildTargetMetadata( + targetId: BuildTargetIdentifier, + sourceFile: AbsolutePath + ): Option[(String, AbsolutePath)] = { + for { + scalaTarget <- buildTargets.scalaTarget(targetId) + classDir = scalaTarget.classDirectory + targetroot = scalaTarget.targetroot + } yield (classDir, targetroot) + } + private def findBuildTargetMetadata( sourceFile: AbsolutePath ): Either[ String, - (BuildTargetIdentifier, ScalaTarget, AbsolutePath, AbsolutePath) + ( + BuildTargetIdentifier, + AbsolutePath, + AbsolutePath, + AbsolutePath, + AbsolutePath + ) ] = { val metadata = for { targetId <- buildTargets.inverseSources(sourceFile) - target <- buildTargets.scalaTarget(targetId) workspaceDirectory <- buildTargets.workspaceDirectory(targetId) sourceRoot <- buildTargets.inverseSourceItem(sourceFile) - } yield (targetId, target, workspaceDirectory, sourceRoot) + (classDir, targetroot) <- + if (sourceFile.isJava) + findJavaBuildTargetMetadata(targetId, sourceFile) + else + findScalaBuildTargetMetadata(targetId, sourceFile) + } yield ( + targetId, + classDir.toAbsolutePath, + targetroot, + workspaceDirectory, + sourceRoot + ) metadata.toRight( s"Cannot find build's metadata for ${sourceFile.toURI.toString()}" ) diff --git a/metals/src/main/scala/scala/meta/internal/metals/FileSystemSemanticdbs.scala b/metals/src/main/scala/scala/meta/internal/metals/FileSystemSemanticdbs.scala index ba647882953..113dde567e0 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FileSystemSemanticdbs.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FileSystemSemanticdbs.scala @@ -23,7 +23,7 @@ final class FileSystemSemanticdbs( override def textDocument(file: AbsolutePath): TextDocumentLookup = { if ( - !file.toLanguage.isScala || + (!file.toLanguage.isScala && !file.toLanguage.isJava) || file.toNIO.getFileSystem != mainWorkspace.toNIO.getFileSystem ) { TextDocumentLookup.NotFound(file) @@ -31,11 +31,12 @@ final class FileSystemSemanticdbs( val paths = for { buildTarget <- buildTargets.inverseSources(file) - scalaInfo <- buildTargets.scalaInfo(buildTarget) workspace <- buildTargets.workspaceDirectory(buildTarget) - scalacOptions <- buildTargets.scalacOptions(buildTarget) + targetroot <- + if (file.toLanguage.isScala) buildTargets.scalaTargetRoot(buildTarget) + else buildTargets.javaTargetRoot(buildTarget) } yield { - (workspace, scalacOptions.targetroot(scalaInfo.getScalaVersion)) + (workspace, targetroot) } paths match { @@ -69,7 +70,7 @@ final class FileSystemSemanticdbs( relativeSourceRoot = sourceRoot.toRelative(workspace) relativeFile = file.toRelative(sourceRoot.dealias) fullRelativePath = relativeSourceRoot.resolve(relativeFile) - alternativeRelativePath = SemanticdbClasspath.fromScala( + alternativeRelativePath = SemanticdbClasspath.fromScalaOrJava( fullRelativePath ) alternativeSemanticdbPath = targetroot.resolve( diff --git a/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala index abafb56307b..af3f2978a8a 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala @@ -160,7 +160,7 @@ final class FormattingProvider( case Some(version) => val text = config.toInputFromBuffers(buffers).text val dialect = - buildTargets.all.map(_.fmtDialect).toList.sorted.lastOption + buildTargets.allScala.map(_.fmtDialect).toList.sorted.lastOption val newText = ScalafmtConfig.update(text, Some(version), dialect, Map.empty) Files.write(config.toNIO, newText.getBytes(StandardCharsets.UTF_8)) @@ -298,7 +298,7 @@ final class FormattingProvider( .map(d => (path, d)) } if (itemsRequiresUpgrade.nonEmpty) { - val nonSbtTargets = buildTargets.all.toList.filter(!_.isSbt) + val nonSbtTargets = buildTargets.allScala.toList.filter(!_.isSbt) val minDialect = config.runnerDialect match { case Some(d) => d diff --git a/metals/src/main/scala/scala/meta/internal/metals/ImportedBuild.scala b/metals/src/main/scala/scala/meta/internal/metals/ImportedBuild.scala index 8a242031b0d..22b15b48335 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ImportedBuild.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ImportedBuild.scala @@ -15,6 +15,7 @@ import ch.epfl.scala.bsp4j._ case class ImportedBuild( workspaceBuildTargets: WorkspaceBuildTargetsResult, scalacOptions: ScalacOptionsResult, + javacOptions: JavacOptionsResult, sources: SourcesResult, dependencySources: DependencySourcesResult ) { @@ -25,6 +26,9 @@ case class ImportedBuild( val updatedScalacOptions = new ScalacOptionsResult( (scalacOptions.getItems.asScala ++ other.scalacOptions.getItems.asScala).asJava ) + val updatedJavacOptions = new JavacOptionsResult( + (javacOptions.getItems.asScala ++ other.javacOptions.getItems.asScala).asJava + ) val updatedSources = new SourcesResult( (sources.getItems.asScala ++ other.sources.getItems.asScala).asJava ) @@ -34,6 +38,7 @@ case class ImportedBuild( ImportedBuild( updatedBuildTargets, updatedScalacOptions, + updatedJavacOptions, updatedSources, updatedDependencySources ) @@ -50,6 +55,7 @@ object ImportedBuild { ImportedBuild( new WorkspaceBuildTargetsResult(ju.Collections.emptyList()), new ScalacOptionsResult(ju.Collections.emptyList()), + new JavacOptionsResult(ju.Collections.emptyList()), new SourcesResult(ju.Collections.emptyList()), new DependencySourcesResult(ju.Collections.emptyList()) ) @@ -63,6 +69,9 @@ object ImportedBuild { scalacOptions <- conn.buildTargetScalacOptions( new ScalacOptionsParams(ids) ) + javacOptions <- conn.buildTargetJavacOptions( + new JavacOptionsParams(ids) + ) sources <- conn.buildTargetSources(new SourcesParams(ids)) dependencySources <- conn.buildTargetDependencySources( new DependencySourcesParams(ids) @@ -71,6 +80,7 @@ object ImportedBuild { ImportedBuild( workspaceBuildTargets, scalacOptions, + javacOptions, sources, dependencySources ) diff --git a/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala new file mode 100644 index 00000000000..86b5f1fd70b --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala @@ -0,0 +1,230 @@ +package scala.meta.internal.metals + +import java.util + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future +import scala.util.Failure +import scala.util.Success +import scala.util.Try +import scala.xml.Node + +import scala.meta._ +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.io.AbsolutePath +import scala.meta.{inputs => m} + +import org.eclipse.jdt.core.JavaCore +import org.eclipse.jdt.core.ToolFactory +import org.eclipse.jdt.core.formatter.CodeFormatter +import org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions +import org.eclipse.jface.text.BadLocationException +import org.eclipse.jface.text.Document +import org.eclipse.text.edits.MalformedTreeException +import org.eclipse.{lsp4j => l} + +final class JavaFormattingProvider( + workspace: AbsolutePath, + buffers: Buffers, + userConfig: () => UserConfiguration, + buildTargets: BuildTargets +)(implicit + ec: ExecutionContext +) { + // TODO cache formatting file and reload on change? + // TODO progress monitor? + // TODO onTypeFormatting - wait until presentation compiler is integrated + + private def decodeProfile(node: Node): Map[String, String] = { + val settings = for { + child <- node.child + id <- child.attribute("id") + value <- child.attribute("value") + } yield (id.text, value.text) + settings.toMap + } + + private def parseEclipseFormatFile( + text: String + ): Try[Map[String, String]] = { + val profileName = userConfig().eclipseFormatProfile + import scala.xml.XML + Try { + val node = XML.loadString(text) + val profiles = node.child.filter(child => { + val foundKind = child.attributes + .get("kind") + .map(_.text) + .contains("CodeFormatterProfile") + val foundProfile = profileName.isEmpty || child.attributes + .get("name") + .map(_.text) == profileName + + foundKind && foundProfile + }) + if (profiles.isEmpty) { + if (profileName.isEmpty) + scribe.error("No Java formatting profiles found") + else + scribe.error(s"Java formatting profile ${profileName.get} not found") + defaultSettings + } else + decodeProfile(profiles.head) + } + } + + private lazy val defaultSettings = + DefaultCodeFormatterOptions.getEclipseDefaultSettings.getMap.asScala.toMap + + private def loadEclipseFormatConfig: Map[String, String] = { + userConfig().eclipseFormatConfigPath + .map(eclipseFormatFile => { + if (eclipseFormatFile.exists) { + val text = eclipseFormatFile.toInputFromBuffers(buffers).text + parseEclipseFormatFile(text) match { + case Failure(e) => + scribe.error( + s"Failed to parse $eclipseFormatFile. Using default formatting", + e + ) + defaultSettings + case Success(values) => + values + } + } else { + scribe.warn( + s"$eclipseFormatFile not found. Using default java formatting" + ) + defaultSettings + } + }) + .getOrElse(defaultSettings) + } + + def format( + params: l.DocumentFormattingParams + ): Future[util.List[l.TextEdit]] = { + Future { + val options = params.getOptions + val path = params.getTextDocument.getUri.toAbsolutePath + val input = path.toInputFromBuffers(buffers) + runFormat(path, input, options, fromLSP(input)).asJava + } + } + + private def fromLSP(input: Input, range: l.Range): m.Position.Range = + m.Position.Range( + input, + range.getStart().getLine(), + range.getStart().getCharacter(), + range.getEnd().getLine(), + range.getEnd().getCharacter() + ) + + private def fromLSP(input: Input): m.Position.Range = + m.Position.Range(input, 0, input.chars.length) + + def format( + params: l.DocumentRangeFormattingParams + ): util.List[l.TextEdit] = { + val options = params.getOptions + val range = params.getRange + val path = params.getTextDocument.getUri.toAbsolutePath + val input = path.toInputFromBuffers(buffers) + runFormat(path, input, options, fromLSP(input, range)).asJava + } + + def format( + params: l.DocumentOnTypeFormattingParams + ): util.List[l.TextEdit] = { + Nil.asJava + } + + private def runFormat( + path: AbsolutePath, + input: Input, + formattingOptions: l.FormattingOptions, + range: m.Position.Range + ): List[l.TextEdit] = { + // if source/target/compliance versions aren't defined by the user then fallback on the build target info + var options = loadEclipseFormatConfig + if ( + !options.contains(JavaCore.COMPILER_SOURCE) || + !options.contains(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM) || + !options.contains(JavaCore.COMPILER_COMPLIANCE) + ) { + + val version = for { + targetID <- buildTargets.sourceBuildTargets(path) + java <- buildTargets.javaTarget(targetID) + sourceVersion <- java.sourceVersion + targetVersion <- java.targetVersion + } yield ((sourceVersion, targetVersion)) + + version + .take(1) + .foreach(version => { + val (sourceVersion, targetVersion) = version + val complianceVersion = + if (sourceVersion.toDouble > targetVersion.toDouble) sourceVersion + else targetVersion + + if (!options.contains(JavaCore.COMPILER_SOURCE)) + options += JavaCore.COMPILER_SOURCE -> sourceVersion + if (!options.contains(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM)) + options += JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM -> targetVersion + if (!options.contains(JavaCore.COMPILER_COMPLIANCE)) + options += JavaCore.COMPILER_COMPLIANCE -> complianceVersion + }) + } + + if (!options.contains(JavaCore.FORMATTER_TAB_SIZE)) + Option(formattingOptions.getTabSize).foreach(f => + options += JavaCore.FORMATTER_TAB_SIZE -> f.toString + ) + if (!options.contains(JavaCore.FORMATTER_TAB_CHAR)) + options += JavaCore.FORMATTER_TAB_CHAR -> { + if (formattingOptions.isInsertSpaces) JavaCore.SPACE else JavaCore.TAB + } + + val codeFormatter = ToolFactory.createCodeFormatter(options.asJava) + + val kind = { + if (userConfig().enableFormatJavaComments) + CodeFormatter.F_INCLUDE_COMMENTS + else 0 + } | CodeFormatter.K_COMPILATION_UNIT + val code = input.text + val doc = new Document(code) + val codeOffset = range.start + val codeLength = range.end - range.start + val indentationLevel = 0 + val lineSeparator: String = null + val textEdit = codeFormatter.format( + kind, + code, + codeOffset, + codeLength, + indentationLevel, + lineSeparator + ) + val formatted = + try { + textEdit.apply(doc) + doc.get + } catch { + case e: MalformedTreeException => + scribe.error("Unable to format", e) + code + case e: BadLocationException => + scribe.error("Unable to format", e) + code + } + + val fullDocumentRange = fromLSP(input).toLSP + if (formatted != code) + List(new l.TextEdit(fullDocumentRange, formatted)) + else + Nil + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/JavaTarget.scala b/metals/src/main/scala/scala/meta/internal/metals/JavaTarget.scala new file mode 100644 index 00000000000..d32bb8301db --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/JavaTarget.scala @@ -0,0 +1,34 @@ +package scala.meta.internal.metals + +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.io.AbsolutePath + +import ch.epfl.scala.bsp4j.BuildTarget +import ch.epfl.scala.bsp4j.JavacOptionsItem + +case class JavaTarget( + info: BuildTarget, + javac: JavacOptionsItem +) { + def displayName: String = info.getDisplayName() + + def dataKind: String = info.dataKind + + def baseDirectory: String = info.baseDirectory + + def isSemanticdbEnabled: Boolean = javac.isSemanticdbEnabled + + def isSourcerootDeclared: Boolean = javac.isSourcerootDeclared + + def isTargetrootDeclared: Boolean = javac.isTargetrootDeclared + + def classDirectory: String = javac.getClassDirectory() + + def releaseVersion: Option[String] = javac.releaseVersion + + def targetVersion: Option[String] = javac.targetVersion + + def sourceVersion: Option[String] = javac.sourceVersion + + def targetroot: AbsolutePath = javac.targetroot +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsBuildServer.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsBuildServer.scala index 5ce9475454b..debf17ca9d2 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsBuildServer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsBuildServer.scala @@ -6,7 +6,10 @@ import ch.epfl.scala.bsp4j.DebugSessionParams import ch.epfl.scala.{bsp4j => b} import org.eclipse.lsp4j.jsonrpc.services.JsonRequest -trait MetalsBuildServer extends b.BuildServer with b.ScalaBuildServer { +trait MetalsBuildServer + extends b.BuildServer + with b.ScalaBuildServer + with b.JavaBuildServer { @JsonRequest("debugSession/start") def startDebugSession( params: DebugSessionParams diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index ef3db42f1b9..c1c406bfbaa 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -74,7 +74,17 @@ object MetalsEnrichments implicit class XtensionBuildTarget(buildTarget: b.BuildTarget) { def isSbtBuild: Boolean = - buildTarget.getDataKind == "sbt" + dataKind == "sbt" + + def baseDirectory: String = { + val baseDir = buildTarget.getBaseDirectory() + if (baseDir != null) baseDir else "" + } + + def dataKind: String = { + val dataking = buildTarget.getDataKind() + if (dataking != null) dataking else "" + } def asScalaBuildTarget: Option[b.ScalaBuildTarget] = { if (isSbtBuild) { @@ -272,7 +282,10 @@ object MetalsEnrichments } implicit class XtensionAbsolutePathBuffers(path: AbsolutePath) { - def sourcerootOption: String = s""""-P:semanticdb:sourceroot:$path"""" + def scalaSourcerootOption: String = s""""-P:semanticdb:sourceroot:$path"""" + + def javaSourcerootOption: String = + s""""-Xplugin:semanticdb -sourceroot:$path"""" /** * Resolve each path segment individually to prevent FileSystem mismatch errors. @@ -717,12 +730,78 @@ object MetalsEnrichments def getQuery(key: String): Option[String] = Option(exchange.getQueryParameters.get(key)).flatMap(_.asScala.headOption) } - implicit class XtensionScalacOptions(item: b.ScalacOptionsItem) { - def classpath: Iterator[AbsolutePath] = { - item.getClasspath.asScala.iterator + implicit class XtensionClasspath(classpath: List[String]) { + def toAbsoluteClasspath: Iterator[AbsolutePath] = { + classpath.iterator .map(uri => AbsolutePath(Paths.get(URI.create(uri)))) .filter(p => Files.exists(p.toNIO)) } + } + implicit class XtensionJavacOptions(item: b.JavacOptionsItem) { + def targetroot: AbsolutePath = { + item.getOptions.asScala + .find(_.startsWith("-Xplugin:semanticdb")) + .map(arg => { + val targetrootPos = arg.indexOf("-targetroot:") + val sourcerootPos = arg.indexOf("-sourceroot:") + if (targetrootPos > sourcerootPos) + arg.substring(targetrootPos + 12).trim() + else + arg.substring(sourcerootPos + 12, targetrootPos - 1).trim() + }) + .filter(_ != "javac-classes-directory") + .map(AbsolutePath(_)) + .getOrElse(item.getClassDirectory.toAbsolutePath) + } + + def isSemanticdbEnabled: Boolean = { + item.getOptions.asScala.exists { opt => + opt.startsWith("-Xplugin:semanticdb") + } + } + + def isSourcerootDeclared: Boolean = { + item.getOptions.asScala + .find(_.startsWith("-Xplugin:semanticdb")) + .map(_.contains("-sourceroot:")) + .getOrElse(false) + } + + def isTargetrootDeclared: Boolean = { + item.getOptions.asScala + .find(_.startsWith("-Xplugin:semanticdb")) + .map(_.contains("-targetroot:")) + .getOrElse(false) + } + + def classpath: List[String] = + item.getClasspath.asScala.toList + + def jarClasspath: List[AbsolutePath] = + classpath + .filter(_.endsWith(".jar")) + .map(_.toAbsolutePath) + + def releaseVersion: Option[String] = + item.getOptions.asScala + .dropWhile(_ != "--release") + .drop(1) + .headOption + + def sourceVersion: Option[String] = + item.getOptions.asScala + .dropWhile(f => f != "--source" && f != "-source" && f != "--release") + .drop(1) + .headOption + + def targetVersion: Option[String] = + item.getOptions.asScala + .dropWhile(f => f != "--target" && f != "-target" && f != "--release") + .drop(1) + .headOption + } + + implicit class XtensionScalacOptions(item: b.ScalacOptionsItem) { def targetroot(scalaVersion: String): AbsolutePath = { if (ScalaVersions.isScala3Version(scalaVersion)) { val options = item.getOptions.asScala @@ -777,6 +856,13 @@ object MetalsEnrichments .map(_.stripPrefix(flag)) } + def classpath: List[String] = + item.getClasspath.asScala.toList + + def jarClasspath: List[AbsolutePath] = + classpath + .filter(_.endsWith(".jar")) + .map(_.toAbsolutePath) } implicit class XtensionChar(ch: Char) { diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala index 6a58d9a5e12..72e9d999ae3 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala @@ -249,6 +249,7 @@ class MetalsLanguageServer( private var renameProvider: RenameProvider = _ private var documentHighlightProvider: DocumentHighlightProvider = _ private var formattingProvider: FormattingProvider = _ + private var javaFormattingProvider: JavaFormattingProvider = _ private var syntheticsDecorator: SyntheticsDecorationProvider = _ private var initializeParams: Option[InitializeParams] = None private var referencesProvider: ReferenceProvider = _ @@ -505,6 +506,12 @@ class MetalsLanguageServer( tables, buildTargets ) + javaFormattingProvider = new JavaFormattingProvider( + workspace, + buffers, + () => userConfig, + buildTargets + ) newFileProvider = new NewFileProvider( workspace, languageClient, @@ -1193,12 +1200,8 @@ class MetalsLanguageServer( // notifications to pick up `*.semanticdb` file updates and there's no // reliable way to await until those notifications appear. for { - item <- buildTargets.scalacOptions(report.getTarget()) - scalaInfo <- buildTargets.scalaInfo(report.getTarget) - semanticdb = - item - .targetroot(scalaInfo.getScalaVersion) - .resolve(Directories.semanticdb) + targetroot <- buildTargets.targetRoots(report.getTarget) + semanticdb = targetroot.resolve(Directories.semanticdb) generatedFile <- semanticdb.listRecursive } { val event = FileWatcherEvent.modify(generatedFile.toNIO) @@ -1426,10 +1429,11 @@ class MetalsLanguageServer( params: DocumentFormattingParams ): CompletableFuture[util.List[TextEdit]] = CancelTokens.future { token => - formattingProvider.format( - params.getTextDocument.getUri.toAbsolutePath, - token - ) + val path = params.getTextDocument.getUri.toAbsolutePath + if (path.isJava) + javaFormattingProvider.format(params) + else + formattingProvider.format(path, token) } @JsonRequest("textDocument/onTypeFormatting") @@ -1437,7 +1441,11 @@ class MetalsLanguageServer( params: DocumentOnTypeFormattingParams ): CompletableFuture[util.List[TextEdit]] = CancelTokens { _ => - onTypeFormattingProvider.format(params).asJava + val path = params.getTextDocument.getUri.toAbsolutePath + if (path.isJava) + javaFormattingProvider.format(params) + else + onTypeFormattingProvider.format(params).asJava } @JsonRequest("textDocument/rangeFormatting") @@ -1445,7 +1453,11 @@ class MetalsLanguageServer( params: DocumentRangeFormattingParams ): CompletableFuture[util.List[TextEdit]] = CancelTokens { _ => - rangeFormattingProvider.format(params).asJava + val path = params.getTextDocument.getUri.toAbsolutePath + if (path.isJava) + javaFormattingProvider.format(params) + else + rangeFormattingProvider.format(params).asJava } @JsonRequest("textDocument/prepareRename") @@ -1805,7 +1817,15 @@ class MetalsLanguageServer( val name = args.lift(1).flatten val fileType = args.lift(2).flatten newFileProvider - .handleFileCreation(directoryURI, name, fileType) + .handleFileCreation(directoryURI, name, fileType, true) + .asJavaObject + + case ServerCommands.NewJavaFile(args) => + val directoryURI = args.lift(0).flatten.map(new URI(_)) + val name = args.lift(1).flatten + val fileType = args.lift(2).flatten + newFileProvider + .handleFileCreation(directoryURI, name, fileType, false) .asJavaObject case ServerCommands.StartAmmoniteBuildServer() => @@ -2350,6 +2370,7 @@ class MetalsLanguageServer( symbolSearch.reset() buildTargets.addWorkspaceBuildTargets(i.workspaceBuildTargets) buildTargets.addScalacOptions(i.scalacOptions) + buildTargets.addJavacOptions(i.javacOptions) for { item <- i.sources.getItems.asScala source <- item.getSources.asScala @@ -2382,11 +2403,17 @@ class MetalsLanguageServer( workspaceSymbols.indexClasspath() } timerProvider.timedThunk( - "indexed workspace SemanticDBs", + "indexed workspace Scala SemanticDBs", clientConfig.initialConfig.statistics.isIndex ) { semanticDBIndexer.onScalacOptions(i.scalacOptions) } + timerProvider.timedThunk( + "indexed workspace Java SemanticDBs", + clientConfig.initialConfig.statistics.isIndex + ) { + semanticDBIndexer.onJavacOptions(i.javacOptions) + } timerProvider.timedThunk( "indexed workspace sources", clientConfig.initialConfig.statistics.isIndex @@ -2406,7 +2433,7 @@ class MetalsLanguageServer( .foreach(focusedDocumentBuildTarget.set) } - val targets = buildTargets.all.map(_.id).toSeq + val targets = buildTargets.allBuildTargetIds buildTargetClasses .rebuildIndex(targets) .foreach(_ => languageClient.refreshModel()) @@ -2453,6 +2480,7 @@ class MetalsLanguageServer( val isVisited = new ju.HashSet[String]() for { item <- dependencySources.getItems.asScala + // TODO(arthurm1) add java sources? What dialect? scalaTarget <- buildTargets.scalaTarget(item.getTarget) sourceUri <- Option(item.getSources).toList.flatMap(_.asScala) path = sourceUri.toAbsolutePath @@ -2609,7 +2637,7 @@ class MetalsLanguageServer( definitionOnly: Boolean = false ): Future[DefinitionResult] = { val source = positionParams.getTextDocument.getUri.toAbsolutePath - if (source.isScalaFilename) { + if (source.isScalaFilename || source.isJavaFilename) { val semanticDBDoc = semanticdbs.textDocument(source).documentIncludingStale (for { @@ -2671,7 +2699,7 @@ class MetalsLanguageServer( token: CancelToken = EmptyCancelToken ): Future[DefinitionResult] = { val source = position.getTextDocument.getUri.toAbsolutePath - if (source.isScalaFilename) { + if (source.isScalaFilename || source.isJavaFilename) { val result = timerProvider.timedThunk( "definition", diff --git a/metals/src/main/scala/scala/meta/internal/metals/PackageProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/PackageProvider.scala index c430a9beed3..bfff5258d2d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/PackageProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/PackageProvider.scala @@ -44,7 +44,7 @@ class PackageProvider(private val buildTargets: BuildTargets) { } } - if (path.isScala && path.toFile.length() == 0) { + if (path.isScalaOrJava && path.toFile.length() == 0) { buildTargets .inverseSourceItem(path) .map(path.toRelative) @@ -59,7 +59,10 @@ class PackageProvider(private val buildTargets: BuildTargets) { .asScala .map(p => wrap(p.toString())) .mkString(".") - Some(NewFileTemplate(s"package $packageName\n\n@@")) + val text = + if (path.isScala) s"package $packageName\n\n@@" + else s"package $packageName;\n\n@@" + Some(NewFileTemplate(text)) } } } else { diff --git a/metals/src/main/scala/scala/meta/internal/metals/ScalaTarget.scala b/metals/src/main/scala/scala/meta/internal/metals/ScalaTarget.scala index f1d931845e2..d6748b1d648 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ScalaTarget.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ScalaTarget.scala @@ -8,7 +8,6 @@ import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.io.AbsolutePath import ch.epfl.scala.bsp4j.BuildTarget -import ch.epfl.scala.bsp4j.BuildTargetIdentifier import ch.epfl.scala.bsp4j.ScalaBuildTarget import ch.epfl.scala.bsp4j.ScalacOptionsItem @@ -22,7 +21,7 @@ case class ScalaTarget( def dialect(path: AbsolutePath): Dialect = { scalaVersion match { - case _ if info.getDataKind() == "sbt" && path.isSbt => Sbt + case _ if info.isSbtBuild && path.isSbt => Sbt case other => val dialect = ScalaVersions.dialectForScalaVersion(other, includeSource3 = false) @@ -36,6 +35,12 @@ case class ScalaTarget( } } + def displayName: String = info.getDisplayName() + + def dataKind: String = info.dataKind + + def baseDirectory: String = info.baseDirectory + def fmtDialect: ScalafmtDialect = ScalaVersions.fmtDialectForScalaVersion(scalaVersion, containsSource3) @@ -43,43 +48,19 @@ case class ScalaTarget( def isSourcerootDeclared: Boolean = scalac.isSourcerootDeclared(scalaVersion) - def id: BuildTargetIdentifier = info.getId() - - def targetroot: AbsolutePath = scalac.targetroot(scalaVersion) - - def baseDirectory: String = { - val baseDir = info.getBaseDirectory() - if (baseDir != null) baseDir else "" - } - - def fullClasspath: List[Path] = { - scalac - .getClasspath() - .map(_.toAbsolutePath) - .asScala - .collect { - case path if path.isJar || path.isDirectory => - path.toNIO - } - .toList - } - - def jarClasspath: List[AbsolutePath] = { - scalac - .getClasspath() - .asScala - .toList - .filter(_.endsWith(".jar")) - .map(_.toAbsolutePath) - } - - def scalaVersion: String = scalaInfo.getScalaVersion() + def fullClasspath: List[Path] = + scalac.classpath.map(_.toAbsolutePath).collect { + case path if path.isJar || path.isDirectory => + path.toNIO + } def classDirectory: String = scalac.getClassDirectory() - def displayName: String = info.getDisplayName() + def scalaVersion: String = scalaInfo.getScalaVersion() def scalaBinaryVersion: String = scalaInfo.getScalaBinaryVersion() private def containsSource3 = scalac.getOptions().contains("-Xsource:3") + + def targetroot: AbsolutePath = scalac.targetroot(scalaVersion) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/ScalaVersionSelector.scala b/metals/src/main/scala/scala/meta/internal/metals/ScalaVersionSelector.scala index ee842e8acea..4a8b84b85ce 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ScalaVersionSelector.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ScalaVersionSelector.scala @@ -20,7 +20,7 @@ class ScalaVersionSelector( val selected = userConfig().fallbackScalaVersion match { case Some(v) => v case None => - buildTargets.all.toList + buildTargets.allScala.toList .map(_.scalaInfo.getScalaVersion) .sorted .lastOption diff --git a/metals/src/main/scala/scala/meta/internal/metals/ScalafixProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/ScalafixProvider.scala index 001b0226adc..c79c8b0f71f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ScalafixProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ScalafixProvider.scala @@ -47,9 +47,10 @@ case class ScalafixProvider( def load(): Unit = { if (!Testing.isEnabled) { try { - val targets = buildTargets.all.toList.groupBy(_.scalaVersion).flatMap { - case (_, targets) => targets.headOption - } + val targets = + buildTargets.allScala.toList.groupBy(_.scalaVersion).flatMap { + case (_, targets) => targets.headOption + } val tmp = workspace .resolve(Directories.tmp) .resolve(s"Main${Random.nextLong()}.scala") @@ -150,7 +151,8 @@ case class ScalafixProvider( semanticdb val dir = workspace.resolve(Directories.tmp) file.toRelativeInside(workspace).flatMap { relativePath => - val writeTo = dir.resolve(SemanticdbClasspath.fromScala(relativePath)) + val writeTo = + dir.resolve(SemanticdbClasspath.fromScalaOrJava(relativePath)) writeTo.parent.createDirectories() val docs = TextDocuments(Seq(toSave)) Files.write(writeTo.toNIO, docs.toByteArray) diff --git a/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala b/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala index a217435758b..63cf4d58263 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala @@ -14,6 +14,7 @@ import scala.meta.internal.semanticdb.TextDocument import scala.meta.internal.semanticdb.TextDocuments import scala.meta.io.AbsolutePath +import ch.epfl.scala.bsp4j.JavacOptionsResult import ch.epfl.scala.bsp4j.ScalacOptionsResult import com.google.protobuf.InvalidProtocolBufferException @@ -35,6 +36,15 @@ class SemanticdbIndexer( } } + def onJavacOptions(javacOptions: JavacOptionsResult): Unit = { + for { + item <- javacOptions.getItems.asScala + } { + val targetroot = item.targetroot + onChangeDirectory(targetroot.resolve(Directories.semanticdb).toNIO) + } + } + def reset(): Unit = { referenceProvider.reset() implementationProvider.clear() diff --git a/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala b/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala index 04fdeb20015..ac57cfdd410 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ServerCommands.scala @@ -348,6 +348,19 @@ object ServerCommands { |""".stripMargin ) + val NewJavaFile = new ListParametrizedCommand[String]( + "new-java-file", + "Create new java file", + """|Create and open new file with either java class, enum or interface. + | + |Note: requires 'metals/inputBox' capability from language client. + |""".stripMargin, + """|[string[]], where the first is a directory location for the new file. + |The second and third positions correspond to the file name and file type to allow for quick + |creation of a file if all are present. + |""".stripMargin + ) + val NewScalaProject = new Command( "new-scala-project", "New Scala Project", @@ -474,6 +487,7 @@ object ServerCommands { ImportBuild, InsertInferredType, NewScalaFile, + NewJavaFile, NewScalaProject, PresentationCompilerRestart, ResetChoicePopup, diff --git a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala index 35dfe1c5dd4..8dea19f5a4d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala @@ -43,7 +43,10 @@ case class UserConfiguration( enableIndentOnPaste: Boolean = false, excludedPackages: Option[List[String]] = None, fallbackScalaVersion: Option[String] = None, - testUserInterface: TestUserInterfaceKind = TestUserInterfaceKind.CodeLenses + testUserInterface: TestUserInterfaceKind = TestUserInterfaceKind.CodeLenses, + eclipseFormatConfigPath: Option[AbsolutePath] = None, + enableFormatJavaComments: Boolean = true, + eclipseFormatProfile: Option[String] = None ) { def currentBloopVersion: String = @@ -249,6 +252,32 @@ object UserConfiguration { """{ "testUserInterface" : "Test explorer" } """, "Test UI used for tests and test suites", "Default way of handling tests and test suites." + ), + UserConfigurationOption( + "eclipse-format-config-path", + """empty string `""`.""", + """"formatters/eclipse-formatter.xml"""", + "Eclipse formatter config path", + """Optional custom path to the eclipse-formatter.xml file. + |Should be an absolute path and use forward slashes `/` for file separators (even on Windows). + |""".stripMargin + ), + UserConfigurationOption( + "format-java-comments", + "false", + "false", + "Should format Java comments as well as code", + """|Default formatting of Java files only formats code. + |Select this to also format comments. + |""".stripMargin + ), + UserConfigurationOption( + "eclipse-format-profile", + """empty string `""`.""", + """"GoogleStyle"""", + "Eclipse formatting profile", + """|If the Eclipse formatter file contains more than one profile then specify the required profile name. + |""".stripMargin ) ) @@ -420,6 +449,12 @@ object UserConfiguration { TestUserInterfaceKind.CodeLenses } } + val eclipseFormatConfigPath = + getStringKey("eclipse-format-config-path").map(AbsolutePath(_)) + val shouldFormatJavaComments = + getBooleanKey("enable-format-java-comments").getOrElse(false) + val eclipseFormatProfile = getStringKey("eclipse-format-profile") + if (errors.isEmpty) { Right( UserConfiguration( @@ -445,7 +480,10 @@ object UserConfiguration { enableIndentOnPaste, excludedPackages, defaultScalaVersion, - disableTestCodeLenses + disableTestCodeLenses, + eclipseFormatConfigPath, + shouldFormatJavaComments, + eclipseFormatProfile ) ) } else { diff --git a/metals/src/main/scala/scala/meta/internal/metals/Warnings.scala b/metals/src/main/scala/scala/meta/internal/metals/Warnings.scala index ccd3b158b01..69fc8f91db0 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Warnings.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Warnings.scala @@ -34,6 +34,7 @@ final class Warnings( } val isReported: Option[Unit] = for { buildTarget <- buildTargets.inverseSources(path) + // TODO(arthurm1) add javaTarget warnings (targetroot, sourceroot, semanticDB etc.) info <- buildTargets.scalaTarget(buildTarget) scalacOptions <- buildTargets.scalacOptions(buildTarget) } yield { @@ -54,7 +55,7 @@ final class Warnings( } } else { if (!info.isSourcerootDeclared) { - val option = workspace.sourcerootOption + val option = workspace.scalaSourcerootOption logger.error( s"$doesntWorkBecause the build target ${info.displayName} is missing the compiler option $option. " + s"To fix this problems, update the build settings to include this compiler option." @@ -67,10 +68,11 @@ final class Warnings( ) statusBar.addMessage(icons.info + tryAgain) } else if (!path.isSbt && !path.isWorksheet) { - val targetRoot = scalacOptions.targetroot(info.scalaVersion) val targetfile = targetRoot - .resolve(SemanticdbClasspath.fromScala(path.toRelative(workspace))) + .resolve( + SemanticdbClasspath.fromScalaOrJava(path.toRelative(workspace)) + ) logger.error( s"$doesntWorkBecause the SemanticDB file '$targetfile' doesn't exist. " + s"There can be many reasons for this error. " diff --git a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala index d0849897d3e..dabab98a7d4 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala @@ -624,10 +624,10 @@ class DebugProvider( result <- Future.fromTry(f()) } yield result case _: ClassNotFoundException => - val allTargets = buildTargets.allBuildTargetIds + val allTargetIds = buildTargets.allBuildTargetIds for { - _ <- compilations.compileTargets(allTargets) - _ <- buildTargetClasses.rebuildIndex(allTargets) + _ <- compilations.compileTargets(allTargetIds) + _ <- buildTargetClasses.rebuildIndex(allTargetIds) result <- Future.fromTry(f()) } yield result } diff --git a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala index 13aa04a2fa1..2fdcb966bf6 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala @@ -37,7 +37,8 @@ class NewFileProvider( def handleFileCreation( directoryUri: Option[URI], name: Option[String], - fileType: Option[String] + fileType: Option[String], + isScala: Boolean ): Future[Unit] = { val directory = directoryUri .map { uri => @@ -53,12 +54,17 @@ class NewFileProvider( fileType.flatMap(getFromString) match { case Some(ft) => createFile(directory, ft, name) case None => - askForKind( - directory.forall(dir => - ScalaVersions.isScala3Version(selector.scalaVersionForPath(dir)) - ) - ) - .flatMapOption(createFile(directory, _, name)) + val askForKind = + if (isScala) + askForScalaKind( + directory.forall(dir => + ScalaVersions.isScala3Version( + selector.scalaVersionForPath(dir) + ) + ) + ) + else askForJavaKind + askForKind.flatMapOption(createFile(directory, _, name)) } } @@ -78,7 +84,12 @@ class NewFileProvider( case kind @ (Class | CaseClass | Object | Trait | Enum) => getName(kind, name) .mapOption( - createClass(directory, _, kind) + createClass(directory, _, kind, ".scala") + ) + case kind @ (JavaClass | JavaEnum | JavaInterface | JavaRecord) => + getName(kind, name) + .mapOption( + createClass(directory, _, kind, ".java") ) case ScalaFile => getName(ScalaFile, name).mapOption( @@ -99,23 +110,13 @@ class NewFileProvider( } } - private def askForKind(isScala3: Boolean): Future[Option[NewFileType]] = { - val allFileTypes = List( - ScalaFile.toQuickPickItem, - Class.toQuickPickItem, - CaseClass.toQuickPickItem, - Object.toQuickPickItem, - Trait.toQuickPickItem, - PackageObject.toQuickPickItem, - Worksheet.toQuickPickItem, - AmmoniteScript.toQuickPickItem - ) - val withEnum = - if (isScala3) allFileTypes :+ Enum.toQuickPickItem else allFileTypes + private def askForKind( + kinds: List[NewFileType] + ): Future[Option[NewFileType]] = { client .metalsQuickPick( MetalsQuickPickParams( - withEnum.asJava, + kinds.map(_.toQuickPickItem).asJava, placeHolder = NewScalaFile.selectTheKindOfFileMessage ) ) @@ -123,6 +124,35 @@ class NewFileProvider( .flatMapOptionInside(kind => getFromString(kind.itemId)) } + private def askForScalaKind( + isScala3: Boolean + ): Future[Option[NewFileType]] = { + val allFileTypes = List( + ScalaFile, + Class, + CaseClass, + Object, + Trait, + PackageObject, + Worksheet, + AmmoniteScript + ) + val withEnum = + if (isScala3) allFileTypes :+ Enum else allFileTypes + askForKind(withEnum) + } + + private def askForJavaKind: Future[Option[NewFileType]] = { + askForKind( + List( + JavaClass, + JavaInterface, + JavaEnum, + JavaRecord + ) + ) + } + private def askForName(kind: String): Future[Option[String]] = { client .metalsInputBox( @@ -145,9 +175,10 @@ class NewFileProvider( private def createClass( directory: Option[AbsolutePath], name: String, - kind: NewFileType + kind: NewFileType, + ext: String ): Future[(AbsolutePath, Range)] = { - val path = directory.getOrElse(workspace).resolve(name + ".scala") + val path = directory.getOrElse(workspace).resolve(name + ext) //name can be actually be "foo/Name", where "foo" is a folder to create val className = Identifier.backtickWrap( directory.getOrElse(workspace).resolve(name).filename @@ -155,7 +186,8 @@ class NewFileProvider( val template = kind match { case CaseClass => caseClassTemplate(className) case Enum => enumTemplate(kind.id, className) - case _ => classTemplate(kind.id, className) + case JavaRecord => javaRecordTemplate(className) + case _ => classTemplate(kind.syntax, className) } val editText = template.map { s => packageProvider @@ -260,6 +292,13 @@ class NewFileProvider( |""".stripMargin) } + private def javaRecordTemplate(name: String): NewFileTemplate = { + NewFileTemplate(s"""|record $name(@@) { + | + |} + |""".stripMargin) + } + private def caseClassTemplate(name: String): NewFileTemplate = NewFileTemplate(s"final case class $name(@@)") diff --git a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala index 68fcd1e5843..5f43078706b 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala @@ -5,6 +5,7 @@ import scala.meta.internal.metals.clients.language.MetalsQuickPickItem object NewFileTypes { sealed trait NewFileType { val id: String + val syntax: String val label: String def toQuickPickItem: MetalsQuickPickItem = MetalsQuickPickItem(id, label) } @@ -15,45 +16,77 @@ object NewFileTypes { } case object Class extends NewFileType { - override val id: String = "class" + override val id: String = "scala-class" + override val syntax: String = "class" override val label: String = "Class" } case object CaseClass extends NewFileType { - override val id: String = "case-class" + override val id: String = "scala-case-class" + override val syntax: String = "NA" override val label: String = "Case Class" } case object Enum extends NewFileType { override val id: String = "enum" + override val syntax: String = "NA" override val label: String = "Enum" } case object Object extends NewFileType { - override val id: String = "object" + override val id: String = "scala-object" + override val syntax: String = "object" override val label: String = "Object" } case object Trait extends NewFileType { - override val id: String = "trait" + override val id: String = "scala-trait" + override val syntax: String = "trait" override val label: String = "Trait" } case object PackageObject extends NewFileType { - override val id: String = "package-object" + override val id: String = "scala-package-object" + override val syntax: String = "NA" override val label: String = "Package Object" } case object Worksheet extends NewFileType { - override val id: String = "worksheet" + override val id: String = "scala-worksheet" + override val syntax: String = "NA" override val label: String = "Worksheet" } case object AmmoniteScript extends NewFileType { - override val id: String = "ammonite" + override val id: String = "ammonite-script" + override val syntax: String = "NA" override val label: String = "Ammonite Script" } + case object JavaClass extends NewFileType { + override val id: String = "java-class" + override val syntax: String = "class" + override val label: String = "Class" + } + + case object JavaInterface extends NewFileType { + override val id: String = "java-interface" + override val syntax: String = "interface" + override val label: String = "Interface" + } + + case object JavaEnum extends NewFileType { + override val id: String = "java-enum" + override val syntax: String = "enum" + override val label: String = "Enum" + } + + case object JavaRecord extends NewFileType { + override val id: String = "java-record" + override val syntax: String = "NA" + override val label: String = "Record" + } + def getFromString(id: String): Option[NewFileType] = id match { case Class.id => Some(Class) @@ -65,9 +98,13 @@ object NewFileTypes { case PackageObject.id => Some(PackageObject) case Worksheet.id => Some(Worksheet) case AmmoniteScript.id => Some(AmmoniteScript) + case JavaClass.id => Some(JavaClass) + case JavaInterface.id => Some(JavaInterface) + case JavaEnum.id => Some(JavaEnum) + case JavaRecord.id => Some(JavaRecord) case invalid => scribe.error( - s"Invalid filetype given to new-scala-file command: $invalid" + s"Invalid filetype given to new-(scala/java)-file command: $invalid" ) None } diff --git a/metals/src/main/scala/scala/meta/internal/metals/watcher/FileWatcher.scala b/metals/src/main/scala/scala/meta/internal/metals/watcher/FileWatcher.scala index 64d4bba689d..137f39dcfee 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/watcher/FileWatcher.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/watcher/FileWatcher.scala @@ -88,13 +88,9 @@ object FileWatcher { // Watch the source directories for "goto definition" index. buildTargets.sourceRoots.foreach(collect) buildTargets.sourceItems.foreach(collect) - val semanticdbs = buildTargets.scalacOptions.flatMap { item => - for { - scalaInfo <- buildTargets.scalaInfo(item.getTarget) - targetroot = item.targetroot(scalaInfo.getScalaVersion) - path = targetroot.resolve(Directories.semanticdb) if !targetroot.isJar - } yield path.toNIO - } + val semanticdbs = buildTargets.allTargetRoots + .filterNot(_.isJar) + .map(_.resolve(Directories.semanticdb).toNIO) FilesToWatch( sourceFilesToWatch.toSet, diff --git a/metals/src/main/scala/scala/meta/internal/troubleshoot/JavaProblem.scala b/metals/src/main/scala/scala/meta/internal/troubleshoot/JavaProblem.scala new file mode 100644 index 00000000000..5ea4a084c1b --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/troubleshoot/JavaProblem.scala @@ -0,0 +1,44 @@ +package scala.meta.internal.troubleshoot + +import scala.meta.internal.metals.BuildInfo + +/** + * Class describing different issues that a build target can have that might influence + * features available to a user. For example without the semanticdb option + * "find references" feature will not work. Those problems will be reported to a user + * with an explanation on how to fix it. + */ +sealed abstract class JavaProblem { + + /** + * Comprehensive message to be presented to the user. + */ + def message: String + protected val hint = "run 'Import build' to enable code navigation." +} + +case class JavaSemanticDBDisabled( + bloopVersion: String, + unsupportedBloopVersion: Boolean +) extends JavaProblem { + override def message: String = { + if (unsupportedBloopVersion) { + s"""|The installed Bloop server version is $bloopVersion while Metals requires at least Bloop version ${BuildInfo.bloopVersion}, + |To fix this problem please update your Bloop server.""".stripMargin + } else { + "Semanticdb is required for code navigation to work correctly in your project," + + " however the Java semanticdb plugin doesn't seem to be enabled. " + + "Please enable the Java semanticdb plugin for this project in order for code navigation to work correctly" + } + } +} + +case class MissingJavaSourceRoot(sourcerootOption: String) extends JavaProblem { + override def message: String = + s"Add the compiler option $sourcerootOption to ensure code navigation works." +} + +case class MissingJavaTargetRoot(targetrootOption: String) extends JavaProblem { + override def message: String = + s"Add the compiler option $targetrootOption to ensure code navigation works." +} diff --git a/metals/src/main/scala/scala/meta/internal/troubleshoot/ProblemResolver.scala b/metals/src/main/scala/scala/meta/internal/troubleshoot/ProblemResolver.scala index 7953fa630c9..a64dee83c9c 100644 --- a/metals/src/main/scala/scala/meta/internal/troubleshoot/ProblemResolver.scala +++ b/metals/src/main/scala/scala/meta/internal/troubleshoot/ProblemResolver.scala @@ -5,6 +5,7 @@ import scala.collection.mutable.ListBuffer import scala.meta.internal.bsp.BspSession import scala.meta.internal.metals.BloopServers import scala.meta.internal.metals.BuildInfo +import scala.meta.internal.metals.JavaTarget import scala.meta.internal.metals.Messages import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.MtagsResolver @@ -32,13 +33,22 @@ class ProblemResolver( } } + def recommendation(java: JavaTarget): String = { + findProblem(java) + .map(_.message) + .getOrElse("") + } + def recommendation(scala: ScalaTarget): String = { findProblem(scala) .map(_.message) .getOrElse("") } - def problemMessage(allTargets: List[ScalaTarget]): Option[String] = { + def problemMessage( + scalaTargets: List[ScalaTarget], + javaTargets: List[JavaTarget] + ): Option[String] = { val unsupportedVersions = ListBuffer[String]() val deprecatedVersions = ListBuffer[String]() @@ -49,7 +59,7 @@ class ProblemResolver( var futureSbt = false for { - target <- allTargets + target <- scalaTargets issue <- findProblem(target) } yield { issue match { @@ -63,6 +73,16 @@ class ProblemResolver( case FutureSbtVersion => futureSbt = true } } + for { + target <- javaTargets + issue <- findProblem(target) + } yield { + issue match { + case _: JavaSemanticDBDisabled => misconfiguredProjects += 1 + case _: MissingJavaSourceRoot => misconfiguredProjects += 1 + case _: MissingJavaTargetRoot => misconfiguredProjects += 1 + } + } val unsupportedMessage = if (unsupportedVersions.nonEmpty) { Some(Messages.UnsupportedScalaVersion.message(unsupportedVersions.toSet)) @@ -90,14 +110,19 @@ class ProblemResolver( val semanticdbMessage = if ( - misconfiguredProjects == allTargets.size && misconfiguredProjects > 0 + misconfiguredProjects == (scalaTargets.size + javaTargets.size) && misconfiguredProjects > 0 ) { Some(Messages.CheckDoctor.allProjectsMisconfigured) } else if (misconfiguredProjects == 1) { - val name = allTargets + val name = scalaTargets .find(t => !t.isSemanticdbEnabled) .map(_.displayName) - .getOrElse("") + .getOrElse( + javaTargets + .find(t => !t.isSemanticdbEnabled) + .map(_.displayName) + .getOrElse("") + ) Some(Messages.CheckDoctor.singleMisconfiguredProject(name)) } else if (misconfiguredProjects > 0) { Some( @@ -168,7 +193,7 @@ class ProblemResolver( case _ if !scalaTarget.isSourcerootDeclared && !ScalaVersions .isScala3Version(scalaTarget.scalaVersion) => - Some(MissingSourceRoot(workspace.sourcerootOption)) + Some(MissingSourceRoot(workspace.scalaSourcerootOption)) case version if ScalaVersions.isDeprecatedScalaVersion( version @@ -179,4 +204,25 @@ class ProblemResolver( case _ => None } } + + private def findProblem( + javaTarget: JavaTarget + ): Option[JavaProblem] = { + if (!javaTarget.isSemanticdbEnabled) + Some( + JavaSemanticDBDisabled( + currentBuildServer().map(_.main.name).getOrElse(""), + isUnsupportedBloopVersion() + ) + ) + else if (!javaTarget.isSourcerootDeclared) + Some(MissingJavaSourceRoot(workspace.javaSourcerootOption)) + else if (!javaTarget.isTargetrootDeclared) + Some( + MissingJavaTargetRoot( + "-Xplugin:semanticdb -targetroot:javac-classes-directory" + ) + ) + else None + } } diff --git a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala index 9015fc546b8..18f0251e267 100644 --- a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala @@ -54,7 +54,7 @@ class MetalsTreeViewProvider( (path, symbol) => classpath.symbols(path, symbol) ) - val projects = new ClasspathTreeView[ScalaTarget, BuildTargetIdentifier]( + val projects = new ClasspathTreeView[CommonTarget, BuildTargetIdentifier]( definitionIndex, Project, "projects", @@ -65,18 +65,16 @@ class MetalsTreeViewProvider( _.displayName, _.baseDirectory, { () => - buildTargets.all.filter(target => + buildTargets.allCommon.filter(target => buildTargets.buildTargetSources(target.id).nonEmpty ) }, { (id, symbol) => if (isBloop()) doCompile(id) - buildTargets.scalacOptions(id) match { - case None => - Nil.iterator - case Some(info) => - classpath.symbols(info.getClassDirectory().toAbsolutePath, symbol) - } + buildTargets + .targetClassDirectories(id) + .flatMap(cd => classpath.symbols(cd.toAbsolutePath, symbol)) + .iterator } ) @@ -102,7 +100,7 @@ class MetalsTreeViewProvider( !isCollapsed.getOrElse(id, true) && isVisible(Project) } - .flatMap(buildTargets.scalaTarget) + .flatMap(buildTargets.commonTarget) .toArray if (toUpdate.nonEmpty) { val nodes = toUpdate.map { target => @@ -117,9 +115,9 @@ class MetalsTreeViewProvider( } override def onBuildTargetDidCompile(id: BuildTargetIdentifier): Unit = { - buildTargets.scalaTarget(id).foreach { target => - classpath.clearCache(target.scalac.getClassDirectory().toAbsolutePath) - } + buildTargets + .targetClassDirectories(id) + .foreach(cd => classpath.clearCache(cd.toAbsolutePath)) if (isCollapsed.contains(id)) { pendingProjectUpdates.add(id) flushPendingProjectUpdates() @@ -213,7 +211,7 @@ class MetalsTreeViewProvider( ) case Project => Option(params.nodeUri) match { - case None if buildTargets.all.nonEmpty => + case None if buildTargets.allTargets.nonEmpty => Array( projects.root, libraries.root diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala b/mtags/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala index 6fb7fac2073..276950e05e7 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/CommonMtagsEnrichments.scala @@ -342,6 +342,8 @@ trait CommonMtagsEnrichments { doc.endsWith(".worksheet.sc") def isScalaFilename: Boolean = doc.isScala || isScalaScript || isSbt + def isJavaFilename: Boolean = + doc.endsWith(".java") def isAmmoniteGeneratedFile: Boolean = doc.endsWith(".sc.scala") def isAmmoniteScript: Boolean = @@ -363,6 +365,8 @@ trait CommonMtagsEnrichments { implicit class XtensionRelativePathMetals(file: RelativePath) { def filename: String = file.toNIO.filename def isScalaFilename: Boolean = filename.isScalaFilename + def isJavaFilename: Boolean = filename.isJavaFilename + def isScalaOrJavaFilename: Boolean = isScalaFilename || isJavaFilename } implicit class XtensionStream[A](stream: java.util.stream.Stream[A]) { @@ -470,6 +474,9 @@ trait CommonMtagsEnrichments { def isWorksheet: Boolean = { filename.endsWith(".worksheet.sc") } + def isJavaFilename: Boolean = { + filename.isJavaFilename + } def isScalaFilename: Boolean = { filename.isScalaFilename } diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/SemanticdbClasspath.scala b/mtags/src/main/scala/scala/meta/internal/mtags/SemanticdbClasspath.scala index a741e52568c..64c69d6eb4e 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/SemanticdbClasspath.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/SemanticdbClasspath.scala @@ -19,20 +19,22 @@ final case class SemanticdbClasspath( val loader = new ClasspathLoader() loader.addClasspath(classpath) - def getSemanticdbPath(scalaPath: AbsolutePath): AbsolutePath = { - semanticdbPath(scalaPath).getOrElse( - throw new NoSuchElementException(scalaPath.toString()) + def getSemanticdbPath(scalaOrJavaPath: AbsolutePath): AbsolutePath = { + semanticdbPath(scalaOrJavaPath).getOrElse( + throw new NoSuchElementException(scalaOrJavaPath.toString()) ) } - def resourcePath(scalaPath: AbsolutePath): RelativePath = { - mtags.SemanticdbClasspath.fromScala(scalaPath.toRelative(sourceroot)) + def resourcePath(scalaOrJavaPath: AbsolutePath): RelativePath = { + mtags.SemanticdbClasspath.fromScalaOrJava( + scalaOrJavaPath.toRelative(sourceroot) + ) } - def semanticdbPath(scalaPath: AbsolutePath): Option[AbsolutePath] = { - loader.load(resourcePath(scalaPath)) + def semanticdbPath(scalaOrJavaPath: AbsolutePath): Option[AbsolutePath] = { + loader.load(resourcePath(scalaOrJavaPath)) } - def textDocument(scalaPath: AbsolutePath): TextDocumentLookup = { + def textDocument(scalaOrJavaPath: AbsolutePath): TextDocumentLookup = { Semanticdbs.loadTextDocument( - scalaPath, + scalaOrJavaPath, sourceroot, charset, fingerprints, @@ -60,8 +62,8 @@ object SemanticdbClasspath { .dealias } } - def fromScala(path: RelativePath): RelativePath = { - require(path.isScalaFilename, path.toString) + def fromScalaOrJava(path: RelativePath): RelativePath = { + require(path.isScalaOrJavaFilename, path.toString) val semanticdbSibling = path.resolveSibling(_ + ".semanticdb") val semanticdbPrefix = RelativePath("META-INF").resolve("semanticdb") semanticdbPrefix.resolve(semanticdbSibling) diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Semanticdbs.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Semanticdbs.scala index 8cf0c3deafc..bbdb7f127b8 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/Semanticdbs.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Semanticdbs.scala @@ -26,24 +26,24 @@ object Semanticdbs { } def loadTextDocument( - scalaPath: AbsolutePath, + scalaOrJavaPath: AbsolutePath, sourceroot: AbsolutePath, charset: Charset, fingerprints: Md5Fingerprints, loader: RelativePath => Option[FoundSemanticDbPath] ): TextDocumentLookup = { - if (scalaPath.toNIO.getFileSystem != sourceroot.toNIO.getFileSystem) { - TextDocumentLookup.NotFound(scalaPath) + if (scalaOrJavaPath.toNIO.getFileSystem != sourceroot.toNIO.getFileSystem) { + TextDocumentLookup.NotFound(scalaOrJavaPath) } else { - val scalaRelativePath = scalaPath.toRelative(sourceroot.dealias) + val scalaRelativePath = scalaOrJavaPath.toRelative(sourceroot.dealias) val semanticdbRelativePath = - SemanticdbClasspath.fromScala(scalaRelativePath) + SemanticdbClasspath.fromScalaOrJava(scalaRelativePath) loader(semanticdbRelativePath) match { case None => - TextDocumentLookup.NotFound(scalaPath) + TextDocumentLookup.NotFound(scalaOrJavaPath) case Some(semanticdbPath) => loadResolvedTextDocument( - scalaPath, + scalaOrJavaPath, semanticdbPath.nonDefaultRelPath.getOrElse(scalaRelativePath), semanticdbPath.path, charset, diff --git a/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala b/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala index fa42d6ddda9..dce3a9009f5 100644 --- a/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala +++ b/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala @@ -192,7 +192,7 @@ class SbtServerSuite ) // assert contains the meta-build-target-build assertNoDiff( - server.server.buildTargets.all + server.server.buildTargets.allCommon .map(_.displayName) .toSeq .sorted diff --git a/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala b/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala index b35f328477b..a49780ffd61 100644 --- a/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala +++ b/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala @@ -104,6 +104,7 @@ object MetalsTestEnrichments { libraries.flatMap(_.classpath.entries).map(_.toURI.toString).asJava, "" ) + // TODO(@arthurm1) test javacOptions? wsp.buildTargets.addScalacOptions( new ScalacOptionsResult(List(item).asJava) ) diff --git a/tests/unit/src/main/scala/tests/TestingServer.scala b/tests/unit/src/main/scala/tests/TestingServer.scala index d115d4081a7..b8a04ac3519 100644 --- a/tests/unit/src/main/scala/tests/TestingServer.scala +++ b/tests/unit/src/main/scala/tests/TestingServer.scala @@ -1451,7 +1451,7 @@ final class TestingServer( .map(_.getId().getUri()) .getOrElse { val alternatives = - server.buildTargets.all.map(_.displayName).mkString(" ") + server.buildTargets.allTargets.map(_.getDisplayName()).mkString(" ") throw new NoSuchElementException( s"$displayName (alternatives: ${alternatives}" ) diff --git a/tests/unit/src/test/scala/tests/DefinitionDirectorySuite.scala b/tests/unit/src/test/scala/tests/DefinitionDirectorySuite.scala index ee2966d0eea..3188ee75d6b 100644 --- a/tests/unit/src/test/scala/tests/DefinitionDirectorySuite.scala +++ b/tests/unit/src/test/scala/tests/DefinitionDirectorySuite.scala @@ -5,7 +5,7 @@ import scala.meta.internal.mtags.OnDemandSymbolIndex import scala.meta.internal.mtags.Symbol class DefinitionDirectorySuite extends BaseSuite { - test("basic") { + test("basicScala") { val index = OnDemandSymbolIndex.empty() def assertDefinition(sym: String): Unit = { val definition = index.definition(Symbol(sym)) @@ -23,4 +23,20 @@ class DefinitionDirectorySuite extends BaseSuite { assertDefinition("com/foo/Foo#") assertDefinition("com/foo/Bar.") } + test("basicJava") { + val index = OnDemandSymbolIndex.empty() + def assertDefinition(sym: String): Unit = { + val definition = index.definition(Symbol(sym)) + if (definition.isEmpty) throw new NoSuchElementException(sym) + } + val root = FileLayout.fromString( + """ + |/com/foo/Foo.java + |package com.foo; + |public class Foo {} + |""".stripMargin + ) + index.addSourceDirectory(root, dialects.Scala213) + assertDefinition("com/foo/Foo.") + } } diff --git a/tests/unit/src/test/scala/tests/NewFileLspSuite.scala b/tests/unit/src/test/scala/tests/NewFileLspSuite.scala index 75eb6305f93..32040a3ac8d 100644 --- a/tests/unit/src/test/scala/tests/NewFileLspSuite.scala +++ b/tests/unit/src/test/scala/tests/NewFileLspSuite.scala @@ -4,6 +4,7 @@ import java.nio.file.FileAlreadyExistsException import java.nio.file.Files import scala.meta.internal.metals.InitializationOptions +import scala.meta.internal.metals.ListParametrizedCommand import scala.meta.internal.metals.Messages.NewScalaFile import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.RecursivelyDelete @@ -352,6 +353,174 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) + check("new-java-class")( + directory = Some("a/src/main/java/foo/"), + fileType = Right(JavaClass), + fileName = Right("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |class Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-class-name-provided")( + directory = Some("a/src/main/java/foo/"), + fileType = Right(JavaClass), + fileName = Left("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |class Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-class-fully-provided")( + directory = Some("a/src/main/java/foo/"), + fileType = Left(JavaClass), + fileName = Left("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |class Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-interface")( + directory = Some("a/src/main/java/foo/"), + fileType = Right(JavaInterface), + fileName = Right("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |interface Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-interface-name-provided")( + directory = Some("a/src/main/java/foo/"), + fileType = Right(JavaInterface), + fileName = Left("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |interface Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-interface-fully-provided")( + directory = Some("a/src/main/java/foo/"), + fileType = Left(JavaInterface), + fileName = Left("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |interface Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-enum")( + directory = Some("a/src/main/java/foo/"), + fileType = Right(JavaEnum), + fileName = Right("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |enum Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-enum-name-provided")( + directory = Some("a/src/main/java/foo/"), + fileType = Right(JavaEnum), + fileName = Left("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |enum Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-enum-fully-provided")( + directory = Some("a/src/main/java/foo/"), + fileType = Left(JavaEnum), + fileName = Left("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = s"""|package foo; + | + |enum Foo { + |$indent + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-record")( + directory = Some("a/src/main/java/foo/"), + fileType = Right(JavaRecord), + fileName = Right("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = """|package foo; + | + |record Foo() { + | + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-record-name-provided")( + directory = Some("a/src/main/java/foo/"), + fileType = Right(JavaRecord), + fileName = Left("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = """|package foo; + | + |record Foo() { + | + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + + check("new-java-record-fully-provided")( + directory = Some("a/src/main/java/foo/"), + fileType = Left(JavaRecord), + fileName = Left("Foo"), + expectedFilePath = "a/src/main/java/foo/Foo.java", + expectedContent = """|package foo; + | + |record Foo() { + | + |} + |""".stripMargin, + command = ServerCommands.NewJavaFile + ) + private lazy val indent = " " type ProvidedFileType = NewFileType @@ -369,6 +538,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { fileName: Either[Provided, Picked], expectedFilePath: String, expectedContent: String, + command: ListParametrizedCommand[String] = ServerCommands.NewScalaFile, existingFiles: String = "", expectedException: List[Class[_]] = Nil, scalaVersion: Option[String] = None @@ -444,10 +614,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { ) _ <- server - .executeCommand( - ServerCommands.NewScalaFile, - args: _* - ) + .executeCommand(command, args: _*) _ = { assertNoDiff( client.workspaceMessageRequests, diff --git a/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala b/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala index cdb122da9c7..18eb8b50952 100644 --- a/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala +++ b/tests/unit/src/test/scala/tests/TreeViewLspSuite.scala @@ -33,7 +33,7 @@ class TreeViewLspSuite extends BaseLspSuite("tree-view") { "org.eclipse.lsp4j.generator", "org.eclipse.lsp4j.jsonrpc", "org.eclipse.xtend.lib", "org.eclipse.xtend.lib.macro", "org.eclipse.xtext.xbase.lib", "scala-library", "scala-reflect", - "sourcecode_2.12" + "semanticdb-javac", "sourcecode_2.12" ) if (scala.util.Properties.isJavaAtLeast(9.toString)) { From 4dacf2946914399d5bf3e6e7ab56f5ba14671236 Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Sat, 4 Dec 2021 13:52:51 +0000 Subject: [PATCH 2/8] Java handling fixes --- build.sbt | 2 +- .../meta/internal/metals/BuildTargets.scala | 5 +- .../internal/metals/FileDecoderProvider.scala | 38 ++-- .../metals/JavaFormattingProvider.scala | 29 +--- .../internal/metals/MetalsEnrichments.scala | 25 ++- .../metals/MetalsLanguageServer.scala | 5 +- .../internal/metals/UserConfiguration.scala | 4 - .../scala/meta/internal/metals/Warnings.scala | 151 +++++++++------- .../metals/newScalaFile/NewFileProvider.scala | 18 +- .../metals/newScalaFile/NewFileTypes.scala | 27 +-- .../scala/tests/MetalsTestEnrichments.scala | 1 - .../test/scala/tests/NewFileLspSuite.scala | 164 +++++++++++------- 12 files changed, 256 insertions(+), 213 deletions(-) diff --git a/build.sbt b/build.sbt index 16623578417..0f233d7e835 100644 --- a/build.sbt +++ b/build.sbt @@ -540,7 +540,7 @@ lazy val metals = project // for finding paths of global log/cache directories "dev.dirs" % "directories" % "26", // for Java formatting - "org.eclipse.jdt" % "org.eclipse.jdt.core" % "3.26.0", + "org.eclipse.jdt" % "org.eclipse.jdt.core" % "3.25.0", // ================== // Scala dependencies // ================== diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala index 8cdc570a367..2b16c596fef 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala @@ -73,10 +73,9 @@ final class BuildTargets( if (isSupportedScalaVersion) score <<= 2 val usesJavac = javacOptions(t).nonEmpty - if (usesJavac) score <<= 1 - val isJVM = scalacOptions(t).exists(_.isJVM) - if (isJVM) score <<= 1 + if (usesJavac) score <<= 1 + else if (isJVM) score <<= 1 // note(@tgodzik) once the support for Scala 3 is on par with Scala 2 this can be removed val isScala2 = scalaInfo(t).exists(info => diff --git a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala index fc358213197..401993df132 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FileDecoderProvider.scala @@ -93,6 +93,13 @@ final class FileDecoderProvider( targetId: BuildTargetIdentifier, path: AbsolutePath ) + private case class BuildTargetMetadata( + targetId: BuildTargetIdentifier, + classDir: AbsolutePath, + targetRoot: AbsolutePath, + workspaceDir: AbsolutePath, + sourceRoot: AbsolutePath + ) /** * URI format... @@ -283,13 +290,13 @@ final class FileDecoderProvider( newExtension: String ): Either[String, PathInfo] = findBuildTargetMetadata(sourceFile) - .map { case (targetId, classDir, _, _, sourceRoot) => + .map(metadata => { val oldExtension = sourceFile.extension val relativePath = sourceFile - .toRelative(sourceRoot) + .toRelative(metadata.sourceRoot) .resolveSibling(_.stripSuffix(oldExtension) + newExtension) - PathInfo(targetId, classDir.resolve(relativePath)) - } + PathInfo(metadata.targetId, metadata.classDir.resolve(relativePath)) + }) private def findPathInfoForClassesPathFile( path: AbsolutePath @@ -329,9 +336,8 @@ final class FileDecoderProvider( DecoderResponse.failed(requestedURI, _) ) } yield { - val (targetId, classDir, _, _, _) = buildMetadata - val pathToResource = classDir.resolve(resourcePath) - PathInfo(targetId, pathToResource) + val pathToResource = buildMetadata.classDir.resolve(resourcePath) + PathInfo(buildMetadata.targetId, pathToResource) } response match { case Left(decoderResponse) => Future.successful(decoderResponse) @@ -368,15 +374,14 @@ final class FileDecoderProvider( ): Either[String, AbsolutePath] = for { metadata <- findBuildTargetMetadata(sourceFile) - (_, _, targetRoot, workspaceDirectory, _) = metadata foundSemanticDbPath <- { val relativePath = SemanticdbClasspath.fromScalaOrJava( - sourceFile.toRelative(workspaceDirectory.dealias) + sourceFile.toRelative(metadata.workspaceDir.dealias) ) fileSystemSemanticdbs .findSemanticDb( relativePath, - targetRoot, + metadata.targetRoot, sourceFile, workspace ) @@ -410,16 +415,7 @@ final class FileDecoderProvider( private def findBuildTargetMetadata( sourceFile: AbsolutePath - ): Either[ - String, - ( - BuildTargetIdentifier, - AbsolutePath, - AbsolutePath, - AbsolutePath, - AbsolutePath - ) - ] = { + ): Either[String, BuildTargetMetadata] = { val metadata = for { targetId <- buildTargets.inverseSources(sourceFile) workspaceDirectory <- buildTargets.workspaceDirectory(targetId) @@ -429,7 +425,7 @@ final class FileDecoderProvider( findJavaBuildTargetMetadata(targetId, sourceFile) else findScalaBuildTargetMetadata(targetId, sourceFile) - } yield ( + } yield BuildTargetMetadata( targetId, classDir.toAbsolutePath, targetroot, diff --git a/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala index 86b5f1fd70b..f58b7675c52 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala @@ -31,10 +31,6 @@ final class JavaFormattingProvider( )(implicit ec: ExecutionContext ) { - // TODO cache formatting file and reload on change? - // TODO progress monitor? - // TODO onTypeFormatting - wait until presentation compiler is integrated - private def decodeProfile(node: Node): Map[String, String] = { val settings = for { child <- node.child @@ -112,16 +108,7 @@ final class JavaFormattingProvider( } } - private def fromLSP(input: Input, range: l.Range): m.Position.Range = - m.Position.Range( - input, - range.getStart().getLine(), - range.getStart().getCharacter(), - range.getEnd().getLine(), - range.getEnd().getCharacter() - ) - - private def fromLSP(input: Input): m.Position.Range = + private def fromLSP(input: Input): Position.Range = m.Position.Range(input, 0, input.chars.length) def format( @@ -131,7 +118,7 @@ final class JavaFormattingProvider( val range = params.getRange val path = params.getTextDocument.getUri.toAbsolutePath val input = path.toInputFromBuffers(buffers) - runFormat(path, input, options, fromLSP(input, range)).asJava + runFormat(path, input, options, range.toMeta(input)).asJava } def format( @@ -144,7 +131,7 @@ final class JavaFormattingProvider( path: AbsolutePath, input: Input, formattingOptions: l.FormattingOptions, - range: m.Position.Range + range: m.Position ): List[l.TextEdit] = { // if source/target/compliance versions aren't defined by the user then fallback on the build target info var options = loadEclipseFormatConfig @@ -161,8 +148,7 @@ final class JavaFormattingProvider( targetVersion <- java.targetVersion } yield ((sourceVersion, targetVersion)) - version - .take(1) + version.headOption .foreach(version => { val (sourceVersion, targetVersion) = version val complianceVersion = @@ -189,11 +175,8 @@ final class JavaFormattingProvider( val codeFormatter = ToolFactory.createCodeFormatter(options.asJava) - val kind = { - if (userConfig().enableFormatJavaComments) - CodeFormatter.F_INCLUDE_COMMENTS - else 0 - } | CodeFormatter.K_COMPILATION_UNIT + val kind = + CodeFormatter.F_INCLUDE_COMMENTS | CodeFormatter.K_COMPILATION_UNIT val code = input.text val doc = new Document(code) val codeOffset = range.start diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index c1c406bfbaa..4b5c6033186 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -76,15 +76,10 @@ object MetalsEnrichments def isSbtBuild: Boolean = dataKind == "sbt" - def baseDirectory: String = { - val baseDir = buildTarget.getBaseDirectory() - if (baseDir != null) baseDir else "" - } + def baseDirectory: String = + Option(buildTarget.getBaseDirectory()).getOrElse("") - def dataKind: String = { - val dataking = buildTarget.getDataKind() - if (dataking != null) dataking else "" - } + def dataKind: String = Option(buildTarget.getDataKind()).getOrElse("") def asScalaBuildTarget: Option[b.ScalaBuildTarget] = { if (isSbtBuild) { @@ -742,12 +737,16 @@ object MetalsEnrichments item.getOptions.asScala .find(_.startsWith("-Xplugin:semanticdb")) .map(arg => { - val targetrootPos = arg.indexOf("-targetroot:") - val sourcerootPos = arg.indexOf("-sourceroot:") - if (targetrootPos > sourcerootPos) - arg.substring(targetrootPos + 12).trim() + val targetRootOpt = "-targetroot:" + val sourceRootOpt = "-sourceroot:" + val targetRootPos = arg.indexOf(targetRootOpt) + val sourceRootPos = arg.indexOf(sourceRootOpt) + if (targetRootPos > sourceRootPos) + arg.substring(targetRootPos + targetRootOpt.size).trim() else - arg.substring(sourcerootPos + 12, targetrootPos - 1).trim() + arg + .substring(sourceRootPos + sourceRootOpt.size, targetRootPos - 1) + .trim() }) .filter(_ != "javac-classes-directory") .map(AbsolutePath(_)) diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala index 72e9d999ae3..ec7d6de6afc 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala @@ -1817,7 +1817,7 @@ class MetalsLanguageServer( val name = args.lift(1).flatten val fileType = args.lift(2).flatten newFileProvider - .handleFileCreation(directoryURI, name, fileType, true) + .handleFileCreation(directoryURI, name, fileType, isScala = true) .asJavaObject case ServerCommands.NewJavaFile(args) => @@ -1825,7 +1825,7 @@ class MetalsLanguageServer( val name = args.lift(1).flatten val fileType = args.lift(2).flatten newFileProvider - .handleFileCreation(directoryURI, name, fileType, false) + .handleFileCreation(directoryURI, name, fileType, isScala = false) .asJavaObject case ServerCommands.StartAmmoniteBuildServer() => @@ -2480,7 +2480,6 @@ class MetalsLanguageServer( val isVisited = new ju.HashSet[String]() for { item <- dependencySources.getItems.asScala - // TODO(arthurm1) add java sources? What dialect? scalaTarget <- buildTargets.scalaTarget(item.getTarget) sourceUri <- Option(item.getSources).toList.flatMap(_.asScala) path = sourceUri.toAbsolutePath diff --git a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala index 8dea19f5a4d..98de5ffdfc1 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala @@ -45,7 +45,6 @@ case class UserConfiguration( fallbackScalaVersion: Option[String] = None, testUserInterface: TestUserInterfaceKind = TestUserInterfaceKind.CodeLenses, eclipseFormatConfigPath: Option[AbsolutePath] = None, - enableFormatJavaComments: Boolean = true, eclipseFormatProfile: Option[String] = None ) { @@ -451,8 +450,6 @@ object UserConfiguration { } val eclipseFormatConfigPath = getStringKey("eclipse-format-config-path").map(AbsolutePath(_)) - val shouldFormatJavaComments = - getBooleanKey("enable-format-java-comments").getOrElse(false) val eclipseFormatProfile = getStringKey("eclipse-format-profile") if (errors.isEmpty) { @@ -482,7 +479,6 @@ object UserConfiguration { defaultScalaVersion, disableTestCodeLenses, eclipseFormatConfigPath, - shouldFormatJavaComments, eclipseFormatProfile ) ) diff --git a/metals/src/main/scala/scala/meta/internal/metals/Warnings.scala b/metals/src/main/scala/scala/meta/internal/metals/Warnings.scala index 69fc8f91db0..1a12989a609 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Warnings.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Warnings.scala @@ -24,74 +24,107 @@ final class Warnings( def noSemanticdb(path: AbsolutePath): Unit = { def doesntWorkBecause = s"code navigation does not work for the file '$path' because" - def buildMisconfiguration(): Unit = { - statusBar.addMessage( - MetalsStatusParams( - s"${icons.alert}Build misconfiguration", - command = ClientCommands.RunDoctor.id - ) - ) - } - val isReported: Option[Unit] = for { - buildTarget <- buildTargets.inverseSources(path) - // TODO(arthurm1) add javaTarget warnings (targetroot, sourceroot, semanticDB etc.) - info <- buildTargets.scalaTarget(buildTarget) - scalacOptions <- buildTargets.scalacOptions(buildTarget) - } yield { - if (!info.isSemanticdbEnabled) { - if (isSupportedAtReleaseMomentScalaVersion(info.scalaVersion)) { - logger.error( - s"$doesntWorkBecause the SemanticDB compiler plugin is not enabled for the build target ${info.displayName}." - ) - buildMisconfiguration() - } else { - logger.error( - s"$doesntWorkBecause the Scala version ${info.scalaVersion} is not supported. " + - s"To fix this problem, change the Scala version to ${isLatestScalaVersion.mkString(" or ")}." - ) - statusBar.addMessage( - s"${icons.alert}Unsupported Scala ${info.scalaVersion}" + + def reportSemanticDB( + buildTarget: BuildTargetIdentifier, + targetName: String + ): Unit = { + def pluginNotEnabled(language: String) = + s"$doesntWorkBecause the $language SemanticDB compiler plugin is not enabled for the build target $targetName." + + def missingCompilerOption( + language: String, + option: String + ) = + s"$doesntWorkBecause the build target $targetName is missing the $language compiler option $option. " + + "To fix this problems, update the build settings to include this compiler option." + + def buildMisconfiguration(): Unit = { + statusBar.addMessage( + MetalsStatusParams( + s"${icons.alert}Build misconfiguration", + command = ClientCommands.RunDoctor.id ) + ) + } + + if (path.isJavaFilename) + buildTargets.javaTarget(buildTarget) match { + case None => + logger.error(pluginNotEnabled("Java")) + buildMisconfiguration() + case Some(target) => + if (!target.isSemanticdbEnabled) { + logger.error(pluginNotEnabled("Java")) + buildMisconfiguration() + } else if (!target.isSourcerootDeclared) { + val option = workspace.javaSourcerootOption + logger.error(missingCompilerOption("Java", option)) + buildMisconfiguration() + } } - } else { - if (!info.isSourcerootDeclared) { - val option = workspace.scalaSourcerootOption - logger.error( - s"$doesntWorkBecause the build target ${info.displayName} is missing the compiler option $option. " + - s"To fix this problems, update the build settings to include this compiler option." - ) - buildMisconfiguration() - } else if (isCompiling(buildTarget)) { - val tryAgain = "Wait until compilation is finished and try again" - logger.error( - s"$doesntWorkBecause the build target ${info.displayName} is being compiled. $tryAgain." - ) - statusBar.addMessage(icons.info + tryAgain) - } else if (!path.isSbt && !path.isWorksheet) { - val targetRoot = scalacOptions.targetroot(info.scalaVersion) - val targetfile = targetRoot - .resolve( - SemanticdbClasspath.fromScalaOrJava(path.toRelative(workspace)) - ) - logger.error( - s"$doesntWorkBecause the SemanticDB file '$targetfile' doesn't exist. " + - s"There can be many reasons for this error. " - ) + else + buildTargets.scalaTarget(buildTarget) match { + case None => + logger.error(pluginNotEnabled("Scala")) + buildMisconfiguration() + case Some(target) => + if (!target.isSemanticdbEnabled) { + if (isSupportedAtReleaseMomentScalaVersion(target.scalaVersion)) { + logger.error(pluginNotEnabled("Scala")) + buildMisconfiguration() + } else { + logger.error( + s"$doesntWorkBecause the Scala version ${target.scalaVersion} is not supported. " + + s"To fix this problem, change the Scala version to ${isLatestScalaVersion.mkString(" or ")}." + ) + statusBar.addMessage( + s"${icons.alert}Unsupported Scala ${target.scalaVersion}" + ) + } + } else if (!target.isSourcerootDeclared) { + val option = workspace.scalaSourcerootOption + logger.error(missingCompilerOption("Scala", option)) + buildMisconfiguration() + } else if (!path.isSbt && !path.isWorksheet) { + val targetRoot = target.targetroot + val targetfile = targetRoot + .resolve( + SemanticdbClasspath.fromScalaOrJava( + path.toRelative(workspace) + ) + ) + logger.error( + s"$doesntWorkBecause the SemanticDB file '$targetfile' doesn't exist. " + + s"There can be many reasons for this error. " + ) + } } - } } - isReported match { - case Some(()) => - case None => - if (buildTools.isEmpty) { - noBuildTool() - } else { + + if (buildTools.isEmpty) + noBuildTool() + else + buildTargets.inverseSources(path) match { + case None => { logger.warn( s"$doesntWorkBecause it doesn't belong to a build target." ) statusBar.addMessage(s"${icons.alert}No build target") } - } + case Some(buildTarget) => + val targetName = buildTargets + .commonTarget(buildTarget) + .map(_.displayName) + .getOrElse("Unknown") + if (isCompiling(buildTarget)) { + val tryAgain = "Wait until compilation is finished and try again" + logger.error( + s"$doesntWorkBecause the build target $targetName is being compiled. $tryAgain." + ) + statusBar.addMessage(icons.info + tryAgain) + } else reportSemanticDB(buildTarget, targetName) + } } def noBuildTool(): Unit = { diff --git a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala index 2fdcb966bf6..cb5a6f8464e 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala @@ -5,6 +5,7 @@ import java.nio.file.FileAlreadyExistsException import scala.concurrent.ExecutionContext import scala.concurrent.Future +import scala.util.Properties import scala.util.control.NonFatal import scala.meta.internal.metals.ClientCommands @@ -143,14 +144,15 @@ class NewFileProvider( } private def askForJavaKind: Future[Option[NewFileType]] = { - askForKind( - List( - JavaClass, - JavaInterface, - JavaEnum, - JavaRecord - ) + val allFileTypes = List( + JavaClass, + JavaInterface, + JavaEnum ) + val withRecord = + if (Properties.isJavaAtLeast("14")) allFileTypes :+ JavaRecord + else allFileTypes + askForKind(withRecord) } private def askForName(kind: String): Future[Option[String]] = { @@ -187,7 +189,7 @@ class NewFileProvider( case CaseClass => caseClassTemplate(className) case Enum => enumTemplate(kind.id, className) case JavaRecord => javaRecordTemplate(className) - case _ => classTemplate(kind.syntax, className) + case _ => classTemplate(kind.syntax.getOrElse(""), className) } val editText = template.map { s => packageProvider diff --git a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala index 5f43078706b..57e84b6fea2 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala @@ -5,85 +5,86 @@ import scala.meta.internal.metals.clients.language.MetalsQuickPickItem object NewFileTypes { sealed trait NewFileType { val id: String - val syntax: String + val syntax: Option[String] val label: String def toQuickPickItem: MetalsQuickPickItem = MetalsQuickPickItem(id, label) } case object ScalaFile extends NewFileType { override val id: String = "scala-file" + override val syntax: Option[String] = None override val label: String = "Scala file with automatically added package" } case object Class extends NewFileType { override val id: String = "scala-class" - override val syntax: String = "class" + override val syntax: Option[String] = Some("class") override val label: String = "Class" } case object CaseClass extends NewFileType { override val id: String = "scala-case-class" - override val syntax: String = "NA" + override val syntax: Option[String] = None override val label: String = "Case Class" } case object Enum extends NewFileType { override val id: String = "enum" - override val syntax: String = "NA" + override val syntax: Option[String] = None override val label: String = "Enum" } case object Object extends NewFileType { override val id: String = "scala-object" - override val syntax: String = "object" + override val syntax: Option[String] = Some("object") override val label: String = "Object" } case object Trait extends NewFileType { override val id: String = "scala-trait" - override val syntax: String = "trait" + override val syntax: Option[String] = Some("trait") override val label: String = "Trait" } case object PackageObject extends NewFileType { override val id: String = "scala-package-object" - override val syntax: String = "NA" + override val syntax: Option[String] = None override val label: String = "Package Object" } case object Worksheet extends NewFileType { override val id: String = "scala-worksheet" - override val syntax: String = "NA" + override val syntax: Option[String] = None override val label: String = "Worksheet" } case object AmmoniteScript extends NewFileType { override val id: String = "ammonite-script" - override val syntax: String = "NA" + override val syntax: Option[String] = None override val label: String = "Ammonite Script" } case object JavaClass extends NewFileType { override val id: String = "java-class" - override val syntax: String = "class" + override val syntax: Option[String] = Some("class") override val label: String = "Class" } case object JavaInterface extends NewFileType { override val id: String = "java-interface" - override val syntax: String = "interface" + override val syntax: Option[String] = Some("interface") override val label: String = "Interface" } case object JavaEnum extends NewFileType { override val id: String = "java-enum" - override val syntax: String = "enum" + override val syntax: Option[String] = Some("enum") override val label: String = "Enum" } case object JavaRecord extends NewFileType { override val id: String = "java-record" - override val syntax: String = "NA" + override val syntax: Option[String] = None override val label: String = "Record" } diff --git a/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala b/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala index a49780ffd61..b35f328477b 100644 --- a/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala +++ b/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala @@ -104,7 +104,6 @@ object MetalsTestEnrichments { libraries.flatMap(_.classpath.entries).map(_.toURI.toString).asJava, "" ) - // TODO(@arthurm1) test javacOptions? wsp.buildTargets.addScalacOptions( new ScalacOptionsResult(List(item).asJava) ) diff --git a/tests/unit/src/test/scala/tests/NewFileLspSuite.scala b/tests/unit/src/test/scala/tests/NewFileLspSuite.scala index 32040a3ac8d..d8367c9d388 100644 --- a/tests/unit/src/test/scala/tests/NewFileLspSuite.scala +++ b/tests/unit/src/test/scala/tests/NewFileLspSuite.scala @@ -3,6 +3,8 @@ package tests import java.nio.file.FileAlreadyExistsException import java.nio.file.Files +import scala.util.Properties + import scala.meta.internal.metals.InitializationOptions import scala.meta.internal.metals.ListParametrizedCommand import scala.meta.internal.metals.Messages.NewScalaFile @@ -23,7 +25,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { override def initializationOptions: Option[InitializationOptions] = Some(InitializationOptions.Default.copy(inputBoxProvider = Some(true))) - check("new-worksheet-picked")( + checkScala("new-worksheet-picked")( directory = Some("a/src/main/scala/"), fileType = Right(Worksheet), fileName = Right("Foo"), @@ -31,7 +33,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { expectedContent = "" ) - check("new-worksheet-name-provided")( + checkScala("new-worksheet-name-provided")( directory = Some("a/src/main/scala/"), fileType = Left(Worksheet), fileName = Right("Foo"), @@ -39,7 +41,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { expectedContent = "" ) - check("new-worksheet-fully-provided")( + checkScala("new-worksheet-fully-provided")( directory = Some("a/src/main/scala/"), fileType = Left(Worksheet), fileName = Left("Foo"), @@ -47,7 +49,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { expectedContent = "" ) - check("new-ammonite-script")( + checkScala("new-ammonite-script")( directory = Some("a/src/main/scala/"), fileType = Right(AmmoniteScript), fileName = Right("Foo"), @@ -55,7 +57,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { expectedContent = "" ) - check("new-ammonite-script-name-provided")( + checkScala("new-ammonite-script-name-provided")( directory = Some("a/src/main/scala/"), fileType = Right(AmmoniteScript), fileName = Left("Foo"), @@ -63,7 +65,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { expectedContent = "" ) - check("new-ammonite-script-fully-provided")( + checkScala("new-ammonite-script-fully-provided")( directory = Some("a/src/main/scala/"), fileType = Left(AmmoniteScript), fileName = Left("Foo"), @@ -71,7 +73,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { expectedContent = "" ) - check("new-class")( + checkScala("new-class")( directory = Some("a/src/main/scala/foo/"), fileType = Right(Class), fileName = Right("Foo"), @@ -84,7 +86,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-class-backticked")( + checkScala("new-class-backticked")( directory = Some("a/src/main/scala/this/"), fileType = Right(Class), fileName = Right("type"), @@ -97,7 +99,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-class-name-provided")( + checkScala("new-class-name-provided")( directory = Some("a/src/main/scala/foo/"), fileType = Right(Class), fileName = Left("Foo"), @@ -110,7 +112,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-class-fully-provided")( + checkScala("new-class-fully-provided")( directory = Some("a/src/main/scala/foo/"), fileType = Left(Class), fileName = Left("Foo"), @@ -123,7 +125,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-case-class")( + checkScala("new-case-class")( directory = Some("a/src/main/scala/foo/"), fileType = Right(CaseClass), fileName = Right("Foo"), @@ -134,7 +136,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-case-class-name-provided")( + checkScala("new-case-class-name-provided")( directory = Some("a/src/main/scala/foo/"), fileType = Right(CaseClass), fileName = Left("Foo"), @@ -145,7 +147,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-case-class-fully-provided")( + checkScala("new-case-class-fully-provided")( directory = Some("a/src/main/scala/foo/"), fileType = Left(CaseClass), fileName = Left("Foo"), @@ -156,7 +158,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-object-null-dir")( + checkScala("new-object-null-dir")( directory = None, fileType = Right(Object), fileName = Right("Bar"), @@ -167,7 +169,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-object-null-dir-name-provided")( + checkScala("new-object-null-dir-name-provided")( directory = None, fileType = Right(Object), fileName = Left("Bar"), @@ -178,7 +180,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-object-null-dir")( + checkScala("new-object-null-dir")( directory = None, fileType = Left(Object), fileName = Left("Bar"), @@ -189,7 +191,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-trait-new-dir")( + checkScala("new-trait-new-dir")( directory = Some("a/src/main/scala/"), fileType = Right(Trait), fileName = Right("bar/Baz"), @@ -202,7 +204,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-trait-new-dir-name-provided")( + checkScala("new-trait-new-dir-name-provided")( directory = Some("a/src/main/scala/"), fileType = Right(Trait), fileName = Left("bar/Baz"), @@ -215,7 +217,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-trait-new-dir-fully-provided")( + checkScala("new-trait-new-dir-fully-provided")( directory = Some("a/src/main/scala/"), fileType = Right(Trait), fileName = Right("bar/Baz"), @@ -228,7 +230,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-package-object")( + checkScala("new-package-object")( directory = Some("a/src/main/scala/foo"), fileType = Right(PackageObject), fileName = Right( @@ -241,7 +243,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-package-object-provided")( + checkScala("new-package-object-provided")( directory = Some("a/src/main/scala/foo"), fileType = Left(PackageObject), fileName = Right( @@ -254,7 +256,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-class-on-file")( + checkScala("new-class-on-file")( directory = Some("a/src/main/scala/foo/Other.scala"), fileType = Right(Class), fileName = Right("Foo"), @@ -272,7 +274,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-class-on-file-name-provided")( + checkScala("new-class-on-file-name-provided")( directory = Some("a/src/main/scala/foo/Other.scala"), fileType = Right(Class), fileName = Left("Foo"), @@ -290,7 +292,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-class-on-file-fully-provided")( + checkScala("new-class-on-file-fully-provided")( directory = Some("a/src/main/scala/foo/Other.scala"), fileType = Right(Class), fileName = Right("Foo"), @@ -308,7 +310,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("existing-file")( + checkScala("existing-file")( directory = Some("a/src/main/scala/foo"), fileType = Right(Class), fileName = Right("Other"), @@ -329,7 +331,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { expectedException = List(classOf[FileAlreadyExistsException]) ) - check("scala3-enum")( + checkScala("scala3-enum")( directory = Some("a/src/main/scala/foo"), fileType = Right(Enum), fileName = Right("Color"), @@ -343,7 +345,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { scalaVersion = Some(V.scala3) ) - check("empty-file-with-package")( + checkScala("empty-file-with-package")( directory = Some("a/src/main/scala/foo"), fileType = Right(ScalaFile), fileName = Right("Foo"), @@ -353,7 +355,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |""".stripMargin ) - check("new-java-class")( + checkJava("new-java-class")( directory = Some("a/src/main/java/foo/"), fileType = Right(JavaClass), fileName = Right("Foo"), @@ -363,11 +365,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |class Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-class-name-provided")( + checkJava("new-java-class-name-provided")( directory = Some("a/src/main/java/foo/"), fileType = Right(JavaClass), fileName = Left("Foo"), @@ -377,11 +378,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |class Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-class-fully-provided")( + checkJava("new-java-class-fully-provided")( directory = Some("a/src/main/java/foo/"), fileType = Left(JavaClass), fileName = Left("Foo"), @@ -391,11 +391,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |class Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-interface")( + checkJava("new-java-interface")( directory = Some("a/src/main/java/foo/"), fileType = Right(JavaInterface), fileName = Right("Foo"), @@ -405,11 +404,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |interface Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-interface-name-provided")( + checkJava("new-java-interface-name-provided")( directory = Some("a/src/main/java/foo/"), fileType = Right(JavaInterface), fileName = Left("Foo"), @@ -419,11 +417,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |interface Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-interface-fully-provided")( + checkJava("new-java-interface-fully-provided")( directory = Some("a/src/main/java/foo/"), fileType = Left(JavaInterface), fileName = Left("Foo"), @@ -433,11 +430,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |interface Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-enum")( + checkJava("new-java-enum")( directory = Some("a/src/main/java/foo/"), fileType = Right(JavaEnum), fileName = Right("Foo"), @@ -447,11 +443,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |enum Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-enum-name-provided")( + checkJava("new-java-enum-name-provided")( directory = Some("a/src/main/java/foo/"), fileType = Right(JavaEnum), fileName = Left("Foo"), @@ -461,11 +456,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |enum Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-enum-fully-provided")( + checkJava("new-java-enum-fully-provided")( directory = Some("a/src/main/java/foo/"), fileType = Left(JavaEnum), fileName = Left("Foo"), @@ -475,11 +469,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { |enum Foo { |$indent |} - |""".stripMargin, - command = ServerCommands.NewJavaFile + |""".stripMargin ) - check("new-java-record")( + checkJava("new-java-record")( directory = Some("a/src/main/java/foo/"), fileType = Right(JavaRecord), fileName = Right("Foo"), @@ -490,10 +483,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { | |} |""".stripMargin, - command = ServerCommands.NewJavaFile + javaMinVersion = Some("14") ) - check("new-java-record-name-provided")( + checkJava("new-java-record-name-provided")( directory = Some("a/src/main/java/foo/"), fileType = Right(JavaRecord), fileName = Left("Foo"), @@ -504,10 +497,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { | |} |""".stripMargin, - command = ServerCommands.NewJavaFile + javaMinVersion = Some("14") ) - check("new-java-record-fully-provided")( + checkJava("new-java-record-fully-provided")( directory = Some("a/src/main/java/foo/"), fileType = Left(JavaRecord), fileName = Left("Foo"), @@ -518,7 +511,7 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { | |} |""".stripMargin, - command = ServerCommands.NewJavaFile + javaMinVersion = Some("14") ) private lazy val indent = " " @@ -528,6 +521,49 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { type Provided = String type Picked = String + private def checkJava(testName: TestOptions)( + directory: Option[String], + fileType: Either[ProvidedFileType, PickedFileType], + fileName: Either[Provided, Picked], + expectedFilePath: String, + expectedContent: String, + existingFiles: String = "", + expectedException: List[Class[_]] = Nil, + javaMinVersion: Option[String] = None + ): Unit = if (Properties.isJavaAtLeast(javaMinVersion.getOrElse("1.8"))) + check(testName)( + directory, + fileType, + fileName, + expectedFilePath, + expectedContent, + ServerCommands.NewJavaFile, + existingFiles, + expectedException, + None + ) + + private def checkScala(testName: TestOptions)( + directory: Option[String], + fileType: Either[ProvidedFileType, PickedFileType], + fileName: Either[Provided, Picked], + expectedFilePath: String, + expectedContent: String, + existingFiles: String = "", + expectedException: List[Class[_]] = Nil, + scalaVersion: Option[String] = None + ): Unit = check(testName)( + directory, + fileType, + fileName, + expectedFilePath, + expectedContent, + ServerCommands.NewScalaFile, + existingFiles, + expectedException, + scalaVersion + ) + /** * NewScalaFile request may include @param fileType and @param fileName (2 x Left) in arguments. * When one of them missing Metals will use quickpick in order to ask the user about lacking information @@ -538,10 +574,10 @@ class NewFileLspSuite extends BaseLspSuite("new-file") { fileName: Either[Provided, Picked], expectedFilePath: String, expectedContent: String, - command: ListParametrizedCommand[String] = ServerCommands.NewScalaFile, - existingFiles: String = "", - expectedException: List[Class[_]] = Nil, - scalaVersion: Option[String] = None + command: ListParametrizedCommand[String], + existingFiles: String, + expectedException: List[Class[_]], + scalaVersion: Option[String] )(implicit loc: Location): Unit = test(testName) { val localScalaVersion = scalaVersion.getOrElse(V.scala212) From bdc4dabe7b6bef5f70912aadae99b0e2fb2b69de Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Fri, 10 Dec 2021 14:15:42 +0000 Subject: [PATCH 3/8] Java handling fixes --- .../metals/ExcludedPackagesHandler.scala | 2 +- .../metals/JavaFormattingProvider.scala | 14 ++++--- .../metals/MetalsLanguageServer.scala | 10 +---- .../internal/metals/SemanticdbIndexer.scala | 17 ++------ .../internal/metals/UserConfiguration.scala | 38 +++++++++--------- .../scala/tests/UserConfigurationSuite.scala | 40 +++++++++++++++++++ 6 files changed, 73 insertions(+), 48 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/ExcludedPackagesHandler.scala b/metals/src/main/scala/scala/meta/internal/metals/ExcludedPackagesHandler.scala index 4ad7d0db516..cf250450b45 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ExcludedPackagesHandler.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ExcludedPackagesHandler.scala @@ -10,7 +10,7 @@ class ExcludedPackagesHandler(pkgsToExclude: Option[List[String]] = None) { val defaultExclusions: List[String] = List( "META-INF/", "images/", "toolbarButtonGraphics/", "jdk/", "sun/", "oracle/", "java/awt/desktop/", "org/jcp/", "org/omg/", "org/graalvm/", "com/oracle/", - "com/sun/", "com/apple/", "apple/" + "com/sun/", "com/apple/", "apple/", "com/sourcegraph/shaded/" ) /** diff --git a/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala index f58b7675c52..a4014982b40 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/JavaFormattingProvider.scala @@ -41,9 +41,9 @@ final class JavaFormattingProvider( } private def parseEclipseFormatFile( - text: String + text: String, + profileName: Option[String] ): Try[Map[String, String]] = { - val profileName = userConfig().eclipseFormatProfile import scala.xml.XML Try { val node = XML.loadString(text) @@ -73,11 +73,15 @@ final class JavaFormattingProvider( DefaultCodeFormatterOptions.getEclipseDefaultSettings.getMap.asScala.toMap private def loadEclipseFormatConfig: Map[String, String] = { - userConfig().eclipseFormatConfigPath - .map(eclipseFormatFile => { + userConfig().javaFormatConfig + .map(javaFormatConfig => { + val eclipseFormatFile = javaFormatConfig.eclipseFormatConfigPath if (eclipseFormatFile.exists) { val text = eclipseFormatFile.toInputFromBuffers(buffers).text - parseEclipseFormatFile(text) match { + parseEclipseFormatFile( + text, + javaFormatConfig.eclipseFormatProfile + ) match { case Failure(e) => scribe.error( s"Failed to parse $eclipseFormatFile. Using default formatting", diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala index ec7d6de6afc..478af5402cf 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala @@ -2403,16 +2403,10 @@ class MetalsLanguageServer( workspaceSymbols.indexClasspath() } timerProvider.timedThunk( - "indexed workspace Scala SemanticDBs", + "indexed workspace SemanticDBs", clientConfig.initialConfig.statistics.isIndex ) { - semanticDBIndexer.onScalacOptions(i.scalacOptions) - } - timerProvider.timedThunk( - "indexed workspace Java SemanticDBs", - clientConfig.initialConfig.statistics.isIndex - ) { - semanticDBIndexer.onJavacOptions(i.javacOptions) + semanticDBIndexer.onTargetRoots() } timerProvider.timedThunk( "indexed workspace sources", diff --git a/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala b/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala index 63cf4d58263..c32fd983608 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala @@ -26,22 +26,11 @@ class SemanticdbIndexer( workspace: AbsolutePath ) { - def onScalacOptions(scalacOptions: ScalacOptionsResult): Unit = { + def onTargetRoots(): Unit = { for { - item <- scalacOptions.getItems.asScala - scalaInfo <- buildTargets.scalaInfo(item.getTarget) + targetRoot <- buildTargets.allTargetRoots } { - val targetroot = item.targetroot(scalaInfo.getScalaVersion) - onChangeDirectory(targetroot.resolve(Directories.semanticdb).toNIO) - } - } - - def onJavacOptions(javacOptions: JavacOptionsResult): Unit = { - for { - item <- javacOptions.getItems.asScala - } { - val targetroot = item.targetroot - onChangeDirectory(targetroot.resolve(Directories.semanticdb).toNIO) + onChangeDirectory(targetRoot.resolve(Directories.semanticdb).toNIO) } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala index 98de5ffdfc1..fea9b549ebc 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala @@ -16,6 +16,11 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +case class JavaFormatConfig( + eclipseFormatConfigPath: AbsolutePath, + eclipseFormatProfile: Option[String] +) + /** * Configuration that the user can override via workspace/didChangeConfiguration. */ @@ -44,8 +49,7 @@ case class UserConfiguration( excludedPackages: Option[List[String]] = None, fallbackScalaVersion: Option[String] = None, testUserInterface: TestUserInterfaceKind = TestUserInterfaceKind.CodeLenses, - eclipseFormatConfigPath: Option[AbsolutePath] = None, - eclipseFormatProfile: Option[String] = None + javaFormatConfig: Option[JavaFormatConfig] = None ) { def currentBloopVersion: String = @@ -253,28 +257,19 @@ object UserConfiguration { "Default way of handling tests and test suites." ), UserConfigurationOption( - "eclipse-format-config-path", + "java-format.eclipse-config-path", """empty string `""`.""", """"formatters/eclipse-formatter.xml"""", - "Eclipse formatter config path", + "Eclipse Java formatter config path", """Optional custom path to the eclipse-formatter.xml file. |Should be an absolute path and use forward slashes `/` for file separators (even on Windows). |""".stripMargin ), UserConfigurationOption( - "format-java-comments", - "false", - "false", - "Should format Java comments as well as code", - """|Default formatting of Java files only formats code. - |Select this to also format comments. - |""".stripMargin - ), - UserConfigurationOption( - "eclipse-format-profile", + "java-format.eclipse-profile", """empty string `""`.""", """"GoogleStyle"""", - "Eclipse formatting profile", + "Eclipse Java formatting profile", """|If the Eclipse formatter file contains more than one profile then specify the required profile name. |""".stripMargin ) @@ -448,9 +443,13 @@ object UserConfiguration { TestUserInterfaceKind.CodeLenses } } - val eclipseFormatConfigPath = - getStringKey("eclipse-format-config-path").map(AbsolutePath(_)) - val eclipseFormatProfile = getStringKey("eclipse-format-profile") + val javaFormatConfig = + getStringKey("java-format.eclipse-config-path").map(f => + JavaFormatConfig( + AbsolutePath(f), + getStringKey("java-format.eclipse-profile") + ) + ) if (errors.isEmpty) { Right( @@ -478,8 +477,7 @@ object UserConfiguration { excludedPackages, defaultScalaVersion, disableTestCodeLenses, - eclipseFormatConfigPath, - eclipseFormatProfile + javaFormatConfig ) ) } else { diff --git a/tests/unit/src/test/scala/tests/UserConfigurationSuite.scala b/tests/unit/src/test/scala/tests/UserConfigurationSuite.scala index ed4873a56c3..ccd051a41ef 100644 --- a/tests/unit/src/test/scala/tests/UserConfigurationSuite.scala +++ b/tests/unit/src/test/scala/tests/UserConfigurationSuite.scala @@ -6,6 +6,8 @@ import scala.meta.internal.metals.ClientConfiguration import scala.meta.internal.metals.UserConfiguration import munit.Location +import scala.meta.internal.metals.JavaFormatConfig +import scala.meta.io.AbsolutePath class UserConfigurationSuite extends BaseSuite { def check( @@ -183,4 +185,42 @@ class UserConfigurationSuite extends BaseSuite { """.stripMargin ) { ok => assert(ok.enableStripMarginOnTypeFormatting == false) } + checkOK( + "java format setting", + """ + |{ + | "javaFormat.eclipseConfigPath": "path", + | "javaFormat.eclipseProfile": "profile" + |} + """.stripMargin + ) { obtained => + assert( + obtained.javaFormatConfig == Some( + JavaFormatConfig(AbsolutePath("path"), Some("profile")) + ) + ) + } + checkOK( + "java format no setting", + """ + |{ + |} + """.stripMargin + ) { obtained => + assert(obtained.javaFormatConfig == None) + } + checkOK( + "java format no profile setting", + """ + |{ + | "javaFormat.eclipseConfigPath": "path" + |} + """.stripMargin + ) { obtained => + assert( + obtained.javaFormatConfig == Some( + JavaFormatConfig(AbsolutePath("path"), None) + ) + ) + } } From 113013fc053bcf08ae4541ccdf3927e7e49db162 Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Fri, 10 Dec 2021 15:56:17 +0000 Subject: [PATCH 4/8] Fix test failures --- .../scala/meta/internal/metals/SemanticdbIndexer.scala | 2 -- .../meta/internal/metals/newScalaFile/NewFileProvider.scala | 6 +++--- .../meta/internal/metals/newScalaFile/NewFileTypes.scala | 2 +- .../scala/tests/codeactions/CreateNewSymbolLspSuite.scala | 6 +++--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala b/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala index c32fd983608..f26332b8445 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/SemanticdbIndexer.scala @@ -14,8 +14,6 @@ import scala.meta.internal.semanticdb.TextDocument import scala.meta.internal.semanticdb.TextDocuments import scala.meta.io.AbsolutePath -import ch.epfl.scala.bsp4j.JavacOptionsResult -import ch.epfl.scala.bsp4j.ScalacOptionsResult import com.google.protobuf.InvalidProtocolBufferException class SemanticdbIndexer( diff --git a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala index cb5a6f8464e..2174ba4145d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileProvider.scala @@ -187,7 +187,7 @@ class NewFileProvider( ) val template = kind match { case CaseClass => caseClassTemplate(className) - case Enum => enumTemplate(kind.id, className) + case Enum => enumTemplate(className) case JavaRecord => javaRecordTemplate(className) case _ => classTemplate(kind.syntax.getOrElse(""), className) } @@ -286,9 +286,9 @@ class NewFileProvider( |""".stripMargin) } - private def enumTemplate(kind: String, name: String): NewFileTemplate = { + private def enumTemplate(name: String): NewFileTemplate = { val indent = " " - NewFileTemplate(s"""|$kind $name { + NewFileTemplate(s"""|enum $name { |${indent}case@@ |} |""".stripMargin) diff --git a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala index 57e84b6fea2..332044c058a 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/newScalaFile/NewFileTypes.scala @@ -29,7 +29,7 @@ object NewFileTypes { } case object Enum extends NewFileType { - override val id: String = "enum" + override val id: String = "scala-enum" override val syntax: Option[String] = None override val label: String = "Enum" } diff --git a/tests/unit/src/test/scala/tests/codeactions/CreateNewSymbolLspSuite.scala b/tests/unit/src/test/scala/tests/codeactions/CreateNewSymbolLspSuite.scala index 61c67dc12b5..20c2e50581b 100644 --- a/tests/unit/src/test/scala/tests/codeactions/CreateNewSymbolLspSuite.scala +++ b/tests/unit/src/test/scala/tests/codeactions/CreateNewSymbolLspSuite.scala @@ -26,7 +26,7 @@ class CreateNewSymbolLspSuite extends BaseCodeActionLspSuite("createNew") { |${ImportMissingSymbol.title("Location", docToolName)} |${CreateNewSymbol.title("Location")}""".stripMargin, selectedActionIndex = 4, - pickedKind = "case-class", + pickedKind = "scala-case-class", newFile = "a/src/main/scala/a/Location.scala" -> """|package a | @@ -46,7 +46,7 @@ class CreateNewSymbolLspSuite extends BaseCodeActionLspSuite("createNew") { |${ImportMissingSymbol.title("Location", docToolName)} |${CreateNewSymbol.title("Location")}""".stripMargin, selectedActionIndex = 4, - pickedKind = "trait", + pickedKind = "scala-trait", newFile = "a/src/main/scala/a/Location.scala" -> s"""|package a | @@ -74,7 +74,7 @@ class CreateNewSymbolLspSuite extends BaseCodeActionLspSuite("createNew") { )} |""".stripMargin, selectedActionIndex = 4, - pickedKind = "class", + pickedKind = "scala-class", newFile = "a/src/main/scala/a/Missing.scala" -> s"""|package a | From 18961e458ab145b4e8976259f428eb3a6999b83f Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Fri, 10 Dec 2021 16:36:26 +0000 Subject: [PATCH 5/8] Fix scalafix import ordering issue --- tests/unit/src/test/scala/tests/UserConfigurationSuite.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/src/test/scala/tests/UserConfigurationSuite.scala b/tests/unit/src/test/scala/tests/UserConfigurationSuite.scala index ccd051a41ef..97ed70bcb0b 100644 --- a/tests/unit/src/test/scala/tests/UserConfigurationSuite.scala +++ b/tests/unit/src/test/scala/tests/UserConfigurationSuite.scala @@ -3,11 +3,11 @@ package tests import java.util.Properties import scala.meta.internal.metals.ClientConfiguration +import scala.meta.internal.metals.JavaFormatConfig import scala.meta.internal.metals.UserConfiguration +import scala.meta.io.AbsolutePath import munit.Location -import scala.meta.internal.metals.JavaFormatConfig -import scala.meta.io.AbsolutePath class UserConfigurationSuite extends BaseSuite { def check( From 69d9e50365d65858edcdaaf4ef2e69f415c9e2d7 Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Tue, 21 Dec 2021 14:48:41 +0000 Subject: [PATCH 6/8] Add exports to CI for jdk 17 + semantic plugin --- .github/workflows/ci.yml | 1 + bin/test.sh | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 904cc9fc949..c7621b54fe0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: run: | bin/test.sh unit/test env: + JAVA_VERSION: ${{ matrix.java }} TEST_SHARD: ${{ matrix.shard }} GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} GOOGLE_APPLICATION_CREDENTIALS_JSON: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }} diff --git a/bin/test.sh b/bin/test.sh index 950424ea787..351d1e52a6e 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -6,9 +6,16 @@ function bloop_version { mkdir -p ~/.bloop touch ~/.bloop/.jvmopts -echo "-Xss16m" >> ~/.bloop/.jvmopts -echo "-Xmx1G" >> ~/.bloop/.jvmopts - +echo "-Xss16m" >> ~/.bloop/.jvmopts +echo "-Xmx1G" >> ~/.bloop/.jvmopts +if [ $JAVA_VERSION -eq 17 ] +then + echo "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED" >> ~/.bloop/.jvmopts + echo "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED" >> ~/.bloop/.jvmopts + echo "--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED" >> ~/.bloop/.jvmopts + echo "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED" >> ~/.bloop/.jvmopts + echo "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" >> ~/.bloop/.jvmopts +fi curl -Lo coursier https://git.io/coursier-cli && chmod +x coursier ./coursier launch ch.epfl.scala:bloopgun-core_2.12:$(bloop_version) -- about From a4a681e5790ff358a81ff5dc949c3d3cfcfa254f Mon Sep 17 00:00:00 2001 From: Arthur McGibbon Date: Wed, 22 Dec 2021 15:36:23 +0000 Subject: [PATCH 7/8] hardcode eclipse transitive dependency versions --- build.sbt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/build.sbt b/build.sbt index 42e998707fa..3ff6e0c8de8 100644 --- a/build.sbt +++ b/build.sbt @@ -112,6 +112,23 @@ inThisBuild( resolvers += Resolver.sonatypeRepo("public"), resolvers += Resolver.sonatypeRepo("snapshot"), dependencyOverrides += V.guava, + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.ant.core" % "3.5.500", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.compare.core" % "3.6.600", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.core.commands" % "3.9.500", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.core.contenttype" % "3.7.500", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.core.expressions" % "3.6.500", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.core.filesystem" % "1.7.500", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.core.jobs" % "3.10.500", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.core.resources" % "3.13.500", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.core.runtime" % "3.16.0", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.core.variables" % "3.4.600", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.equinox.app" % "1.4.300", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.equinox.common" % "3.10.600", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.equinox.preferences" % "3.7.600", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.equinox.registry" % "3.8.600", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.osgi" % "3.15.0", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.team.core" % "3.8.700", + dependencyOverrides += "org.eclipse.platform" % "org.eclipse.text" % "3.9.0", // faster publishLocal: packageDoc / publishArtifact := sys.env.contains("CI"), packageSrc / publishArtifact := sys.env.contains("CI"), From 675816aee04bcae56cf7c7cf7790b8e892fd2ec7 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Wed, 22 Dec 2021 20:20:56 +0100 Subject: [PATCH 8/8] Skipp asking about javacOptions when using sbt via BSP --- .../internal/metals/BuildServerConnection.scala | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala index d067ccbe9dd..be091c89bc8 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala @@ -165,10 +165,16 @@ class BuildServerConnection private ( val resultOnJavacOptionsUnsupported = new JavacOptionsResult( List.empty[JavacOptionsItem].asJava ) - val onFail = Some( - (resultOnJavacOptionsUnsupported, "Java targets not supported by server") - ) - register(server => server.buildTargetJavacOptions(params), onFail).asScala + if (isSbt) Future.successful(resultOnJavacOptionsUnsupported) + else { + val onFail = Some( + ( + resultOnJavacOptionsUnsupported, + "Java targets not supported by server" + ) + ) + register(server => server.buildTargetJavacOptions(params), onFail).asScala + } } def buildTargetScalacOptions(