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 163119842e2..439aa19c85a 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala @@ -6,7 +6,9 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.meta.internal.bsp.BspConfigGenerationStatus._ +import scala.meta.internal.builds.BloopInstallProvider import scala.meta.internal.builds.BuildServerProvider +import scala.meta.internal.builds.BuildTool import scala.meta.internal.builds.BuildTools import scala.meta.internal.builds.SbtBuildTool import scala.meta.internal.builds.ShellRunner @@ -43,13 +45,9 @@ class BspConnector( * Resolves the current build servers that either have a bsp entry or if the * workspace can support Bloop, it will also resolve Bloop. */ - def resolve(): BspResolvedResult = { + def resolve(buildTool: Option[BuildTool]): BspResolvedResult = { resolveExplicit().getOrElse { - if ( - buildTools - .loadSupported() - .exists(_.isBloopDefaultBsp) || buildTools.isBloop - ) + if (buildTool.isInstanceOf[BloopInstallProvider] || buildTools.isBloop) ResolvedBloop else bspServers.resolve() } @@ -74,11 +72,12 @@ class BspConnector( * of the bsp entry has already happened at this point. */ def connect( - projectRoot: AbsolutePath, + buildTool: Option[BuildTool], workspace: AbsolutePath, userConfiguration: UserConfiguration, shellRunner: ShellRunner, )(implicit ec: ExecutionContext): Future[Option[BspSession]] = { + val projectRoot = buildTool.map(_.projectRoot).getOrElse(workspace) def connect( projectRoot: AbsolutePath, bspTraceRoot: AbsolutePath, @@ -86,7 +85,7 @@ class BspConnector( ): Future[Option[BuildServerConnection]] = { def bspStatusOpt = Option.when(addLivenessMonitor)(bspStatus) scribe.info("Attempting to connect to the build server...") - resolve() match { + resolve(buildTool) match { case ResolvedNone => scribe.info("No build server found") Future.successful(None) @@ -180,7 +179,9 @@ class BspConnector( possibleBuildServerConn match { case None => Future.successful(None) case Some(buildServerConn) - if buildServerConn.isBloop && buildTools.isSbt => + if buildServerConn.isBloop && buildTool.exists( + _.isInstanceOf[SbtBuildTool] + ) => // 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 @@ -277,7 +278,9 @@ class BspConnector( BspConnectionDetails, ]] = { if ( - bloopPresent || buildTools.loadSupported().exists(_.isBloopDefaultBsp) + bloopPresent || buildTools + .loadSupported() + .exists(_.isInstanceOf[BloopInstallProvider]) ) new BspConnectionDetails( BloopServers.name, 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 d6593e8f1f8..a24bcb62466 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BloopInstall.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BloopInstall.scala @@ -37,7 +37,7 @@ final class BloopInstall( override def toString: String = s"BloopInstall($workspace)" def runUnconditionally( - buildTool: BuildTool, + buildTool: BloopInstallProvider, isImportInProcess: AtomicBoolean, ): Future[WorkspaceLoadedStatus] = { if (isImportInProcess.compareAndSet(false, true)) { @@ -121,7 +121,7 @@ final class BloopInstall( // notifications. This method is synchronized to prevent asking the user // twice whether to import the build. def runIfApproved( - buildTool: BuildTool, + buildTool: BloopInstallProvider, digest: String, isImportInProcess: AtomicBoolean, ): Future[WorkspaceLoadedStatus] = diff --git a/metals/src/main/scala/scala/meta/internal/builds/BloopInstallProvider.scala b/metals/src/main/scala/scala/meta/internal/builds/BloopInstallProvider.scala index ca18924a2a4..ccc4c5b12aa 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BloopInstallProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BloopInstallProvider.scala @@ -6,7 +6,7 @@ import scala.meta.io.AbsolutePath /** * Helper trait for build tools that have a Bloop plugin */ -trait BloopInstallProvider { this: BuildTool => +trait BloopInstallProvider extends BuildTool { /** * Method used to generate the necesary .bloop files for the diff --git a/metals/src/main/scala/scala/meta/internal/builds/BspOnly.scala b/metals/src/main/scala/scala/meta/internal/builds/BspOnly.scala new file mode 100644 index 00000000000..c175d8e23c7 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/builds/BspOnly.scala @@ -0,0 +1,14 @@ +package scala.meta.internal.builds + +import scala.meta.io.AbsolutePath + +/** + * Build tool for custom bsp detected in `.bsp/.json` + */ +case class BspOnly( + override val executableName: String, + override val projectRoot: AbsolutePath, +) extends BuildTool { + override def digest(workspace: AbsolutePath): Option[String] = Some("OK") + override val forcesBuildServer = true +} 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 5724e745a19..c6670036ee7 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala @@ -4,32 +4,12 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption -import scala.concurrent.Future - import scala.meta.io.AbsolutePath trait BuildTool { - /** - * Export the build to Bloop - * - * This operation should be roughly equivalent to running `sbt bloopInstall` - * and should work for both updating an existing Bloop build or creating a new - * Bloop build. - */ - def bloopInstall( - workspace: AbsolutePath, - systemProcess: List[String] => Future[WorkspaceLoadedStatus], - ): Future[WorkspaceLoadedStatus] - def digest(workspace: AbsolutePath): Option[String] - def version: String - - def minimumVersion: String - - def recommendedVersion: String - protected lazy val tempDir: Path = { val dir = Files.createTempDirectory("metals") dir.toFile.deleteOnExit() @@ -40,15 +20,14 @@ trait BuildTool { def executableName: String - def isBloopDefaultBsp = true - def projectRoot: AbsolutePath + val forcesBuildServer = false + } object BuildTool { - case class Found(buildTool: BuildTool, digest: String) def copyFromResource( tempDir: Path, filePath: String, @@ -62,4 +41,16 @@ object BuildTool { outFile } + trait Verified + case class IncompatibleVersion(buildTool: VersionRecommendation) + extends Verified { + def message: String = s"Unsupported $buildTool version ${buildTool.version}" + } + case class NoChecksum(buildTool: BuildTool, root: AbsolutePath) + extends Verified { + def message: String = + s"Could not calculate checksum for ${buildTool.executableName} in $root" + } + case class Found(buildTool: BuildTool, digest: String) extends Verified + } 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 b9c44746772..13b891d309a 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala @@ -9,6 +9,7 @@ import scala.meta.internal.io.PathIO import scala.meta.internal.metals.BloopServers import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.UserConfiguration +import scala.meta.internal.metals.scalacli.ScalaCli import scala.meta.io.AbsolutePath import ujson.ParsingFailedException @@ -121,6 +122,26 @@ final class BuildTools( ) def isBazel: Boolean = bazelProject.isDefined + private def customBsps: List[BspOnly] = { + val bspFolder = workspace.resolve(".bsp") + val root = customProjectRoot.getOrElse(workspace) + if (bspFolder.exists && bspFolder.isDirectory) { + bspFolder.toFile + .listFiles() + .collect { + case file + if file.isFile() && file.getName().endsWith(".json") && !knowBsps( + file.getName().stripSuffix(".json") + ) => + BspOnly(file.getName().stripSuffix(".json"), root) + } + .toList + } else Nil + } + + private def knowBsps = + Set(SbtBuildTool.name, MillBuildTool.name) ++ ScalaCli.names + private def customProjectRoot = userConfig().customProjectRoot .map(relativePath => workspace.resolve(relativePath.trim())) @@ -187,6 +208,7 @@ final class BuildTools( mavenProject.foreach(buf += MavenBuildTool(userConfig, _)) millProject.foreach(buf += MillBuildTool(userConfig, _)) scalaCliProject.foreach(buf += ScalaCliBuildTool(workspace, _, userConfig)) + buf.addAll(customBsps) buf.result() } @@ -221,6 +243,7 @@ final class BuildTools( val before = lastDetectedBuildTools.getAndUpdate(_ + buildTool) !before.contains(buildTool) } + } object BuildTools { 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 3b7f38deefb..077817c9260 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/GradleBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/GradleBuildTool.scala @@ -20,7 +20,8 @@ case class GradleBuildTool( userConfig: () => UserConfiguration, projectRoot: AbsolutePath, ) extends BuildTool - with BloopInstallProvider { + with BloopInstallProvider + with VersionRecommendation { private val initScriptName = "init-script.gradle" private val gradleBloopVersion = BuildInfo.gradleBloopVersion 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 a2ab5d48ffe..ba655d9f601 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/MavenBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/MavenBuildTool.scala @@ -10,7 +10,8 @@ case class MavenBuildTool( userConfig: () => UserConfiguration, projectRoot: AbsolutePath, ) extends BuildTool - with BloopInstallProvider { + with BloopInstallProvider + with VersionRecommendation { private lazy val embeddedMavenLauncher: AbsolutePath = { val out = BuildTool.copyFromResource(tempDir, "maven-wrapper.jar") 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 0510de98a62..48c373e036d 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/MillBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/MillBuildTool.scala @@ -14,7 +14,8 @@ case class MillBuildTool( projectRoot: AbsolutePath, ) extends BuildTool with BloopInstallProvider - with BuildServerProvider { + with BuildServerProvider + with VersionRecommendation { private def getMillVersion(workspace: AbsolutePath): String = { import scala.meta.internal.jdk.CollectionConverters._ 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 14690d8a747..2850245e564 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/SbtBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/SbtBuildTool.scala @@ -26,7 +26,8 @@ case class SbtBuildTool( userConfig: () => UserConfiguration, ) extends BuildTool with BloopInstallProvider - with BuildServerProvider { + with BuildServerProvider + with VersionRecommendation { import SbtBuildTool._ 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 091f0b85e18..5fbd6013164 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala @@ -17,7 +17,8 @@ class ScalaCliBuildTool( val projectRoot: AbsolutePath, userConfig: () => UserConfiguration, ) extends BuildTool - with BuildServerProvider { + with BuildServerProvider + with VersionRecommendation { lazy val runScalaCliCommand: Option[Seq[String]] = ScalaCli.localScalaCli(userConfig()) @@ -47,12 +48,6 @@ class ScalaCliBuildTool( ) ) - override def bloopInstall( - workspace: AbsolutePath, - systemProcess: List[String] => Future[WorkspaceLoadedStatus], - ): Future[WorkspaceLoadedStatus] = - Future.successful(WorkspaceLoadedStatus.Dismissed) - override def digest(workspace: AbsolutePath): Option[String] = ScalaCliDigest.current(workspace) @@ -64,16 +59,19 @@ class ScalaCliBuildTool( override def executableName: String = ScalaCliBuildTool.name - override def isBloopDefaultBsp = false + override val forcesBuildServer = true + + def isBspGenerated(workspace: AbsolutePath): Boolean = + ScalaCliBuildTool.pathsToScalaCliBsp(workspace).exists(_.isFile) } object ScalaCliBuildTool { def name = "scala-cli" - def pathsToScalaCliBsp(root: AbsolutePath): List[AbsolutePath] = List( - root.resolve(".bsp").resolve("scala-cli.json"), - root.resolve(".bsp").resolve("scala.json"), - ) + def pathsToScalaCliBsp(root: AbsolutePath): List[AbsolutePath] = + ScalaCli.names.toList.map(name => + root.resolve(".bsp").resolve(s"$name.json") + ) def apply( workspace: AbsolutePath, diff --git a/metals/src/main/scala/scala/meta/internal/builds/VersionRecommendation.scala b/metals/src/main/scala/scala/meta/internal/builds/VersionRecommendation.scala new file mode 100644 index 00000000000..5d70db053b2 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/builds/VersionRecommendation.scala @@ -0,0 +1,7 @@ +package scala.meta.internal.builds + +trait VersionRecommendation { self: BuildTool => + def minimumVersion: String + def recommendedVersion: String + def version: String +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/Messages.scala b/metals/src/main/scala/scala/meta/internal/metals/Messages.scala index 317091cb03c..cbd8048f6a7 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Messages.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Messages.scala @@ -5,6 +5,7 @@ import java.nio.file.Path import scala.collection.mutable import scala.meta.internal.builds.BuildTool +import scala.meta.internal.builds.VersionRecommendation import scala.meta.internal.jdk.CollectionConverters._ import scala.meta.internal.metals.BloopJsonUpdateCause.BloopJsonUpdateCause import scala.meta.internal.metals.clients.language.MetalsInputBoxParams @@ -193,13 +194,13 @@ object Messages { } object ChooseBuildTool { + def message = + "Multiple build definitions found. Which would you like to use?" def params(builtTools: List[BuildTool]): ShowMessageRequestParams = { val messageActionItems = builtTools.map(bt => new MessageActionItem(bt.executableName)) val params = new ShowMessageRequestParams() - params.setMessage( - "Multiple build definitions found. Which would you like to use?" - ) + params.setMessage(message) params.setType(MessageType.Info) params.setActions(messageActionItems.asJava) params @@ -288,7 +289,7 @@ object Messages { def learnMoreUrl: String = Urls.docs("import-build") - def params(tool: BuildTool): ShowMessageRequestParams = { + def params(tool: VersionRecommendation): ShowMessageRequestParams = { def toFixMessage = s"To fix this problem, upgrade to $tool ${tool.recommendedVersion} " 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 44aa20d0c85..d52e544d83c 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -29,14 +29,17 @@ import scala.meta.internal.bsp.BuildChange import scala.meta.internal.bsp.ConnectionBspStatus import scala.meta.internal.bsp.ScalaCliBspScope import scala.meta.internal.builds.BloopInstall +import scala.meta.internal.builds.BloopInstallProvider import scala.meta.internal.builds.BspErrorHandler import scala.meta.internal.builds.BuildServerProvider import scala.meta.internal.builds.BuildTool import scala.meta.internal.builds.BuildToolSelector import scala.meta.internal.builds.BuildTools +import scala.meta.internal.builds.Digest import scala.meta.internal.builds.SbtBuildTool import scala.meta.internal.builds.ScalaCliBuildTool import scala.meta.internal.builds.ShellRunner +import scala.meta.internal.builds.VersionRecommendation import scala.meta.internal.builds.WorkspaceReload import scala.meta.internal.decorations.PublishDecorationsParams import scala.meta.internal.decorations.SyntheticHoverProvider @@ -756,7 +759,7 @@ class MetalsLspService( diagnostics, languageClient, () => bspSession, - () => bspConnector.resolve(), + () => bspConnector.resolve(buildTool), tables, clientConfig, mtagsResolver, @@ -771,7 +774,7 @@ class MetalsLspService( () => tables.buildTool.selectedBuildTool(), buildTargets, () => bspSession, - () => bspConnector.resolve(), + () => bspConnector.resolve(buildTool), buildTools, ) @@ -909,6 +912,29 @@ class MetalsLspService( val isInitialized = new AtomicBoolean(false) + def fullConnect(): Future[Unit] = + for { + found <- supportedBuildTool() + chosenBuildServer = found match { + case Some(BuildTool.Found(buildServer, _)) + if buildServer.forcesBuildServer => + tables.buildServers.chooseServer(buildServer.executableName) + Some(buildServer.executableName) + case _ => tables.buildServers.selectedServer() + } + _ <- Future + .sequence( + List( + quickConnectToBuildServer(), + slowConnectToBuildServer( + forceImport = false, + found, + chosenBuildServer, + ), + ) + ) + } yield () + def initialized(): Future[Unit] = { loadFingerPrints() registerNiceToHaveFilePatterns() @@ -921,8 +947,7 @@ class MetalsLspService( .sequence( List[Future[Unit]]( Future(buildTools.initialize()), - quickConnectToBuildServer().ignoreValue, - slowConnectToBuildServer(forceImport = false).ignoreValue, + fullConnect().ignoreValue, Future(workspaceSymbols.indexClasspath()), Future(formattingProvider.load()), ) @@ -969,7 +994,7 @@ class MetalsLspService( if (userConfig.customProjectRoot != old.customProjectRoot) { tables.buildTool.reset() tables.buildServers.reset() - slowConnectToBuildServer(false).ignoreValue + fullConnect() } else Future.successful(()) val resetDecorations = @@ -1482,11 +1507,10 @@ class MetalsLspService( val path = params.getTextDocument.getUri.toAbsolutePath if (path.isJava) javaFormattingProvider.format(params) - else - for { - projectRoot <- calculateOptProjectRoot().map(_.getOrElse(folder)) - res <- formattingProvider.format(path, projectRoot, token) - } yield res + else { + val projectRoot = optProjectRoot.getOrElse(folder) + formattingProvider.format(path, projectRoot, token) + } } override def onTypeFormatting( @@ -1744,8 +1768,7 @@ class MetalsLspService( } yield bloopServers.shutdownServer() case Some(session) if session.main.isSbt => for { - currentBuildTool <- buildTool() - res <- currentBuildTool match { + res <- buildTool match { case Some(sbt: SbtBuildTool) => for { _ <- disconnectOldBuildServer() @@ -2022,46 +2045,34 @@ class MetalsLspService( }) } - private def buildTool(): Future[Option[BuildTool]] = { - buildTools.loadSupported match { - case Nil => Future(None) - case buildTools => - for { - buildTool <- buildToolSelector.checkForChosenBuildTool( - buildTools - ) - } yield buildTool.filter(isCompatibleVersion) - } - } - - private def isCompatibleVersion(buildTool: BuildTool) = - SemVer.isCompatibleVersion( - buildTool.minimumVersion, - buildTool.version, - ) - - def supportedBuildTool(): Future[Option[BuildTool.Found]] = { - def isCompatibleVersion(buildTool: BuildTool) = { - val isCompatibleVersion = this.isCompatibleVersion(buildTool) - if (isCompatibleVersion) { + private def buildTool: Option[BuildTool] = + for { + name <- tables.buildTool.selectedBuildTool() + buildTool <- buildTools.loadSupported.find(_.executableName == name) + found <- isCompatibleVersion(buildTool) match { + case BuildTool.Found(bt, _) => Some(bt) + case _ => None + } + } yield found + + def isCompatibleVersion(buildTool: BuildTool): BuildTool.Verified = { + buildTool match { + case buildTool: VersionRecommendation + if !SemVer.isCompatibleVersion( + buildTool.minimumVersion, + buildTool.version, + ) => + BuildTool.IncompatibleVersion(buildTool) + case _ => buildTool.digest(folder) match { case Some(digest) => - Some(BuildTool.Found(buildTool, digest)) - case None => - scribe.warn( - s"Could not calculate checksum for ${buildTool.executableName} in $folder" - ) - None + BuildTool.Found(buildTool, digest) + case None => BuildTool.NoChecksum(buildTool, folder) } - } else { - scribe.warn(s"Unsupported $buildTool version ${buildTool.version}") - languageClient.showMessage( - Messages.IncompatibleBuildToolVersion.params(buildTool) - ) - None - } } + } + def supportedBuildTool(): Future[Option[BuildTool.Found]] = { buildTools.loadSupported match { case Nil => { if (!buildTools.isAutoConnectable()) { @@ -2077,49 +2088,76 @@ class MetalsLspService( buildTools ) } yield { - buildTool.flatMap(isCompatibleVersion) + buildTool.flatMap { bt => + isCompatibleVersion(bt) match { + case found: BuildTool.Found => Some(found) + case warn @ BuildTool.IncompatibleVersion(buildTool) => + scribe.warn(warn.message) + languageClient.showMessage( + Messages.IncompatibleBuildToolVersion.params(buildTool) + ) + None + case warn: BuildTool.NoChecksum => + scribe.warn(warn.message) + None + } + } } } } def slowConnectToBuildServer( forceImport: Boolean - ): Future[BuildChange] = - for { - possibleBuildTool <- supportedBuildTool() - chosenBuildServer = tables.buildServers.selectedServer() - isBloopOrEmpty = chosenBuildServer.isEmpty || chosenBuildServer.exists( - _ == BloopServers.name - ) - buildChange <- possibleBuildTool match { - case Some(BuildTool.Found(buildTool: ScalaCliBuildTool, _)) - if chosenBuildServer.isEmpty => - tables.buildServers.chooseServer(ScalaCliBuildTool.name) - val scalaCliBspConfigExists = - ScalaCliBuildTool.pathsToScalaCliBsp(folder).exists(_.isFile) - if (scalaCliBspConfigExists) Future.successful(BuildChange.None) - else - buildTool - .generateBspConfig( - folder, - args => bspConfigGenerator.runUnconditionally(buildTool, args), - statusBar, - ) - .flatMap(_ => quickConnectToBuildServer()) - case Some(found) if isBloopOrEmpty => - slowConnectToBloopServer(forceImport, found.buildTool, found.digest) - case Some(found) => - indexer.reloadWorkspaceAndIndex( - forceImport, - found.buildTool, - found.digest, - importBuild, - ) - case None => - Future.successful(BuildChange.None) + ): Future[BuildChange] = for { + buildTool <- supportedBuildTool() + chosenBuildServer = tables.buildServers.selectedServer() + buildChange <- slowConnectToBuildServer( + forceImport, + buildTool, + chosenBuildServer, + ) + } yield buildChange - } - } yield buildChange + def slowConnectToBuildServer( + forceImport: Boolean, + buildTool: Option[BuildTool.Found], + chosenBuildServer: Option[String], + ): Future[BuildChange] = { + val isBloopOrEmpty = chosenBuildServer.isEmpty || chosenBuildServer.exists( + _ == BloopServers.name + ) + + buildTool match { + case Some(BuildTool.Found(buildTool: BloopInstallProvider, digest)) + if isBloopOrEmpty => + slowConnectToBloopServer(forceImport, buildTool, digest) + case Some(BuildTool.Found(buildTool: ScalaCliBuildTool, _)) + if !buildTool.isBspGenerated(folder) => + tables.buildServers.chooseServer(buildTool.executableName) + buildTool + .generateBspConfig( + folder, + args => bspConfigGenerator.runUnconditionally(buildTool, args), + statusBar, + ) + .flatMap(_ => quickConnectToBuildServer()) + case Some(BuildTool.Found(buildTool, _)) + if !chosenBuildServer.exists( + _ == buildTool.executableName + ) && buildTool.forcesBuildServer => + tables.buildServers.chooseServer(buildTool.executableName) + quickConnectToBuildServer() + case Some(found) => + indexer.reloadWorkspaceAndIndex( + forceImport, + found.buildTool, + found.digest, + importBuild, + ) + case None => + Future.successful(BuildChange.None) + } + } /** * If there is no auto-connectable build server and no supported build tool is found @@ -2136,7 +2174,7 @@ class MetalsLspService( private def slowConnectToBloopServer( forceImport: Boolean, - buildTool: BuildTool, + buildTool: BloopInstallProvider, checksum: String, ): Future[BuildChange] = for { @@ -2149,9 +2187,8 @@ class MetalsLspService( if (result.isInstalled) quickConnectToBuildServer() else if (result.isFailed) { for { - maybeProjectRoot <- calculateOptProjectRoot() change <- - if (buildTools.isAutoConnectable(maybeProjectRoot)) { + if (buildTools.isAutoConnectable(optProjectRoot)) { // TODO(olafur) try to connect but gracefully error languageClient.showMessage( Messages.ImportProjectPartiallyFailed @@ -2172,16 +2209,13 @@ class MetalsLspService( } } yield change - def calculateOptProjectRoot(): Future[Option[AbsolutePath]] = - for { - possibleBuildTool <- buildTool() - } yield possibleBuildTool.map(_.projectRoot).orElse(buildTools.bloopProject) + def optProjectRoot(): Option[AbsolutePath] = + buildTool.map(_.projectRoot).orElse(buildTools.bloopProject) def quickConnectToBuildServer(): Future[BuildChange] = for { - optRoot <- calculateOptProjectRoot() change <- - if (!buildTools.isAutoConnectable(optRoot)) { + if (!buildTools.isAutoConnectable(optProjectRoot)) { scribe.warn("Build server is not auto-connectable.") Future.successful(BuildChange.None) } else { @@ -2259,10 +2293,9 @@ class MetalsLspService( (for { _ <- disconnectOldBuildServer() - maybeProjectRoot <- calculateOptProjectRoot() maybeSession <- timerProvider.timed("Connected to build server", true) { bspConnector.connect( - maybeProjectRoot.getOrElse(folder), + buildTool, folder, userConfig, shellRunner, @@ -2347,10 +2380,16 @@ class MetalsLspService( s"Connected to Build server: ${session.main.name} v${session.version}" ) cancelables.add(session) + buildTool.foreach( + workspaceReload.persistChecksumStatus(Digest.Status.Started, _) + ) bspSession = Some(session) for { _ <- importBuild(session) _ <- indexer.profiledIndexWorkspace(runDoctorCheck) + _ = buildTool.foreach( + workspaceReload.persistChecksumStatus(Digest.Status.Installed, _) + ) _ = if (session.main.isBloop) checkRunningBloopVersion(session.version) } yield { BuildChange.Reconnected @@ -2767,9 +2806,8 @@ class MetalsLspService( def resetWorkspace(): Future[Unit] = for { - maybeProjectRoot <- calculateOptProjectRoot() _ <- disconnectOldBuildServer() - _ = maybeProjectRoot match { + _ = optProjectRoot match { case Some(path) if buildTools.isBloop(path) => bloopServers.shutdownServer() clearBloopDir(path) diff --git a/metals/src/main/scala/scala/meta/internal/metals/PopupChoiceReset.scala b/metals/src/main/scala/scala/meta/internal/metals/PopupChoiceReset.scala index dc8b63b7398..a4544eb2302 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/PopupChoiceReset.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/PopupChoiceReset.scala @@ -28,7 +28,6 @@ class PopupChoiceReset( val result = if (value == BuildTool) { scribe.info("Resetting build tool selection.") tables.buildTool.reset() - tables.buildServers.reset() slowConnect().ignoreValue } else if (value == BuildImport) { tables.dismissedNotifications.ImportChanges.reset() 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 2c2bcf854d4..592ffe4396f 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 @@ -425,6 +425,7 @@ object ScalaCli { def scalaCliBspJsonContent( args: List[String] = Nil, projectRoot: String = ".", + bspName: String = "scala-cli", ): String = { val argv = List( ScalaCli.javaCommand, @@ -435,7 +436,7 @@ object ScalaCli { projectRoot, ) ++ args val bsjJson = ujson.Obj( - "name" -> "scala-cli", + "name" -> bspName, "argv" -> argv, "version" -> BuildInfo.scalaCliVersion, "bspVersion" -> scalaCliBspVersion, diff --git a/tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala b/tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala index 3dfa69f7d8d..a24339b97c7 100644 --- a/tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala +++ b/tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala @@ -3,6 +3,7 @@ package tests import scala.meta.internal.builds.MillBuildTool import scala.meta.internal.builds.SbtBuildTool import scala.meta.internal.metals.Messages.ChooseBuildTool +import scala.meta.internal.metals.scalacli.ScalaCli import scala.meta.internal.metals.{BuildInfo => V} import scala.meta.io.AbsolutePath @@ -65,4 +66,24 @@ class MultipleBuildFilesLspSuite } } + test("custom-bsp") { + cleanWorkspace() + client.chooseBuildTool = actions => + actions + .find(_.getTitle == "custom") + .getOrElse(throw new Exception("no custom as build tool")) + for { + _ <- initialize( + s"""|/.bsp/custom.json + |${ScalaCli.scalaCliBspJsonContent(bspName = "custom")} + |/build.sbt + |scalaVersion := "${V.scala213}" + |""".stripMargin + ) + _ <- server.server.indexingPromise.future + _ = assert(server.server.bspSession.nonEmpty) + _ = assert(server.server.bspSession.get.main.name == "custom") + } yield () + } + } diff --git a/tests/unit/src/main/scala/tests/TestingClient.scala b/tests/unit/src/main/scala/tests/TestingClient.scala index a8a2d8ebf77..9792c7821cf 100644 --- a/tests/unit/src/main/scala/tests/TestingClient.scala +++ b/tests/unit/src/main/scala/tests/TestingClient.scala @@ -14,6 +14,7 @@ import scala.concurrent.Promise import scala.meta.inputs.Input import scala.meta.internal.bsp.ConnectionBspStatus import scala.meta.internal.builds.BuildTool +import scala.meta.internal.builds.BspErrorHandler import scala.meta.internal.builds.BuildTools import scala.meta.internal.decorations.PublishDecorationsParams import scala.meta.internal.metals.Buffers @@ -306,17 +307,6 @@ class TestingClient(workspace: AbsolutePath, val buffers: Buffers) // NOTE: (ckipp01) Just for easiness of testing, we are going to just look // for sbt and mill builds together, which are most common. The logic however // is identical for all build tools. - def isSameMessageFromList( - createParams: List[BuildTool] => ShowMessageRequestParams - ): Boolean = { - val buildTools = BuildTools - .default() - .allAvailable - .filter(bt => bt.executableName == "sbt" || bt.executableName == "mill") - - val targetParams = createParams(buildTools) - params == targetParams - } def isNewBuildToolDetectedMessage(): Boolean = { val buildTools = BuildTools.default().allAvailable @@ -347,7 +337,7 @@ class TestingClient(workspace: AbsolutePath, val buffers: Buffers) getDoctorInformation } else if (BspSwitch.isSelectBspServer(params)) { selectBspServer(params.getActions.asScala.toSeq) - } else if (isSameMessageFromList(ChooseBuildTool.params)) { + } else if (params.getMessage == ChooseBuildTool.message) { chooseBuildTool(params.getActions.asScala.toSeq) } else if (MissingScalafmtConf.isCreateScalafmtConf(params)) { createScalaFmtConf diff --git a/tests/unit/src/test/scala/tests/BillLspSuite.scala b/tests/unit/src/test/scala/tests/BillLspSuite.scala index 73d6611f099..35c26fe2b87 100644 --- a/tests/unit/src/test/scala/tests/BillLspSuite.scala +++ b/tests/unit/src/test/scala/tests/BillLspSuite.scala @@ -187,7 +187,9 @@ class BillLspSuite extends BaseLspSuite("bill") { testRoundtripCompilation() } - def testSelectServerDialogue(): Future[Unit] = { + def testSelectServerDialogue( + additionalMessages: List[String] = Nil + ): Future[Unit] = { // when asked, choose the Bob build tool client.selectBspServer = { actions => actions.find(_.getTitle == "Bob").get @@ -201,10 +203,11 @@ class BillLspSuite extends BaseLspSuite("bill") { ) _ = assertNoDiff( client.workspaceMessageRequests, - List( - Messages.BspSwitch.message, - Messages.CheckDoctor.allProjectsMisconfigured, - ).mkString("\n"), + (additionalMessages ++ + List( + Messages.BspSwitch.message, + Messages.CheckDoctor.allProjectsMisconfigured, + )).mkString("\n"), ) } yield () } @@ -213,7 +216,7 @@ class BillLspSuite extends BaseLspSuite("bill") { cleanWorkspace() Bill.installWorkspace(workspace.toNIO, "Bill") Bill.installWorkspace(workspace.toNIO, "Bob") - testSelectServerDialogue() + testSelectServerDialogue(List(Messages.ChooseBuildTool.message)) } test("mix") {