diff --git a/metals/src/main/scala/scala/meta/internal/bsp/BspConfigGenerator.scala b/metals/src/main/scala/scala/meta/internal/bsp/BspConfigGenerator.scala index b0878127876..ebb800147b4 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspConfigGenerator.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspConfigGenerator.scala @@ -1,7 +1,11 @@ package scala.meta.internal.bsp +import java.nio.file.Files +import java.nio.file.StandardCopyOption + import scala.concurrent.ExecutionContext import scala.concurrent.Future +import scala.util.control.NonFatal import scala.meta.internal.bsp.BspConfigGenerationStatus._ import scala.meta.internal.builds.BuildServerProvider @@ -31,10 +35,33 @@ final class BspConfigGenerator( .run( s"${buildTool.getBuildServerName} bspConfig", args, - workspace, + buildTool.projectRoot, buildTool.redirectErrorOutput, ) .map(BspConfigGenerationStatus.fromExitCode) + .map { + case Generated if buildTool.projectRoot != workspace => + try { + val bsp = ".bsp" + workspace.resolve(bsp).createDirectories() + val buildToolBspDir = buildTool.projectRoot.resolve(bsp).toNIO + val workspaceBspDir = workspace.resolve(bsp).toNIO + buildToolBspDir.toFile.listFiles().foreach { file => + val path = file.toPath() + if (!file.isDirectory() && path.filename.endsWith(".json")) { + val to = + workspaceBspDir.resolve(path.relativize(buildToolBspDir)) + Files.move(path, to, StandardCopyOption.REPLACE_EXISTING) + } + } + Files.delete(buildToolBspDir) + Generated + } catch { + case NonFatal(_) => + Failed(Right("Could not move bsp config from project root")) + } + case status => status + } /** * Given multiple build tools that are all BuildServerProviders, allow the diff --git a/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala b/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala index 5dbb79d3715..489d64d7020 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala @@ -72,12 +72,14 @@ class BspConnector( * of the bsp entry has already happened at this point. */ def connect( + projectRoot: AbsolutePath, workspace: AbsolutePath, userConfiguration: UserConfiguration, shellRunner: ShellRunner, )(implicit ec: ExecutionContext): Future[Option[BspSession]] = { def connect( - workspace: AbsolutePath, + projectRoot: AbsolutePath, + bspTraceRoot: AbsolutePath, addLivenessMonitor: Boolean, ): Future[Option[BuildServerConnection]] = { scribe.info("Attempting to connect to the build server...") @@ -87,18 +89,24 @@ class BspConnector( Future.successful(None) case ResolvedBloop => bloopServers - .newServer(workspace, userConfiguration, addLivenessMonitor) + .newServer( + projectRoot, + bspTraceRoot, + userConfiguration, + addLivenessMonitor, + ) .map(Some(_)) case ResolvedBspOne(details) if details.getName() == SbtBuildTool.name => tables.buildServers.chooseServer(SbtBuildTool.name) - val shouldReload = SbtBuildTool.writeSbtMetalsPlugins(workspace) + val shouldReload = SbtBuildTool.writeSbtMetalsPlugins(projectRoot) val connectionF = for { - _ <- SbtBuildTool(workspace, () => userConfiguration) - .ensureCorrectJavaVersion(shellRunner, workspace, client) + _ <- SbtBuildTool(projectRoot, () => userConfiguration) + .ensureCorrectJavaVersion(shellRunner, projectRoot, client) connection <- bspServers.newServer( - workspace, + projectRoot, + bspTraceRoot, details, addLivenessMonitor, ) @@ -112,7 +120,7 @@ class BspConnector( case ResolvedBspOne(details) => tables.buildServers.chooseServer(details.getName()) bspServers - .newServer(workspace, details, addLivenessMonitor) + .newServer(projectRoot, bspTraceRoot, details, addLivenessMonitor) .map(Some(_)) case ResolvedMultiple(_, availableServers) => val distinctServers = availableServers @@ -146,7 +154,8 @@ class BspConnector( ) _ = tables.buildServers.chooseServer(item.getName()) conn <- bspServers.newServer( - workspace, + projectRoot, + bspTraceRoot, item, addLivenessMonitor, ) @@ -154,7 +163,7 @@ class BspConnector( } } - connect(workspace, addLivenessMonitor = true).flatMap { + connect(projectRoot, workspace, addLivenessMonitor = true).flatMap { possibleBuildServerConn => possibleBuildServerConn match { case None => Future.successful(None) @@ -163,8 +172,8 @@ class BspConnector( // NOTE: (ckipp01) we special case this here since sbt bsp server // doesn't yet support metabuilds. So in the future when that // changes, re-work this and move the creation of this out above - val metaConns = sbtMetaWorkspaces(workspace).map( - connect(_, addLivenessMonitor = false) + val metaConns = sbtMetaWorkspaces(workspace).map(root => + connect(root, root, addLivenessMonitor = false) ) Future .sequence(metaConns) @@ -220,8 +229,8 @@ class BspConnector( * Runs "Switch build server" command, returns true if build server choice * was changed. * - * NOTE: that in most cases this doesn't actaully change your build server - * and connect to it, but stores that you want to chage it unless you are + * NOTE: that in most cases this doesn't actually change your build server + * and connect to it, but stores that you want to change it unless you are * choosing Bloop, since in that case it's special cased and does start it. */ def switchBuildServer( @@ -255,7 +264,9 @@ class BspConnector( BuildServerProvider, BspConnectionDetails, ]] = { - if (bloopPresent || buildTools.loadSupported().nonEmpty) + if ( + bloopPresent || buildTools.loadSupported().exists(_.isBloopDefaultBsp) + ) new BspConnectionDetails( BloopServers.name, ImmutableList.of(), diff --git a/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala b/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala index a4d1911f92d..1f00eaf0bae 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala @@ -67,6 +67,7 @@ final class BspServers( def newServer( projectDirectory: AbsolutePath, + bspTraceRoot: AbsolutePath, details: BspConnectionDetails, addLivenessMonitor: Boolean, ): Future[BuildServerConnection] = { @@ -135,6 +136,7 @@ final class BspServers( BuildServerConnection.fromSockets( projectDirectory, + bspTraceRoot, buildClient, client, newConnection, diff --git a/metals/src/main/scala/scala/meta/internal/bsp/ScalaCliBspScope.scala b/metals/src/main/scala/scala/meta/internal/bsp/ScalaCliBspScope.scala index ad7898c53ac..faa93173c52 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/ScalaCliBspScope.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/ScalaCliBspScope.scala @@ -14,7 +14,7 @@ object ScalaCliBspScope { ) } - private def scalaCliBspRoot(root: AbsolutePath): List[AbsolutePath] = + def scalaCliBspRoot(root: AbsolutePath): List[AbsolutePath] = for { path <- ScalaCliBuildTool.pathsToScalaCliBsp(root) text <- path.readTextOpt.toList @@ -25,7 +25,7 @@ object ScalaCliBspScope { case "bsp" :: tail => dropOptions(tail).takeWhile(!_.startsWith("-")) case _ => Nil } - rootPath <- Try(AbsolutePath(rootArg).dealias).toOption + rootPath <- Try(AbsolutePath(rootArg)(root).dealias).toOption if rootPath.exists } yield rootPath diff --git a/metals/src/main/scala/scala/meta/internal/builds/BloopInstall.scala b/metals/src/main/scala/scala/meta/internal/builds/BloopInstall.scala index 0f1518b2ee7..1e9b3b7ab90 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BloopInstall.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BloopInstall.scala @@ -72,7 +72,7 @@ final class BloopInstall( .run( s"${buildTool.executableName} bloopInstall", args, - workspace, + buildTool.projectRoot, buildTool.redirectErrorOutput, Map( "COURSIER_PROGRESS" -> "disable", @@ -165,7 +165,7 @@ final class BloopInstall( )(implicit ec: ExecutionContext): Future[Confirmation] = { tables.digests.setStatus(digest, Status.Requested) val (params, yes) = - if (buildTools.isBloop) { + if (buildTools.isBloop(buildTool.projectRoot)) { ImportBuildChanges.params(buildTool.toString) -> ImportBuildChanges.yes } else { diff --git a/metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala b/metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala index 66ffaef95d8..7420900501f 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala @@ -42,6 +42,8 @@ trait BuildTool { def isBloopDefaultBsp = true + def projectRoot: AbsolutePath + } object BuildTool { diff --git a/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala b/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala index a9f26d5dd8c..d4b887940c8 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala @@ -4,6 +4,7 @@ import java.nio.file.Files import java.util.Properties import java.util.concurrent.atomic.AtomicReference +import scala.meta.internal.bsp.ScalaCliBspScope import scala.meta.internal.io.PathIO import scala.meta.internal.metals.BloopServers import scala.meta.internal.metals.MetalsEnrichments._ @@ -30,19 +31,25 @@ final class BuildTools( ) { private val lastDetectedBuildTools = new AtomicReference(Set.empty[String]) // NOTE: We do a couple extra check here before we say a workspace with a - // `.bsp` is auto-connectable, and we ensure that a user has explicity chosen + // `.bsp` is auto-connectable, and we ensure that a user has explicitly chosen // to use another build server besides Bloop or it's a BSP server for a build // tool we don't support. If this isn't done, it causes unexpected warnings // since if a `.bsp/.json` exists before a `.bloop` one does in a // workspace with a build tool we support, we will attempt to autoconnect to // Bloop since Metals thinks it's in state that's auto-connectable before the // user is even prompted. - def isAutoConnectable: Boolean = { - isBloop || (isBsp && all.isEmpty) || (isBsp && explicitChoiceMade()) || (isBsp && isBazel) - } - def isBloop: Boolean = { - hasJsonFile(workspace.resolve(".bloop")) + def isAutoConnectable( + maybeProjectRoot: Option[AbsolutePath] = None + ): Boolean = { + maybeProjectRoot + .map(isBloop) + .getOrElse( + isBloop + ) || (isBsp && all.isEmpty) || (isBsp && explicitChoiceMade()) || (isBsp && isBazel) } + def isBloop(root: AbsolutePath): Boolean = hasJsonFile(root.resolve(".bloop")) + def bloopProject: Option[AbsolutePath] = searchForBuildTool(isBloop) + def isBloop: Boolean = bloopProject.isDefined def isBsp: Boolean = { hasJsonFile(workspace.resolve(".bsp")) || bspGlobalDirectories.exists(hasJsonFile) @@ -50,11 +57,12 @@ final class BuildTools( private def hasJsonFile(dir: AbsolutePath): Boolean = { dir.list.exists(_.extension == "json") } + // Returns true if there's a build.sbt file or project/build.properties with sbt.version - def isSbt: Boolean = { - workspace.resolve("build.sbt").isFile || { + def sbtProject: Option[AbsolutePath] = searchForBuildTool { root => + root.resolve("build.sbt").isFile || { val buildProperties = - workspace.resolve("project").resolve("build.properties") + root.resolve("project").resolve("build.properties") buildProperties.isFile && { val props = new Properties() val in = Files.newInputStream(buildProperties.toNIO) @@ -64,31 +72,72 @@ final class BuildTools( } } } - def isMill: Boolean = workspace.resolve("build.sc").isFile - def isScalaCli: Boolean = - ScalaCliBuildTool - .pathsToScalaCliBsp(workspace) - .exists(_.isFile) || workspace.resolve("project.scala").isFile - def isGradle: Boolean = { + def isSbt: Boolean = sbtProject.isDefined + def millProject: Option[AbsolutePath] = searchForBuildTool( + _.resolve("build.sc").isFile + ) + def isMill: Boolean = millProject.isDefined + def scalaCliProject: Option[AbsolutePath] = + searchForBuildTool(_.resolve("project.scala").isFile) + .orElse { + ScalaCliBspScope.scalaCliBspRoot(workspace) match { + case Nil => None + case path :: Nil if path.isFile => Some(path.parent) + case path :: Nil => + scribe.info(s"path: $path") + Some(path) + case _ => Some(workspace) + } + } + + def gradleProject: Option[AbsolutePath] = { val defaultGradlePaths = List( "settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts", ) - defaultGradlePaths.exists(workspace.resolve(_).isFile) + searchForBuildTool(root => + defaultGradlePaths.exists(root.resolve(_).isFile) + ) } - def isMaven: Boolean = workspace.resolve("pom.xml").isFile - def isPants: Boolean = workspace.resolve("pants.ini").isFile - def isBazel: Boolean = workspace.resolve("WORKSPACE").isFile + def isGradle: Boolean = gradleProject.isDefined + def mavenProject: Option[AbsolutePath] = searchForBuildTool( + _.resolve("pom.xml").isFile + ) + def isMaven: Boolean = mavenProject.isDefined + def pantsProject: Option[AbsolutePath] = searchForBuildTool( + _.resolve("pants.ini").isFile + ) + def isPants: Boolean = pantsProject.isDefined + def bazelProject: Option[AbsolutePath] = searchForBuildTool( + _.resolve("WORKSPACE").isFile + ) + def isBazel: Boolean = bazelProject.isDefined + + private def searchForBuildTool( + isProjectRoot: AbsolutePath => Boolean + ): Option[AbsolutePath] = + if (isProjectRoot(workspace)) Some(workspace) + else + workspace.toNIO + .toFile() + .listFiles() + .collectFirst { + case file + if file.isDirectory && + !file.getName.startsWith(".") && + isProjectRoot(AbsolutePath(file.toPath())) => + AbsolutePath(file.toPath()) + } def allAvailable: List[BuildTool] = { List( - SbtBuildTool(workspaceVersion = None, userConfig), - GradleBuildTool(userConfig), - MavenBuildTool(userConfig), - MillBuildTool(userConfig), - ScalaCliBuildTool(workspace, userConfig), + SbtBuildTool(workspaceVersion = None, workspace, userConfig), + GradleBuildTool(userConfig, workspace), + MavenBuildTool(userConfig, workspace), + MillBuildTool(userConfig, workspace), + ScalaCliBuildTool(workspace, workspace, userConfig), ) } @@ -111,12 +160,11 @@ final class BuildTools( def loadSupported(): List[BuildTool] = { val buf = List.newBuilder[BuildTool] - if (isSbt) buf += SbtBuildTool(workspace, userConfig) - if (isGradle) buf += GradleBuildTool(userConfig) - if (isMaven) buf += MavenBuildTool(userConfig) - if (isMill) buf += MillBuildTool(userConfig) - if (isScalaCli) - buf += ScalaCliBuildTool(workspace, userConfig) + sbtProject.foreach(buf += SbtBuildTool(_, userConfig)) + gradleProject.foreach(buf += GradleBuildTool(userConfig, _)) + mavenProject.foreach(buf += MavenBuildTool(userConfig, _)) + millProject.foreach(buf += MillBuildTool(userConfig, _)) + scalaCliProject.foreach(buf += ScalaCliBuildTool(workspace, _, userConfig)) buf.result() } @@ -130,11 +178,11 @@ final class BuildTools( def isBuildRelated( path: AbsolutePath ): Option[String] = { - if (isSbt && SbtBuildTool.isSbtRelatedPath(workspace, path)) + if (sbtProject.exists(SbtBuildTool.isSbtRelatedPath(_, path))) Some(SbtBuildTool.name) - else if (isGradle && GradleBuildTool.isGradleRelatedPath(workspace, path)) + else if (gradleProject.exists(GradleBuildTool.isGradleRelatedPath(_, path))) Some(GradleBuildTool.name) - else if (isMaven && MavenBuildTool.isMavenRelatedPath(workspace, path)) + else if (mavenProject.exists(MavenBuildTool.isMavenRelatedPath(_, path))) Some(MavenBuildTool.name) else if (isMill && MillBuildTool.isMillRelatedPath(path)) Some(MillBuildTool.name) diff --git a/metals/src/main/scala/scala/meta/internal/builds/GradleBuildTool.scala b/metals/src/main/scala/scala/meta/internal/builds/GradleBuildTool.scala index a6af972b78e..3b7f38deefb 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/GradleBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/GradleBuildTool.scala @@ -16,8 +16,10 @@ import coursierapi.IvyRepository import coursierapi.MavenRepository import coursierapi.Repository -case class GradleBuildTool(userConfig: () => UserConfiguration) - extends BuildTool +case class GradleBuildTool( + userConfig: () => UserConfiguration, + projectRoot: AbsolutePath, +) extends BuildTool with BloopInstallProvider { private val initScriptName = "init-script.gradle" @@ -55,8 +57,8 @@ case class GradleBuildTool(userConfig: () => UserConfiguration) AbsolutePath(out) } - private def isBloopConfigured(workspace: AbsolutePath): Boolean = { - val gradlePropsFile = workspace.resolve("gradle.properties") + private def isBloopConfigured(): Boolean = { + val gradlePropsFile = projectRoot.resolve("gradle.properties") try { val contents = new String(gradlePropsFile.readAllBytes, StandardCharsets.UTF_8) @@ -71,11 +73,11 @@ case class GradleBuildTool(userConfig: () => UserConfiguration) } override def digest(workspace: AbsolutePath): Option[String] = - GradleDigest.current(workspace) + GradleDigest.current(projectRoot) override def bloopInstallArgs(workspace: AbsolutePath): List[String] = { val cmd = { - if (isBloopConfigured(workspace)) + if (isBloopConfigured()) List("--stacktrace", "--console=plain", "bloopInstall") else { List( diff --git a/metals/src/main/scala/scala/meta/internal/builds/MavenBuildTool.scala b/metals/src/main/scala/scala/meta/internal/builds/MavenBuildTool.scala index 2cabbb486cb..a2ab5d48ffe 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/MavenBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/MavenBuildTool.scala @@ -6,8 +6,10 @@ import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.UserConfiguration import scala.meta.io.AbsolutePath -case class MavenBuildTool(userConfig: () => UserConfiguration) - extends BuildTool +case class MavenBuildTool( + userConfig: () => UserConfiguration, + projectRoot: AbsolutePath, +) extends BuildTool with BloopInstallProvider { private lazy val embeddedMavenLauncher: AbsolutePath = { @@ -37,7 +39,7 @@ case class MavenBuildTool(userConfig: () => UserConfiguration) val javaArgs = List[String]( JavaBinary(userConfig().javaHome), "-Dfile.encoding=UTF-8", - s"-Dmaven.multiModuleProjectDirectory=$workspace", + s"-Dmaven.multiModuleProjectDirectory=$projectRoot", s"-Dmaven.home=$tempDir", ) @@ -55,7 +57,7 @@ case class MavenBuildTool(userConfig: () => UserConfiguration) } def digest(workspace: AbsolutePath): Option[String] = { - MavenDigest.current(workspace) + MavenDigest.current(projectRoot) } override def minimumVersion: String = "3.5.2" diff --git a/metals/src/main/scala/scala/meta/internal/builds/MillBuildTool.scala b/metals/src/main/scala/scala/meta/internal/builds/MillBuildTool.scala index b01cb5482ca..0510de98a62 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/MillBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/MillBuildTool.scala @@ -9,8 +9,10 @@ import scala.meta.internal.metals.UserConfiguration import scala.meta.internal.semver.SemVer import scala.meta.io.AbsolutePath -case class MillBuildTool(userConfig: () => UserConfiguration) - extends BuildTool +case class MillBuildTool( + userConfig: () => UserConfiguration, + projectRoot: AbsolutePath, +) extends BuildTool with BloopInstallProvider with BuildServerProvider { @@ -102,7 +104,7 @@ case class MillBuildTool(userConfig: () => UserConfiguration) } override def bloopInstallArgs(workspace: AbsolutePath): List[String] = { - val millVersion = getMillVersion(workspace) + val millVersion = getMillVersion(projectRoot) val cmd = bloopImportArgs(millVersion) ::: bloopCmd(millVersion) :: Nil @@ -110,7 +112,7 @@ case class MillBuildTool(userConfig: () => UserConfiguration) } override def digest(workspace: AbsolutePath): Option[String] = - MillDigest.current(workspace) + MillDigest.current(projectRoot) override def minimumVersion: String = "0.6.0" @@ -134,7 +136,7 @@ case class MillBuildTool(userConfig: () => UserConfiguration) ): Option[List[String]] = Option.when(workspaceSupportsBsp(workspace: AbsolutePath)) { val cmd = "mill.bsp.BSP/install" :: Nil - putTogetherArgs(cmd, getMillVersion(workspace), workspace) + putTogetherArgs(cmd, getMillVersion(projectRoot), workspace) } def workspaceSupportsBsp(workspace: AbsolutePath): Boolean = { diff --git a/metals/src/main/scala/scala/meta/internal/builds/SbtBuildTool.scala b/metals/src/main/scala/scala/meta/internal/builds/SbtBuildTool.scala index 5244a66467a..cfd280dadd3 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/SbtBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/SbtBuildTool.scala @@ -19,6 +19,7 @@ import org.eclipse.lsp4j.services.LanguageClient case class SbtBuildTool( workspaceVersion: Option[String], + projectRoot: AbsolutePath, userConfig: () => UserConfiguration, ) extends BuildTool with BloopInstallProvider @@ -46,37 +47,36 @@ case class SbtBuildTool( "-Dbloop.export-jar-classifiers=sources", "bloopInstall", ) - val allArgs = composeArgs(bloopInstallArgs, workspace, tempDir) + val allArgs = composeArgs(bloopInstallArgs, projectRoot, tempDir) removeLegacyGlobalPlugin() - writeBloopPlugin(workspace) + writeBloopPlugin(projectRoot) allArgs } override def digest(workspace: AbsolutePath): Option[String] = - SbtDigest.current(workspace) + SbtDigest.current(projectRoot) override val minimumVersion: String = "0.13.17" override val recommendedVersion: String = BuildInfo.sbtVersion override def createBspFileArgs( workspace: AbsolutePath ): Option[List[String]] = - Option.when(workspaceSupportsBsp(workspace)) { + Option.when(workspaceSupportsBsp(projectRoot)) { val bspConfigArgs = List[String]("bspConfig") val bspDir = workspace.resolve(".bsp").toNIO - composeArgs(bspConfigArgs, workspace, bspDir) + composeArgs(bspConfigArgs, projectRoot, bspDir) } def shutdownBspServer( - shellRunner: ShellRunner, - workspace: AbsolutePath, + shellRunner: ShellRunner ): Future[Int] = { val shutdownArgs = - composeArgs(List("--client", "shutdown"), workspace, workspace.toNIO) + composeArgs(List("--client", "shutdown"), projectRoot, projectRoot.toNIO) scribe.info(s"running ${shutdownArgs.mkString(" ")}") shellRunner.run( "Shutting down sbt server", shutdownArgs, - workspace, + projectRoot, true, ) } @@ -149,7 +149,7 @@ case class SbtBuildTool( } private def writeBloopPlugin( - workspace: AbsolutePath + projectRoot: AbsolutePath ): Unit = { def sbtMetaDirs( @@ -178,8 +178,8 @@ case class SbtBuildTool( else userConfig().currentBloopVersion val plugin = bloopPluginDetails(pluginVersion) - val mainMeta = workspace.resolve("project") - val metaMeta = workspace.resolve("project").resolve("project") + val mainMeta = projectRoot.resolve("project") + val metaMeta = projectRoot.resolve("project").resolve("project") sbtMetaDirs(mainMeta, Set(mainMeta, metaMeta)).foreach(dir => writePlugins(dir, plugin) ) @@ -203,7 +203,7 @@ case class SbtBuildTool( .asScala .flatMap { case Messages.SbtServerJavaHomeUpdate.restart => - shutdownBspServer(shellRunner, workspace).ignoreValue + shutdownBspServer(shellRunner).ignoreValue case _ => Future.successful(()) } } @@ -250,9 +250,9 @@ object SbtBuildTool { * * Return true if any plugin file changed, meaning we should reload */ - def writeSbtMetalsPlugins(workspace: AbsolutePath): Boolean = { - val mainMeta = workspace.resolve("project") - val metaMeta = workspace.resolve("project").resolve("project") + def writeSbtMetalsPlugins(projectRoot: AbsolutePath): Boolean = { + val mainMeta = projectRoot.resolve("project") + val metaMeta = projectRoot.resolve("project").resolve("project") val writtenPlugin = writePlugins(mainMeta, metalsPluginDetails, debugAdapterPluginDetails) val writtenMeta = @@ -354,11 +354,11 @@ object SbtBuildTool { } def apply( - workspace: AbsolutePath, + projectRoot: AbsolutePath, userConfig: () => UserConfiguration, ): SbtBuildTool = { - val version = loadVersion(workspace).map(_.toString()) - SbtBuildTool(version, userConfig) + val version = loadVersion(projectRoot).map(_.toString()) + SbtBuildTool(version, projectRoot, userConfig) } def loadVersion(workspace: AbsolutePath): Option[String] = { diff --git a/metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala b/metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala index 31860b39924..2cc1aad7cec 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala @@ -14,6 +14,7 @@ import scala.meta.io.AbsolutePath class ScalaCliBuildTool( val workspaceVersion: Option[String], + val projectRoot: AbsolutePath, userConfig: () => UserConfiguration, ) extends BuildTool with BuildServerProvider { @@ -30,8 +31,9 @@ class ScalaCliBuildTool( // fallback to creating `.bsp/scala-cli.json` that starts JVM launcher val bspConfig = workspace.resolve(".bsp").resolve("scala-cli.json") statusBar.addMessage("scala-cli bspConfig") - bspConfig - .writeText(ScalaCli.scalaCliBspJsonContent(root = workspace.toString())) + bspConfig.writeText( + ScalaCli.scalaCliBspJsonContent(projectRoot = projectRoot.toString()) + ) Future.successful(Generated) } @@ -51,7 +53,7 @@ class ScalaCliBuildTool( runScalaCliCommand.map( _.toList ++ List( "setup-ide", - workspace.toString(), + projectRoot.toString(), ) ) @@ -84,18 +86,20 @@ object ScalaCliBuildTool { ) def apply( - root: AbsolutePath, + workspace: AbsolutePath, + projectRoot: AbsolutePath, userConfig: () => UserConfiguration, ): ScalaCliBuildTool = { val workspaceFolderVersions = for { - path <- pathsToScalaCliBsp(root) + path <- pathsToScalaCliBsp(workspace) text <- path.readTextOpt json = ujson.read(text) version <- json("version").strOpt } yield version new ScalaCliBuildTool( workspaceFolderVersions.headOption, + projectRoot, userConfig, ) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala b/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala index 4b4d74c49c1..9be67e903c5 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala @@ -78,14 +78,16 @@ final class BloopServers( } def newServer( - workspace: AbsolutePath, + projectRoot: AbsolutePath, + bspTraceRoot: AbsolutePath, userConfiguration: UserConfiguration, addLivenessMonitor: Boolean, ): Future[BuildServerConnection] = { val bloopVersion = userConfiguration.currentBloopVersion BuildServerConnection .fromSockets( - workspace, + projectRoot, + bspTraceRoot, client, languageClient, () => 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 f6d80eeb4f8..bb11df07f28 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala @@ -431,9 +431,12 @@ object BuildServerConnection { * This method is blocking, doesn't return Future[], because if the `initialize` handshake * doesn't complete within a few seconds then something is wrong. We want to fail fast * when initialization is not successful. + * + * @param bspTraceRoot we look for `bspTraceRoot/.metals/.bsp.trace.json` to write down bsp trace */ def fromSockets( - workspace: AbsolutePath, + projectRoot: AbsolutePath, + bspTraceRoot: AbsolutePath, localClient: MetalsBuildClient, languageClient: LanguageClient, connect: () => Future[SocketConnection], @@ -449,7 +452,7 @@ object BuildServerConnection { def setupServer(): Future[LauncherConnection] = { connect().map { case conn @ SocketConnection(_, output, input, _, _) => - val tracePrinter = Trace.setupTracePrinter("BSP", workspace) + val tracePrinter = Trace.setupTracePrinter("BSP", bspTraceRoot) val requestMonitor = if (addLivenessMonitor) Some(new RequestMonitorImpl) else None val wrapper: MessageConsumer => MessageConsumer = @@ -470,7 +473,7 @@ object BuildServerConnection { Cancelable(() => listening.cancel(false)) val result = try { - BuildServerConnection.initialize(workspace, server, serverName) + BuildServerConnection.initialize(projectRoot, server, serverName) } catch { case e: TimeoutException => conn.cancelables.foreach(_.cancel()) @@ -511,7 +514,7 @@ object BuildServerConnection { languageClient, reconnectNotification, config, - workspace, + projectRoot, supportsWrappedSources.getOrElse(connection.supportsWrappedSources), ) } @@ -519,7 +522,8 @@ object BuildServerConnection { if (retry > 0) { scribe.warn(s"Retrying connection to the build server $serverName") fromSockets( - workspace, + projectRoot, + bspTraceRoot, localClient, languageClient, connect, diff --git a/metals/src/main/scala/scala/meta/internal/metals/ChosenBuildTool.scala b/metals/src/main/scala/scala/meta/internal/metals/ChosenBuildTool.scala index 3ea9f9ec350..7a373b95f70 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ChosenBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ChosenBuildTool.scala @@ -12,7 +12,8 @@ class ChosenBuildTool(conn: () => Connection) { )(_ => ()) { _.getString("build_tool") } .headOption } - def chooseBuildTool(buildTool: String): Int = { + def chooseBuildTool(buildTool: String): Int = synchronized { + reset() conn().update { "insert into chosen_build_tool values (?);" } { stmt => stmt.setString(1, buildTool) } 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 13bea2de1e2..20a9d9d970b 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FormattingProvider.scala @@ -27,6 +27,7 @@ import scala.meta.internal.metals.Messages.UpdateScalafmtConf import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.clients.language.MetalsLanguageClient import scala.meta.internal.semver.SemVer +import scala.meta.io.AbsolutePath import ch.epfl.scala.bsp4j.BuildTargetIdentifier import org.eclipse.lsp4j.jsonrpc.CancelChecker @@ -81,10 +82,10 @@ final class FormattingProvider( // Warms up the Scalafmt instance so that the first formatting request responds faster. // Does nothing if there is no .scalafmt.conf or there is no configured version setting. def load(): Unit = { - if (scalafmtConf.isFile && !Testing.isEnabled) { + if (scalafmtConf(workspace).isFile && !Testing.isEnabled) { try { scalafmt.format( - scalafmtConf.toNIO, + scalafmtConf(workspace).toNIO, Paths.get("Main.scala"), "object Main {}", ) @@ -100,20 +101,21 @@ final class FormattingProvider( def format( path: AbsolutePath, + projectRoot: AbsolutePath, token: CancelChecker, ): Future[util.List[l.TextEdit]] = { scalafmt = scalafmt.withReporter(activeReporter) reset(token) val input = path.toInputFromBuffers(buffers) - if (!scalafmtConf.isFile) { - handleMissingFile(scalafmtConf).map { + if (!scalafmtConf(projectRoot).isFile) { + handleMissingFile(scalafmtConf(projectRoot)).map { case true => - runFormat(path, input).asJava + runFormat(path, projectRoot, input).asJava case false => Collections.emptyList[l.TextEdit]() } } else { - val result = runFormat(path, input) + val result = runFormat(path, projectRoot, input) if (token.isCancelled) { statusBar.addMessage( s"${icons.info}Scalafmt cancelled by editor, try saving file again" @@ -124,7 +126,8 @@ final class FormattingProvider( // Wait until "update .scalafmt.conf" dialogue has completed // before returning future. promise.future.map { - case true if !token.isCancelled => runFormat(path, input).asJava + case true if !token.isCancelled => + runFormat(path, projectRoot, input).asJava case _ => result.asJava } case None => @@ -133,11 +136,15 @@ final class FormattingProvider( } } - private def runFormat(path: AbsolutePath, input: Input): List[l.TextEdit] = { + private def runFormat( + path: AbsolutePath, + projectRoot: AbsolutePath, + input: Input, + ): List[l.TextEdit] = { val fullDocumentRange = Position.Range(input, 0, input.chars.length).toLsp val formatted = try { - scalafmt.format(scalafmtConf.toNIO, path.toNIO, input.text) + scalafmt.format(scalafmtConf(projectRoot).toNIO, path.toNIO, input.text) } catch { case e: ScalafmtDynamicError => scribe.debug( @@ -351,14 +358,16 @@ final class FormattingProvider( } private def checkIfDialectUpgradeRequired( - config: ScalafmtConfig + config: ScalafmtConfig, + projectRoot: AbsolutePath, ): Future[Unit] = { if (tables.dismissedNotifications.UpdateScalafmtConf.isDismissed) Future.unit else { Future(inspectDialectRewrite(config)).flatMap { case Some(rewrite) => - val canUpdate = rewrite.canUpdate && scalafmtConf.isInside(workspace) + val canUpdate = + rewrite.canUpdate && scalafmtConf(projectRoot).isInside(projectRoot) val params = UpdateScalafmtConf.params(rewrite.maxDialect, canUpdate) @@ -369,10 +378,11 @@ final class FormattingProvider( client.showMessageRequest(params).asScala.map { item => if (item == UpdateScalafmtConf.letUpdate) { - val text = scalafmtConf.toInputFromBuffers(buffers).text + val text = + scalafmtConf(projectRoot).toInputFromBuffers(buffers).text val updatedText = rewrite.rewrite(text) Files.write( - scalafmtConf.toNIO, + scalafmtConf(projectRoot).toNIO, updatedText.getBytes(StandardCharsets.UTF_8), ) } else if (item == Messages.notNow) { @@ -387,36 +397,36 @@ final class FormattingProvider( } } - def validateWorkspace(): Future[Unit] = { - if (scalafmtConf.exists) { - val text = scalafmtConf.toInputFromBuffers(buffers).text + def validateWorkspace(projectRoot: AbsolutePath): Future[Unit] = { + if (scalafmtConf(projectRoot).exists) { + val text = scalafmtConf(projectRoot).toInputFromBuffers(buffers).text ScalafmtConfig.parse(text) match { case Failure(e) => - scribe.error(s"Failed to parse ${scalafmtConf}", e) + scribe.error(s"Failed to parse ${scalafmtConf(projectRoot)}", e) Future.unit case Success(values) => - checkIfDialectUpgradeRequired(values) + checkIfDialectUpgradeRequired(values, projectRoot) } } else { Future.unit } } - private def scalafmtConf: AbsolutePath = { + private def scalafmtConf(projectRoot: AbsolutePath): AbsolutePath = { val configpath = userConfig().scalafmtConfigPath - configpath.getOrElse(workspace.resolve(".scalafmt.conf")) + configpath.getOrElse(projectRoot.resolve(".scalafmt.conf")) } private val activeReporter: ScalafmtReporter = new ScalafmtReporter { private var downloadingScalafmt = Promise[Unit]() override def error(file: Path, message: String): Unit = { scribe.error(s"scalafmt: $file: $message") - if (file == scalafmtConf.toNIO) { + if (file == scalafmtConf(workspace).toNIO) { downloadingScalafmt.trySuccess(()) if (message.contains("failed to resolve Scalafmt version")) { client.showMessage(MissingScalafmtVersion.failedToResolve(message)) } - val input = scalafmtConf.toInputFromBuffers(buffers) + val input = scalafmtConf(workspace).toInputFromBuffers(buffers) val pos = Position.Range(input, 0, input.chars.length) client.publishDiagnostics( new l.PublishDiagnosticsParams( diff --git a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala index cacdd666ec9..e6396948819 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala @@ -78,6 +78,7 @@ final case class Indexer( symbolDocs: Docstrings, scalaVersionSelector: ScalaVersionSelector, sourceMapper: SourceMapper, + workspaceFolder: AbsolutePath, )(implicit rc: ReportContext) { private implicit def ec: ExecutionContextExecutorService = executionContext @@ -306,7 +307,15 @@ final case class Indexer( check() buildTools() .loadSupported() - formattingProvider().validateWorkspace() + .map(_.projectRoot) + .distinct match { + case Nil => formattingProvider().validateWorkspace(workspaceFolder) + case paths => + paths.foreach( + formattingProvider().validateWorkspace(_) + ) + } + } timerProvider.timedThunk( "started file watcher", diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index 7b9246aa0d1..06b6f5aaf66 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -104,9 +104,9 @@ import org.eclipse.{lsp4j => l} * manage the lifecycle of this executor. * @param serverInputs * Collection of different parameters used by Metals for running, - * which main purpose is allowing for custom bahaviour in tests. + * which main purpose is allowing for custom behavior in tests. * @param workspace - * An absolute path to the workscape. + * An absolute path to the workspace. * @param client * Metals client used for sending notifications to the client. This class DO * NOT manage the lifecycle of this client. It is the responsibility of the @@ -215,7 +215,6 @@ class MetalsLspService( private val sourceMapper = SourceMapper( buildTargets, buffers, - () => folder, ) private val scalaVersionSelector = new ScalaVersionSelector( @@ -981,7 +980,12 @@ class MetalsLspService( } } .getOrElse(Future.successful(())) - Future.sequence(List(restartBuildServer, resetDecorations)).map(_ => ()) + + Future + .sequence( + List(restartBuildServer, resetDecorations) + ) + .map(_ => ()) } override def didOpen( @@ -1175,8 +1179,7 @@ class MetalsLspService( .asScala .flatMap { case FileOutOfScalaCliBspScope.regenerateAndRestart => - val buildTool = - ScalaCliBuildTool(folder, userConfig) + val buildTool = ScalaCliBuildTool(folder, folder, userConfig) for { _ <- buildTool.generateBspConfig( folder, @@ -1392,7 +1395,10 @@ class MetalsLspService( if (path.isJava) javaFormattingProvider.format(params) else - formattingProvider.format(path, token) + for { + projectRoot <- calculateOptProjectRoot().map(_.getOrElse(folder)) + res <- formattingProvider.format(path, projectRoot, token) + } yield res } override def onTypeFormatting( @@ -1647,12 +1653,12 @@ class MetalsLspService( } yield bloopServers.shutdownServer() case Some(session) if session.main.isSbt => for { - currentBuildTool <- supportedBuildTool + currentBuildTool <- buildTool() res <- currentBuildTool match { case Some(sbt: SbtBuildTool) => for { _ <- disconnectOldBuildServer() - code <- sbt.shutdownBspServer(shellRunner, folder) + code <- sbt.shutdownBspServer(shellRunner) } yield code == 0 case _ => Future.successful(false) } @@ -1939,12 +1945,28 @@ class MetalsLspService( }) } - private def supportedBuildTool(): Future[Option[BuildTool]] = { + private def buildTool(): Future[Option[BuildTool]] = { + buildTools.loadSupported match { + case Nil => Future(None) + case buildTools => + for { + Some(buildTool) <- buildToolSelector.checkForChosenBuildTool( + buildTools + ) + if isCompatibleVersion(buildTool) + } yield Some(buildTool) + } + } + + private def isCompatibleVersion(buildTool: BuildTool) = + SemVer.isCompatibleVersion( + buildTool.minimumVersion, + buildTool.version, + ) + + def supportedBuildTool(): Future[Option[BuildTool]] = { def isCompatibleVersion(buildTool: BuildTool) = { - val isCompatibleVersion = SemVer.isCompatibleVersion( - buildTool.minimumVersion, - buildTool.version, - ) + val isCompatibleVersion = this.isCompatibleVersion(buildTool) if (isCompatibleVersion) { Some(buildTool) } else { @@ -1958,7 +1980,7 @@ class MetalsLspService( buildTools.loadSupported match { case Nil => { - if (!buildTools.isAutoConnectable) { + if (!buildTools.isAutoConnectable()) { warnings.noBuildTool() } // wait for a bsp file to show up @@ -1978,7 +2000,7 @@ class MetalsLspService( forceImport: Boolean ): Future[BuildChange] = for { - possibleBuildTool <- supportedBuildTool + possibleBuildTool <- supportedBuildTool() chosenBuildServer = tables.buildServers.selectedServer() isBloopOrEmpty = chosenBuildServer.isEmpty || chosenBuildServer.exists( _ == BloopServers.name @@ -2022,12 +2044,11 @@ class MetalsLspService( */ def maybeSetupScalaCli(): Future[Unit] = { if ( - !buildTools.isAutoConnectable + !buildTools.isAutoConnectable() && buildTools.loadSupported.isEmpty && folder.hasScalaFiles - ) { - scalaCli.setupIDE(folder) - } else Future.successful(()) + ) scalaCli.setupIDE(folder) + else Future.successful(()) } private def slowConnectToBloopServer( @@ -2044,38 +2065,49 @@ class MetalsLspService( change <- { if (result.isInstalled) quickConnectToBuildServer() else if (result.isFailed) { - if (buildTools.isAutoConnectable) { - // TODO(olafur) try to connect but gracefully error - languageClient.showMessage( - Messages.ImportProjectPartiallyFailed - ) - // Connect nevertheless, many build import failures are caused - // by resolution errors in one weird module while other modules - // exported successfully. - quickConnectToBuildServer() - } else { - languageClient.showMessage(Messages.ImportProjectFailed) - Future.successful(BuildChange.Failed) - } + for { + maybeProjectRoot <- calculateOptProjectRoot() + change <- + if (buildTools.isAutoConnectable(maybeProjectRoot)) { + // TODO(olafur) try to connect but gracefully error + languageClient.showMessage( + Messages.ImportProjectPartiallyFailed + ) + // Connect nevertheless, many build import failures are caused + // by resolution errors in one weird module while other modules + // exported successfully. + quickConnectToBuildServer() + } else { + languageClient.showMessage(Messages.ImportProjectFailed) + Future.successful(BuildChange.Failed) + } + } yield change } else { Future.successful(BuildChange.None) } + } } yield change - def quickConnectToBuildServer(): Future[BuildChange] = { - val connected = if (!buildTools.isAutoConnectable) { - scribe.warn("Build server is not auto-connectable.") - Future.successful(BuildChange.None) - } else { - autoConnectToBuildServer() - } + def calculateOptProjectRoot(): Future[Option[AbsolutePath]] = + for { + possibleBuildTool <- buildTool() + } yield possibleBuildTool.map(_.projectRoot).orElse(buildTools.bloopProject) - connected.map { change => + def quickConnectToBuildServer(): Future[BuildChange] = + for { + optRoot <- calculateOptProjectRoot() + change <- + if (!buildTools.isAutoConnectable(optRoot)) { + scribe.warn("Build server is not auto-connectable.") + Future.successful(BuildChange.None) + } else { + autoConnectToBuildServer() + } + } yield { buildServerPromise.trySuccess(()) change } - } private def maybeQuickConnectToBuildServer( params: b.DidChangeBuildTarget @@ -2136,8 +2168,10 @@ class MetalsLspService( (for { _ <- disconnectOldBuildServer() + maybeProjectRoot <- calculateOptProjectRoot() maybeSession <- timerProvider.timed("Connected to build server", true) { bspConnector.connect( + maybeProjectRoot.getOrElse(folder), folder, userConfig(), shellRunner, @@ -2302,6 +2336,7 @@ class MetalsLspService( symbolDocs, scalaVersionSelector, sourceMapper, + folder, ) private def checkRunningBloopVersion(bspServerVersion: String) = { @@ -2629,7 +2664,7 @@ class MetalsLspService( } } - private def clearBloopDir(): Unit = { + private def clearBloopDir(folder: AbsolutePath): Unit = { try BloopDir.clear(folder) catch { case e: Throwable => @@ -2638,17 +2673,19 @@ class MetalsLspService( } } - def resetWorkspace(): Future[Unit] = { - if (buildTools.isBloop) { - bloopServers.shutdownServer() - } - disconnectOldBuildServer() - .map { _ => - if (buildTools.isBloop) clearBloopDir() - tables.cleanAll() + def resetWorkspace(): Future[Unit] = + for { + maybeProjectRoot <- calculateOptProjectRoot() + _ <- disconnectOldBuildServer() + _ = maybeProjectRoot match { + case Some(path) if buildTools.isBloop(path) => + bloopServers.shutdownServer() + clearBloopDir(path) + case _ => } - .flatMap(_ => autoConnectToBuildServer().map(_ => ())) - } + _ = tables.cleanAll() + _ <- autoConnectToBuildServer().map(_ => ()) + } yield () def getTastyForURI(uri: URI): Future[Either[String, String]] = fileDecoderProvider.getTastyForURI(uri) diff --git a/metals/src/main/scala/scala/meta/internal/metals/SourceMapper.scala b/metals/src/main/scala/scala/meta/internal/metals/SourceMapper.scala index 4cd698ce494..2501ac1360f 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/SourceMapper.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/SourceMapper.scala @@ -11,7 +11,6 @@ import org.eclipse.{lsp4j => l} final case class SourceMapper( buildTargets: BuildTargets, buffers: Buffers, - workspace: () => AbsolutePath, ) { def mappedFrom(path: AbsolutePath): Option[AbsolutePath] = buildTargets.mappedFrom(path) diff --git a/metals/src/main/scala/scala/meta/internal/metals/ammonite/Ammonite.scala b/metals/src/main/scala/scala/meta/internal/metals/ammonite/Ammonite.scala index 272c85e89e3..06f9ee42a83 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ammonite/Ammonite.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ammonite/Ammonite.scala @@ -243,6 +243,7 @@ final class Ammonite( val commandWithJVMOpts = command.addJvmArgs(jvmOpts: _*) val futureConn = BuildServerConnection.fromSockets( + workspace(), workspace(), buildClient, languageClient, diff --git a/metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala b/metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala index ce221194bf5..498dafbe2c0 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala @@ -218,6 +218,7 @@ class ScalaCli( val nextSt = ConnectionState.Connecting(path) if (state.compareAndSet(ConnectionState.Empty, nextSt)) { val futureConn = BuildServerConnection.fromSockets( + connDir, connDir, buildClient(), languageClient, @@ -423,7 +424,7 @@ object ScalaCli { def scalaCliBspJsonContent( args: List[String] = Nil, - root: String = ".", + projectRoot: String = ".", ): String = { val argv = List( ScalaCli.javaCommand, @@ -431,7 +432,7 @@ object ScalaCli { ScalaCli.scalaCliClassPath().mkString(File.pathSeparator), ScalaCli.scalaCliMainClass, "bsp", - root, + projectRoot, ) ++ args val bsjJson = ujson.Obj( "name" -> "scala-cli", diff --git a/tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala b/tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala index d171e0a771f..3dfa69f7d8d 100644 --- a/tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala +++ b/tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala @@ -11,9 +11,10 @@ class MultipleBuildFilesLspSuite // SBT will be the main tool for this test, which is what will be // chosen when the user is prompted in the test - val buildTool: SbtBuildTool = SbtBuildTool(None, () => userConfig) + def buildTool: SbtBuildTool = SbtBuildTool(None, workspace, () => userConfig) - val alternativeBuildTool: MillBuildTool = MillBuildTool(() => userConfig) + def alternativeBuildTool: MillBuildTool = + MillBuildTool(() => userConfig, workspace) def chooseBuildToolMessage: String = ChooseBuildTool.params(List(buildTool, alternativeBuildTool)).getMessage diff --git a/tests/slow/src/test/scala/tests/gradle/GradleLspSuite.scala b/tests/slow/src/test/scala/tests/gradle/GradleLspSuite.scala index 0b2e78e2f0c..22cdf49e0f9 100644 --- a/tests/slow/src/test/scala/tests/gradle/GradleLspSuite.scala +++ b/tests/slow/src/test/scala/tests/gradle/GradleLspSuite.scala @@ -17,7 +17,7 @@ import tests.BaseImportSuite class GradleLspSuite extends BaseImportSuite("gradle-import") { - val buildTool: GradleBuildTool = GradleBuildTool(() => userConfig) + def buildTool: GradleBuildTool = GradleBuildTool(() => userConfig, workspace) override def currentDigest( workspace: AbsolutePath @@ -72,6 +72,46 @@ class GradleLspSuite extends BaseImportSuite("gradle-import") { } } + test("inner") { + client.importBuild = ImportBuild.yes + cleanWorkspace() + for { + _ <- initialize( + s"""|/inner/build.gradle + |plugins { + | id 'scala' + |} + |repositories { + | mavenCentral() + |} + |dependencies { + | implementation 'org.scala-lang:scala-library:${V.scala213}' + |} + |/inner/src/main/scala/A.scala + |object A { + | val foo: Int = "aaa" + |} + |""".stripMargin + ) + _ <- server.server.indexingPromise.future + _ = assert(server.server.bspSession.get.main.isBloop) + buildTool <- server.server.supportedBuildTool() + _ = assertEquals(buildTool.get.executableName, "gradle") + _ = assertEquals(buildTool.get.projectRoot, workspace.resolve("inner")) + _ <- server.didOpen("inner/src/main/scala/A.scala") + _ <- server.didSave("inner/src/main/scala/A.scala")(identity) + _ = assertNoDiff( + client.pathDiagnostics("inner/src/main/scala/A.scala"), + """|inner/src/main/scala/A.scala:2:18: error: type mismatch; + | found : String("aaa") + | required: Int + | val foo: Int = "aaa" + | ^^^^^ + |""".stripMargin, + ) + } yield () + } + test("basic-configured") { cleanWorkspace() for { diff --git a/tests/slow/src/test/scala/tests/maven/MavenLspSuite.scala b/tests/slow/src/test/scala/tests/maven/MavenLspSuite.scala index 2dbbfa35d07..80f025c01f7 100644 --- a/tests/slow/src/test/scala/tests/maven/MavenLspSuite.scala +++ b/tests/slow/src/test/scala/tests/maven/MavenLspSuite.scala @@ -17,7 +17,7 @@ import tests.BaseImportSuite class MavenLspSuite extends BaseImportSuite("maven-import") { - val buildTool: MavenBuildTool = MavenBuildTool(() => userConfig) + def buildTool: MavenBuildTool = MavenBuildTool(() => userConfig, workspace) val defaultPom: String = new String( InputStreamIO.readBytes( diff --git a/tests/slow/src/test/scala/tests/mill/MillLspSuite.scala b/tests/slow/src/test/scala/tests/mill/MillLspSuite.scala index f24a59465cc..01b87b31b47 100644 --- a/tests/slow/src/test/scala/tests/mill/MillLspSuite.scala +++ b/tests/slow/src/test/scala/tests/mill/MillLspSuite.scala @@ -10,7 +10,7 @@ import tests.BaseImportSuite class MillLspSuite extends BaseImportSuite("mill-import") { - val buildTool: MillBuildTool = MillBuildTool(() => userConfig) + def buildTool: MillBuildTool = MillBuildTool(() => userConfig, workspace) override def currentDigest( workspace: AbsolutePath diff --git a/tests/slow/src/test/scala/tests/mill/MillServerSuite.scala b/tests/slow/src/test/scala/tests/mill/MillServerSuite.scala index aae4a50a465..aa37ce29641 100644 --- a/tests/slow/src/test/scala/tests/mill/MillServerSuite.scala +++ b/tests/slow/src/test/scala/tests/mill/MillServerSuite.scala @@ -25,7 +25,7 @@ class MillServerSuite val preBspVersion = "0.9.10" val supportedBspVersion = V.millVersion val scalaVersion = V.scala213 - val buildTool: MillBuildTool = MillBuildTool(() => userConfig) + def buildTool: MillBuildTool = MillBuildTool(() => userConfig, workspace) def bspTrace: AbsolutePath = workspace.resolve(".metals").resolve("bsp.trace.json") diff --git a/tests/slow/src/test/scala/tests/sbt/SbtBloopLspSuite.scala b/tests/slow/src/test/scala/tests/sbt/SbtBloopLspSuite.scala index 189dadf3dfc..246f500008f 100644 --- a/tests/slow/src/test/scala/tests/sbt/SbtBloopLspSuite.scala +++ b/tests/slow/src/test/scala/tests/sbt/SbtBloopLspSuite.scala @@ -28,7 +28,7 @@ class SbtBloopLspSuite val sbtVersion = V.sbtVersion val scalaVersion = V.scala213 - val buildTool: SbtBuildTool = SbtBuildTool(None, () => userConfig) + val buildTool: SbtBuildTool = SbtBuildTool(None, workspace, () => userConfig) override def currentDigest( workspace: AbsolutePath @@ -77,6 +77,40 @@ class SbtBloopLspSuite } } + test("inner") { + cleanWorkspace() + client.importBuild = ImportBuild.yes + for { + _ <- initialize( + s"""|/inner/project/build.properties + |sbt.version=$sbtVersion + |/inner/build.sbt + |scalaVersion := "${V.scala213}" + |/inner/src/main/scala/A.scala + | + |object A { + | val i: Int = "aaa" + |} + |""".stripMargin + ) + _ <- server.server.indexingPromise.future + _ = assert(workspace.resolve("inner/.bloop").exists) + _ = assert(server.server.bspSession.get.main.isBloop) + _ <- server.didOpen("inner/src/main/scala/A.scala") + _ <- server.didSave("inner/src/main/scala/A.scala")(identity) + _ = assertNoDiff( + client.pathDiagnostics("inner/src/main/scala/A.scala"), + """|inner/src/main/scala/A.scala:3:16: error: type mismatch; + | found : String("aaa") + | required: Int + | val i: Int = "aaa" + | ^^^^^ + |""".stripMargin, + ) + } yield () + + } + test("no-sbt-version") { cleanWorkspace() for { @@ -501,7 +535,7 @@ class SbtBloopLspSuite _ = assertNoDiff( client.workspaceShowMessages, IncompatibleBuildToolVersion - .params(SbtBuildTool(Some("0.13.15"), () => userConfig)) + .params(SbtBuildTool(Some("0.13.15"), workspace, () => userConfig)) .getMessage, ) } yield () @@ -509,7 +543,11 @@ class SbtBloopLspSuite test("min-sbt-version") { val minimal = - SbtBuildTool(None, () => UserConfiguration.default).minimumVersion + SbtBuildTool( + None, + workspace, + () => UserConfiguration.default, + ).minimumVersion cleanWorkspace() for { _ <- initialize( diff --git a/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala b/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala index 0014a351c8b..75b47c71884 100644 --- a/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala +++ b/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala @@ -35,7 +35,7 @@ class SbtServerSuite val supportedMetaBuildVersion = "1.6.0-M1" val supportedBspVersion = V.sbtVersion val scalaVersion = V.scala213 - val buildTool: SbtBuildTool = SbtBuildTool(None, () => userConfig) + val buildTool: SbtBuildTool = SbtBuildTool(None, workspace, () => userConfig) var compilationCount = 0 override def beforeEach(context: BeforeEach): Unit = { @@ -105,6 +105,39 @@ class SbtServerSuite } } + test("inner-project") { + cleanWorkspace() + client.importBuildChanges = ImportBuildChanges.yes + for { + _ <- initialize( + SbtBuildLayout( + """|/inner/a/src/main/scala/A.scala + | + |object A { + | val foo: Int = "aaa" + |} + |""".stripMargin, + V.scala213, + "/inner", + ) + ) + _ <- server.server.indexingPromise.future + _ = assert(workspace.resolve(".bsp/sbt.json").exists) + _ = assert(server.server.bspSession.get.main.isSbt) + _ <- server.didOpen("inner/a/src/main/scala/A.scala") + _ <- server.didSave("inner/a/src/main/scala/A.scala")(identity) + _ = assertNoDiff( + client.pathDiagnostics("inner/a/src/main/scala/A.scala"), + """|inner/a/src/main/scala/A.scala:3:18: error: type mismatch; + | found : String("aaa") + | required: Int + | val foo: Int = "aaa" + | ^^^^^ + |""".stripMargin, + ) + } yield () + } + test("reload plugins") { // should reload existing server after writing the metals.sbt plugin file cleanWorkspace @@ -168,7 +201,7 @@ class SbtServerSuite ) client.showMessages.clear() } - // This is a little hacky but up above this promise is suceeded already, so down + // This is a little hacky but up above this promise is succeeded already, so down // below it won't wait until it reconnects to Sbt and indexed like we want _ = server.server.indexingPromise = Promise() _ <- server.didSave("build.sbt") { text => diff --git a/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala b/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala index b6b01c4d93b..8a97b5fafa9 100644 --- a/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala +++ b/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala @@ -203,6 +203,7 @@ class ScalaCliSuite extends BaseScalaCliSuite(V.scala3) { |""".stripMargin test(s"simple-file-bsp") { + cleanWorkspace() simpleFileTest(useBsp = true) } @@ -211,6 +212,7 @@ class ScalaCliSuite extends BaseScalaCliSuite(V.scala3) { } test(s"simple-script-bsp") { + cleanWorkspace() simpleScriptTest(useBsp = true) } @@ -398,7 +400,7 @@ class ScalaCliSuite extends BaseScalaCliSuite(V.scala3) { | def foo = 3 | val m = foo |/.bsp/scala-cli.json - |${ScalaCli.scalaCliBspJsonContent(root = workspace.resolve("src/Main.scala").toString())} + |${ScalaCli.scalaCliBspJsonContent(projectRoot = workspace.resolve("src/Main.scala").toString())} |/.scala-build/ide-inputs.json |${BaseScalaCliSuite.scalaCliIdeInputJson(".")} |""".stripMargin diff --git a/tests/unit/src/main/scala/tests/BuildServerLayout.scala b/tests/unit/src/main/scala/tests/BuildServerLayout.scala index c12c2027fa8..e2aa8159a84 100644 --- a/tests/unit/src/main/scala/tests/BuildServerLayout.scala +++ b/tests/unit/src/main/scala/tests/BuildServerLayout.scala @@ -29,10 +29,16 @@ object SbtBuildLayout extends BuildToolLayout { override def apply( sourceLayout: String, scalaVersion: String, + ): String = apply(sourceLayout, scalaVersion, "") + + def apply( + sourceLayout: String, + scalaVersion: String, + directory: String, ): String = { - s"""|/project/build.properties + s"""|$directory/project/build.properties |sbt.version=${V.sbtVersion} - |/build.sbt + |$directory/build.sbt |$commonSbtSettings |ThisBuild / scalaVersion := "$scalaVersion" |val a = project.in(file("a")) diff --git a/tests/unit/src/test/scala/tests/MillVersionSuite.scala b/tests/unit/src/test/scala/tests/MillVersionSuite.scala index 745097d530a..e9d477a19e5 100644 --- a/tests/unit/src/test/scala/tests/MillVersionSuite.scala +++ b/tests/unit/src/test/scala/tests/MillVersionSuite.scala @@ -12,7 +12,7 @@ class MillVersionSuite extends BaseSuite { test(expected) { val root = FileLayout.fromString(layout) val obtained = - MillBuildTool(() => UserConfiguration()).bloopInstallArgs(root) + MillBuildTool(() => UserConfiguration(), root).bloopInstallArgs(root) assert(obtained.contains(expected)) } }