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 dc0f92c42f0..7061fe8d752 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BloopInstall.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BloopInstall.scala @@ -7,7 +7,6 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.meta.internal.builds.Digest.Status -import scala.meta.internal.metals.AutoImportBuildKind import scala.meta.internal.metals.BuildInfo import scala.meta.internal.metals.Confirmation import scala.meta.internal.metals.Messages._ @@ -133,9 +132,7 @@ final class BloopInstall( scribe.info(s"skipping build import with status '${result.name}'") Future.successful(result) case _ => - if ( - userConfig().automaticImportBuild == AutoImportBuildKind.Initial || userConfig().automaticImportBuild == AutoImportBuildKind.All - ) { + if (userConfig().shouldAutoImportNewProject) { runUnconditionally(buildTool, isImportInProcess) } else { scribe.debug("Awaiting user response...") 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 13bcc38bac6..83720a511f9 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala @@ -34,6 +34,12 @@ trait BuildTool { */ def buildServerName = executableName + def possibleBuildServerNames: List[String] = List(buildServerName) + + def isBspGenerated(workspace: AbsolutePath): Boolean = + possibleBuildServerNames + .map(name => workspace.resolve(".bsp").resolve(s"$name.json")) + .exists(_.isFile) } object BuildTool { 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 5fbd6013164..8604062d545 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala @@ -61,13 +61,13 @@ class ScalaCliBuildTool( override val forcesBuildServer = true - def isBspGenerated(workspace: AbsolutePath): Boolean = - ScalaCliBuildTool.pathsToScalaCliBsp(workspace).exists(_.isFile) + override def possibleBuildServerNames = ScalaCli.names.toList } object ScalaCliBuildTool { def name = "scala-cli" + def pathsToScalaCliBsp(root: AbsolutePath): List[AbsolutePath] = ScalaCli.names.toList.map(name => root.resolve(".bsp").resolve(s"$name.json") 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 6dca10da430..4514dffffa0 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Messages.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Messages.scala @@ -189,6 +189,31 @@ object Messages { } } + object GenerateBspAndConnect { + def yes = new MessageActionItem("Connect") + + def notNow: MessageActionItem = Messages.notNow + + def params( + buildToolName: String, + buildServerName: String, + ): ShowMessageRequestParams = { + val params = new ShowMessageRequestParams() + params.setMessage( + s"New $buildToolName workspace detected, would you like connect to the $buildServerName build server?" + ) + params.setType(MessageType.Info) + params.setActions( + List( + yes, + notNow, + dontShowAgain, + ).asJava + ) + params + } + } + object MainClass { val message = "Multiple main classes found. Which would you like to run?" } 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 3cdc8853d73..35e9cd6c360 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -2091,23 +2091,17 @@ class MetalsLspService( buildTool: Option[BuildTool.Found], chosenBuildServer: Option[String], ): Future[BuildChange] = { - val isBloopOrEmpty = chosenBuildServer.isEmpty || chosenBuildServer.exists( + def isBloopOrEmpty = chosenBuildServer.exists( _ == BloopServers.name - ) + ) || chosenBuildServer.isEmpty + + def useBuildToolBsp(buildTool: BuildServerProvider) = + buildTool match { + case _: BloopInstallProvider => userConfig.defaultBspToBuildTool + case _ => true + } + 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.buildServerName) - buildTool - .generateBspConfig( - folder, - args => bspConfigGenerator.runUnconditionally(buildTool, args), - statusBar, - ) - .flatMap(_ => quickConnectToBuildServer()) // If there is no .bazelbsp present, we ask user to write bsp config // After that, we should fall into the last case and index workspace case Some(BuildTool.Found(_: BazelBuildTool, _)) @@ -2122,12 +2116,22 @@ class MetalsLspService( forceImport, ) .flatMap(_ => quickConnectToBuildServer()) + case Some(BuildTool.Found(buildTool: BuildServerProvider, _)) + if chosenBuildServer.isEmpty && useBuildToolBsp(buildTool) => + slowConnectToBuildToolBsp(buildTool) + case Some(BuildTool.Found(buildTool: BloopInstallProvider, digest)) + if isBloopOrEmpty => + slowConnectToBloopServer(forceImport, buildTool, digest) case Some(BuildTool.Found(buildTool, _)) if !chosenBuildServer.exists( _ == buildTool.buildServerName ) && buildTool.forcesBuildServer => tables.buildServers.chooseServer(buildTool.buildServerName) quickConnectToBuildServer() + case Some(BuildTool.Found(buildTool: BuildServerProvider, _)) + if chosenBuildServer.contains(buildTool.buildServerName) && !buildTool + .isBspGenerated(folder) => + generateBspAndConnect(buildTool) case Some(found) => indexer.reloadWorkspaceAndIndex( forceImport, @@ -2141,6 +2145,51 @@ class MetalsLspService( } } + private def slowConnectToBuildToolBsp( + buildTool: BuildServerProvider + ) = { + val notification = tables.dismissedNotifications.ImportChanges + if (buildTool.isBspGenerated(folder)) { + tables.buildServers.chooseServer(buildTool.buildServerName) + quickConnectToBuildServer() + } else if ( + userConfig.shouldAutoImportNewProject || buildTool.forcesBuildServer + ) { + generateBspAndConnect(buildTool) + } else if (notification.isDismissed) { + Future.successful(BuildChange.None) + } else { + scribe.debug("Awaiting user response...") + languageClient + .showMessageRequest( + Messages.GenerateBspAndConnect + .params(buildTool.executableName, buildTool.buildServerName) + ) + .asScala + .flatMap { item => + if (item == Messages.dontShowAgain) { + notification.dismissForever() + Future.successful(BuildChange.None) + } else if (item == Messages.GenerateBspAndConnect.yes) { + generateBspAndConnect(buildTool) + } else Future.successful(BuildChange.None) + } + } + } + + private def generateBspAndConnect( + buildTool: BuildServerProvider + ): Future[BuildChange] = { + tables.buildServers.chooseServer(buildTool.buildServerName) + buildTool + .generateBspConfig( + folder, + args => bspConfigGenerator.runUnconditionally(buildTool, args), + statusBar, + ) + .flatMap(_ => quickConnectToBuildServer()) + } + /** * If there is no auto-connectable build server and no supported build tool is found * we assume it's a scala-cli project. 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 c5ed01feb87..62ba6667c71 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala @@ -56,8 +56,12 @@ case class UserConfiguration( verboseCompilation: Boolean = false, automaticImportBuild: AutoImportBuildKind = AutoImportBuildKind.Off, scalaCliLauncher: Option[String] = None, + defaultBspToBuildTool: Boolean = false, ) { + def shouldAutoImportNewProject: Boolean = + automaticImportBuild != AutoImportBuildKind.Off + def currentBloopVersion: String = bloopVersion.getOrElse(BuildInfo.bloopVersion) @@ -349,6 +353,15 @@ object UserConfiguration { |only automatically import a build when a project is first opened, "all" will automate |build imports after subsequent changes as well.""".stripMargin, ), + UserConfigurationOption( + "default-bsp-to-build-tool", + "false", + "true", + "Default to using build tool as your build server.", + """|If your build tool can also serve as a build server, + |default to using it instead of Bloop. + |""".stripMargin, + ), ) def fromJson( @@ -570,6 +583,11 @@ object UserConfiguration { case _ => AutoImportBuildKind.Off } + val scalaCliLauncher = getStringKey("scala-cli-launcher") + + val defaultBspToBuildTool = + getBooleanKey("default-bsp-to-build-tool").getOrElse(false) + if (errors.isEmpty) { Right( UserConfiguration( @@ -602,6 +620,8 @@ object UserConfiguration { customProjectRoot, verboseCompilation, autoImportBuilds, + scalaCliLauncher, + defaultBspToBuildTool, ) ) } else { diff --git a/tests/slow/src/test/scala/tests/PreferredBuildServer.scala b/tests/slow/src/test/scala/tests/PreferredBuildServer.scala new file mode 100644 index 00000000000..f95ca2a10e7 --- /dev/null +++ b/tests/slow/src/test/scala/tests/PreferredBuildServer.scala @@ -0,0 +1,80 @@ +package tests + +import scala.meta.internal.metals.Messages +import scala.meta.internal.metals.UserConfiguration +import scala.meta.internal.metals.{BuildInfo => V} + +class PreferredBuildServer extends BaseLspSuite("preferred-build-server") { + override def userConfig: UserConfiguration = + super.userConfig.copy(defaultBspToBuildTool = true) + + test("start-sbt-when-preferred-no-bsp") { + cleanWorkspace() + + val importMessage = + Messages.GenerateBspAndConnect.params("sbt", "sbt").getMessage() + + client.showMessageRequestHandler = msg => { + if (msg.getMessage() == importMessage) + Some(Messages.GenerateBspAndConnect.notNow) + else None + } + + val fileLayout = + s"""|/project/build.properties + |sbt.version=${V.sbtVersion} + |/build.sbt + |${SbtBuildLayout.commonSbtSettings} + |ThisBuild / scalaVersion := "${V.scala213}" + |val a = project.in(file("a")) + |/a/src/main/scala/a/A.scala + |package a + |object A { + | val a = 1 + |} + |""".stripMargin + FileLayout.fromString(fileLayout, workspace) + + for { + _ <- server.initialize() + _ <- server.initialized() + _ = assertNoDiff( + client.workspaceMessageRequests, + importMessage, + ) + _ = assert(server.server.bspSession.isEmpty) + } yield () + } + + test("start-sbt-when-preferred-with-bsp") { + cleanWorkspace() + + val fileLayout = + s"""|/project/build.properties + |sbt.version=${V.sbtVersion} + |/build.sbt + |${SbtBuildLayout.commonSbtSettings} + |ThisBuild / scalaVersion := "${V.scala213}" + |val a = project.in(file("a")) + |/a/src/main/scala/a/A.scala + |package a + |object A { + | val a = 1 + |} + |""".stripMargin + + FileLayout.fromString(fileLayout, workspace) + SbtServerInitializer.generateBspConfig(workspace, V.sbtVersion) + + for { + _ <- server.initialize() + _ <- server.initialized() + _ <- server.server.buildServerPromise.future + _ = assertNoDiff( + server.server.tables.buildServers.selectedServer().get, + "sbt", + ) + _ = assert(server.server.bspSession.exists(_.main.isSbt)) + } yield () + } +} diff --git a/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala b/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala index ea776826057..3d44ae7d828 100644 --- a/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala +++ b/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala @@ -312,6 +312,7 @@ class ScalaCliSuite extends BaseScalaCliSuite(V.scala3) { } test("inner") { + cleanWorkspace() for { _ <- scalaCliInitialize(useBsp = false)( s"""|/inner/project.scala diff --git a/tests/unit/src/main/scala/tests/BuildServerInitializer.scala b/tests/unit/src/main/scala/tests/BuildServerInitializer.scala index c41a036620d..b521fafcecf 100644 --- a/tests/unit/src/main/scala/tests/BuildServerInitializer.scala +++ b/tests/unit/src/main/scala/tests/BuildServerInitializer.scala @@ -133,7 +133,7 @@ object SbtServerInitializer extends BuildServerInitializer { } } - private def generateBspConfig( + def generateBspConfig( workspace: AbsolutePath, sbtVersion: String, ): Unit = {