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 be0a783f211..b4f98558b78 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspConfigGenerator.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspConfigGenerator.scala @@ -67,24 +67,7 @@ final class BspConfigGenerator( case status => status } - /** - * Given multiple build tools that are all BuildServerProviders, allow the - * choose the desired build server and then connect to it. - */ - def chooseAndGenerate( - buildTools: List[BuildServerProvider] - ): Future[(BuildServerProvider, BspConfigGenerationStatus)] = { - for { - Some(buildTool) <- chooseBuildServerProvider(buildTools) - status <- buildTool.generateBspConfig( - workspace, - args => runUnconditionally(buildTool, args), - statusBar, - ) - } yield (buildTool, status) - } - - private def chooseBuildServerProvider( + def chooseBuildServerProvider( buildTools: List[BuildServerProvider] ): Future[Option[BuildServerProvider]] = { languageClient 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 fe340b9b520..bdd3c70dbf5 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala @@ -5,7 +5,6 @@ import java.nio.file.Files import scala.concurrent.ExecutionContext import scala.concurrent.Future -import scala.meta.internal.bsp.BspConfigGenerationStatus._ import scala.meta.internal.builds.BuildServerProvider import scala.meta.internal.builds.BuildTool import scala.meta.internal.builds.BuildTools @@ -14,9 +13,13 @@ import scala.meta.internal.builds.ScalaCliBuildTool import scala.meta.internal.builds.ShellRunner import scala.meta.internal.metals.BloopServers import scala.meta.internal.metals.BuildServerConnection +import scala.meta.internal.metals.ConnectKind +import scala.meta.internal.metals.CreateSession +import scala.meta.internal.metals.GenerateBspConfigAndConnect import scala.meta.internal.metals.Messages import scala.meta.internal.metals.Messages.BspSwitch import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.SlowConnect import scala.meta.internal.metals.StatusBar import scala.meta.internal.metals.Tables import scala.meta.internal.metals.UserConfiguration @@ -40,7 +43,7 @@ class BspConnector( workDoneProgress: WorkDoneProgress, bspConfigGenerator: BspConfigGenerator, currentConnection: () => Option[BuildServerConnection], - restartBspServer: () => Future[Boolean], + restartBspServer: () => Future[Unit], bspStatus: ConnectionBspStatus, )(implicit ec: ExecutionContext) { @@ -111,7 +114,7 @@ class BspConnector( val shouldReload = SbtBuildTool.writeSbtMetalsPlugins(projectRoot) def restartSbtBuildServer() = currentConnection() .withFilter(_.isSbt) - .map(_ => restartBspServer().ignoreValue) + .map(_ => restartBspServer()) .getOrElse(Future.successful(())) val connectionF = for { @@ -152,7 +155,6 @@ class BspConnector( args => bspConfigGenerator.runUnconditionally(bsp, args), statusBar, ) - .map(status => handleGenerationStatus(bsp, status)) .flatMap { _ => connect( projectRoot, @@ -161,7 +163,6 @@ class BspConnector( regeneratedConfig = true, ) } - case _ => bspServers .newServer(projectRoot, bspTraceRoot, details, bspStatusOpt) @@ -299,10 +300,7 @@ class BspConnector( * 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( - workspace: AbsolutePath, - createBloopAndConnect: () => Future[BuildChange], - ): Future[Boolean] = { + def switchBuildServer[T](): Future[Option[ConnectKind]] = { val foundServers = bspServers.findAvailableServers() val bloopPresent: Boolean = buildTools.isBloop @@ -356,93 +354,43 @@ class BspConnector( possibleChoice match { case Some(choice) => allPossibleServers(choice) match { - case Left(buildTool) => - buildTool - .generateBspConfig( - workspace, - args => - bspConfigGenerator.runUnconditionally(buildTool, args), - statusBar, - ) - .map(status => handleGenerationStatus(buildTool, status)) + case Left(buildTool) => Some(GenerateBspConfigAndConnect(buildTool)) case Right(details) if details.getName == BloopServers.name => tables.buildServers.chooseServer(details.getName) - if (bloopPresent) { - Future.successful(true) - } else { - createBloopAndConnect().ignoreValue - Future.successful(false) - } + if (bloopPresent) Some(CreateSession()) + else Some(SlowConnect) case Right(details) if !currentSelectedServer.contains(details.getName) => tables.buildServers.chooseServer(details.getName) - Future.successful(true) - case _ => Future.successful(false) + Some(CreateSession()) + case _ => None } - case _ => - Future.successful(false) + case _ => None } } allPossibleServers.keys.toList match { case Nil => client.showMessage(BspSwitch.noInstalledServer) - Future.successful(false) + Future.successful(None) case singleServer :: Nil => allPossibleServers(singleServer) match { case Left(buildTool) => - buildTool - .generateBspConfig( - workspace, - args => bspConfigGenerator.runUnconditionally(buildTool, args), - statusBar, - ) - .map(status => handleGenerationStatus(buildTool, status)) + Future.successful(Some(GenerateBspConfigAndConnect(buildTool))) case Right(connectionDetails) => client.showMessage( BspSwitch.onlyOneServer(name = connectionDetails.getName()) ) - Future.successful(false) + Future.successful(None) } case multipleServers => val currentSelectedServer = tables.buildServers .selectedServer() .orElse(currentConnection().map(_.name)) - askUser(multipleServers, currentSelectedServer).flatMap(choice => + askUser(multipleServers, currentSelectedServer).map(choice => handleServerChoice(choice, currentSelectedServer) ) } } - - /** - * Handles showing the user what they need to know after an attempt to - * generate a bsp config has happened. - */ - private def handleGenerationStatus( - buildTool: BuildServerProvider, - status: BspConfigGenerationStatus, - ): Boolean = status match { - case BspConfigGenerationStatus.Generated => - tables.buildServers.chooseServer(buildTool.buildServerName) - true - case Cancelled => false - case Failed(exit) => - exit match { - case Left(exitCode) => - scribe.error( - s"Creation of .bsp/${buildTool.buildServerName} failed with exit code: $exitCode" - ) - client.showMessage( - Messages.BspProvider.genericUnableToCreateConfig - ) - case Right(message) => - client.showMessage( - Messages.BspProvider.unableToCreateConfigFromMessage( - message - ) - ) - } - false - } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala index d5e670811a6..9226f663cff 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala @@ -524,6 +524,12 @@ final class BuildTargets private ( ): Option[Iterable[BuildTargetIdentifier]] = data.fromOptions(_.sourceBuildTargets(sourceItem)) + def belongsToBuildTarget(nioDir: Path): Boolean = + sourceItems.filter(_.exists).exists { item => + val nioItem = item.toNIO + nioDir.startsWith(nioItem) || nioItem.startsWith(nioDir) + } + def inverseSourceItem(source: AbsolutePath): Option[AbsolutePath] = sourceItems.find(item => source.toNIO.startsWith(item.toNIO)) diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildToolProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildToolProvider.scala new file mode 100644 index 00000000000..c51097a4eff --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildToolProvider.scala @@ -0,0 +1,102 @@ +package scala.meta.internal.metals + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import scala.meta.internal.builds.BuildTool +import scala.meta.internal.builds.BuildToolSelector +import scala.meta.internal.builds.BuildTools +import scala.meta.internal.builds.VersionRecommendation +import scala.meta.internal.metals.clients.language.MetalsLanguageClient +import scala.meta.internal.semver.SemVer +import scala.meta.io.AbsolutePath + +class BuildToolProvider( + buildTools: BuildTools, + tables: Tables, + folder: AbsolutePath, + warnings: ProjectWarnings, + languageClient: MetalsLanguageClient, +)(implicit ec: ExecutionContext) { + private val buildToolSelector: BuildToolSelector = new BuildToolSelector( + languageClient, + tables, + ) + + def buildTool: Option[BuildTool] = + for { + name <- tables.buildTool.selectedBuildTool() + buildTool <- buildTools.current().find(_.executableName == name) + if isCompatibleVersion(buildTool) + } yield buildTool + + def optProjectRoot: Option[AbsolutePath] = + buildTool.map(_.projectRoot).orElse(buildTools.bloopProject) + + private def isCompatibleVersion(buildTool: BuildTool): Boolean = { + buildTool match { + case buildTool: VersionRecommendation => + SemVer.isCompatibleVersion( + buildTool.minimumVersion, + buildTool.version, + ) + case _ => true + } + } + + /** + * Checks if the version of the build tool is compatible with the version that + * metals expects and returns the current digest of the build tool. + */ + private def verifyBuildTool(buildTool: BuildTool): BuildTool.Verified = { + buildTool match { + case buildTool: VersionRecommendation + if !isCompatibleVersion(buildTool) => + BuildTool.IncompatibleVersion(buildTool) + case _ => + buildTool.digestWithRetry(folder) match { + case Some(digest) => + BuildTool.Found(buildTool, digest) + case None => BuildTool.NoChecksum(buildTool, folder) + } + } + } + + def supportedBuildTool(): Future[Option[BuildTool.Found]] = { + buildTools.loadSupported() match { + case Nil => { + if (!buildTools.isAutoConnectable()) { + warnings.noBuildTool() + } + Future(None) + } + case buildTools => + for { + buildTool <- buildToolSelector.checkForChosenBuildTool( + buildTools + ) + } yield { + buildTool.flatMap { bt => + verifyBuildTool(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 onNewBuildToolAdded( + newBuildTool: BuildTool, + currentBuildTool: BuildTool, + ): Future[Boolean] = + buildToolSelector.onNewBuildToolAdded(newBuildTool, currentBuildTool) +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/ConnectionProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/ConnectionProvider.scala new file mode 100644 index 00000000000..91debfe58dd --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/ConnectionProvider.scala @@ -0,0 +1,650 @@ +package scala.meta.internal.metals + +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +import scala.concurrent.ExecutionContextExecutorService +import scala.concurrent.Future +import scala.concurrent.Promise +import scala.util.control.NonFatal + +import scala.meta.internal.bsp +import scala.meta.internal.bsp.BspConfigGenerationStatus.BspConfigGenerationStatus +import scala.meta.internal.bsp.BspConnector +import scala.meta.internal.bsp.BspSession +import scala.meta.internal.bsp.BuildChange +import scala.meta.internal.builds.BloopInstall +import scala.meta.internal.builds.BloopInstallProvider +import scala.meta.internal.builds.BspOnly +import scala.meta.internal.builds.BuildServerProvider +import scala.meta.internal.builds.BuildTool +import scala.meta.internal.builds.BuildTools +import scala.meta.internal.builds.Digest +import scala.meta.internal.builds.Digest.Status +import scala.meta.internal.builds.SbtBuildTool +import scala.meta.internal.builds.ScalaCliBuildTool +import scala.meta.internal.builds.ShellRunner +import scala.meta.internal.metals.Messages.IncompatibleBloopVersion +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.doctor.Doctor +import scala.meta.internal.metals.scalacli.ScalaCliServers +import scala.meta.io.AbsolutePath + +import org.eclipse.lsp4j.MessageParams +import org.eclipse.lsp4j.MessageType + +class ConnectionProvider( + buildToolProvider: BuildToolProvider, + compilations: Compilations, + buildTools: BuildTools, + buffers: Buffers, + compilers: Compilers, + scalaCli: ScalaCliServers, + bloopServers: BloopServers, + shellRunner: ShellRunner, + bspConfigGenerator: bsp.BspConfigGenerator, + check: () => Unit, + doctor: Doctor, + initTreeView: () => Unit, + diagnostics: Diagnostics, + charset: Charset, + buildClient: MetalsBuildClient, + bspGlobalDirectories: List[AbsolutePath], + bspStatus: bsp.ConnectionBspStatus, + mainBuildTargetsData: TargetData, + indexProviders: IndexProviders, +)(implicit ec: ExecutionContextExecutorService, rc: ReportContext) + extends Indexer(indexProviders) + with Cancelable { + import Connect.connect + import indexProviders._ + + def resolveBsp(): bsp.BspResolvedResult = + bspConnector.resolve(buildToolProvider.buildTool) + + protected val bspServers: bsp.BspServers = new bsp.BspServers( + folder, + charset, + languageClient, + buildClient, + tables, + bspGlobalDirectories, + clientConfig.initialConfig, + () => userConfig, + workDoneProgress, + ) + + val bspConnector: BspConnector = new BspConnector( + bloopServers, + bspServers, + buildTools, + languageClient, + tables, + () => userConfig, + statusBar, + workDoneProgress, + bspConfigGenerator, + () => bspSession.map(_.mainConnection), + () => connect(new CreateSession(true)).ignoreValue, + bspStatus, + ) + + private val bloopInstall: BloopInstall = new BloopInstall( + folder, + languageClient, + buildTools, + tables, + shellRunner, + () => userConfig, + ) + + val cancelables = new MutableCancelable + var buildServerPromise: Promise[Unit] = Promise[Unit]() + val isConnecting = new AtomicBoolean(false) + + override def index(check: () => Unit): Future[Unit] = connect( + Index(check) + ).ignoreValue + override def cancel(): Unit = { + cancelables.cancel() + } + + def fullConnect(): Future[Unit] = { + buildTools.initialize() + for { + _ <- + if (buildTools.isAutoConnectable(buildToolProvider.optProjectRoot)) + connect(CreateSession()) + else slowConnectToBuildServer(forceImport = false) + } yield buildServerPromise.trySuccess(()) + } + + def slowConnectToBuildServer( + forceImport: Boolean + ): Future[BuildChange] = { + val chosenBuildServer = tables.buildServers.selectedServer() + def useBuildToolBsp(buildTool: BloopInstallProvider) = + buildTool match { + case _: BuildServerProvider => userConfig.defaultBspToBuildTool + case _ => false + } + + def isSelected(buildTool: BuildTool) = + buildTool match { + case _: BuildServerProvider => + chosenBuildServer.contains(buildTool.buildServerName) + case _ => false + } + + buildToolProvider.supportedBuildTool().flatMap { + case Some(BuildTool.Found(buildTool: BloopInstallProvider, digest)) + if chosenBuildServer.contains(BloopServers.name) || + chosenBuildServer.isEmpty && !useBuildToolBsp(buildTool) => + connect( + new BloopInstallAndConnect( + buildTool, + digest, + forceImport, + shutdownServer = false, + ) + ) + case Some(found) + if isSelected(found.buildTool) && + found.buildTool.isBspGenerated(folder) => + reloadWorkspaceAndIndex( + forceImport, + found.buildTool, + found.digest, + ) + case Some(BuildTool.Found(buildTool: BuildServerProvider, _)) => + slowConnectToBuildToolBsp(buildTool, forceImport, isSelected(buildTool)) + // Used when there are multiple `.bsp/.json` configs and a known build tool (e.g. sbt) + case Some(BuildTool.Found(buildTool, _)) + if buildTool.isBspGenerated(folder) => + maybeChooseServer(buildTool.buildServerName, isSelected(buildTool)) + connect(CreateSession()) + // Used in tests, `.bloop` folder exists but no build tool is detected + case _ => quickConnectToBuildServer() + } + } + + protected def slowConnectToBuildToolBsp( + buildTool: BuildServerProvider, + forceImport: Boolean, + isSelected: Boolean, + ): Future[BuildChange] = { + val notification = tables.dismissedNotifications.ImportChanges + if (buildTool.isBspGenerated(folder)) { + maybeChooseServer(buildTool.buildServerName, isSelected) + connect(CreateSession()) + } else if ( + userConfig.shouldAutoImportNewProject || forceImport || isSelected || + buildTool.isInstanceOf[ScalaCliBuildTool] + ) { + connect(GenerateBspConfigAndConnect(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) { + connect(GenerateBspConfigAndConnect(buildTool)) + } else { + notification.dismiss(2, TimeUnit.MINUTES) + Future.successful(BuildChange.None) + } + } + } + } + + def quickConnectToBuildServer(): Future[BuildChange] = + for { + change <- + if (!buildTools.isAutoConnectable(buildToolProvider.optProjectRoot)) { + scribe.warn("Build server is not auto-connectable.") + Future.successful(BuildChange.None) + } else { + connect(CreateSession()) + } + } yield { + buildServerPromise.trySuccess(()) + change + } + + private def maybeChooseServer(name: String, alreadySelected: Boolean): Any = + if (alreadySelected) Future.successful(()) + else tables.buildServers.chooseServer(name) + + private def reloadWorkspaceAndIndex( + forceRefresh: Boolean, + buildTool: BuildTool, + checksum: String, + ): Future[BuildChange] = { + def reloadAndIndex(session: BspSession): Future[BuildChange] = { + workspaceReload.persistChecksumStatus(Status.Started, buildTool) + + buildTool.ensurePrerequisites(folder) + buildTool match { + case _: BspOnly => + connect(CreateSession()) + case _ if !session.canReloadWorkspace => + connect(CreateSession()) + case _ => + session.workspaceReload + .flatMap(_ => connect(new ImportBuildAndIndex(session))) + .map { _ => + scribe.info("Correctly reloaded workspace") + workspaceReload.persistChecksumStatus( + Status.Installed, + buildTool, + ) + BuildChange.Reloaded + } + .recoverWith { case NonFatal(e) => + scribe.error(s"Unable to reload workspace: ${e.getMessage()}") + workspaceReload.persistChecksumStatus(Status.Failed, buildTool) + languageClient.showMessage(Messages.ReloadProjectFailed) + Future.successful(BuildChange.Failed) + } + } + } + + bspSession match { + case None => + scribe.warn( + "No build session currently active to reload. Attempting to reconnect." + ) + quickConnectToBuildServer() + case Some(session) if forceRefresh => reloadAndIndex(session) + case Some(session) => + workspaceReload.oldReloadResult(checksum) match { + case Some(status) => + scribe.info(s"Skipping reload with status '${status.name}'") + Future.successful(BuildChange.None) + case None => + if (userConfig.automaticImportBuild == AutoImportBuildKind.All) { + reloadAndIndex(session) + } else { + for { + userResponse <- workspaceReload.requestReload( + buildTool, + checksum, + ) + installResult <- { + if (userResponse.isYes) { + reloadAndIndex(session) + } else { + tables.dismissedNotifications.ImportChanges + .dismiss(2, TimeUnit.MINUTES) + Future.successful(BuildChange.None) + } + } + } yield installResult + } + } + } + } + + object Connect { + def connect[T](request: ConnectRequest): Future[BuildChange] = { + request match { + case Disconnect(shutdownBuildServer) => disconnect(shutdownBuildServer) + case Index(check) => index(check) + case ImportBuildAndIndex(session) => importBuildAndIndex(session) + case ConnectToSession(session) => connectToSession(session) + case CreateSession(shutdownBuildServer) => + createSession(shutdownBuildServer) + case GenerateBspConfigAndConnect(buildTool, shutdownServer) => + generateBspConfigAndConnect(buildTool, shutdownServer) + case BloopInstallAndConnect( + buildTool, + checksum, + forceImport, + shutdownServer, + ) => + bloopInstallAndConnect( + buildTool, + checksum, + forceImport, + shutdownServer, + ) + } + } + + private def disconnect( + shutdownBuildServer: Boolean + ): Future[BuildChange] = { + def shutdownBsp(optMainBsp: Option[String]): Future[Boolean] = { + optMainBsp match { + case Some(BloopServers.name) => + Future { bloopServers.shutdownServer() } + case Some(SbtBuildTool.name) => + for { + res <- buildToolProvider.buildTool match { + case Some(sbt: SbtBuildTool) => + sbt.shutdownBspServer(shellRunner).map(_ == 0) + case _ => Future.successful(false) + } + } yield res + case s => Future.successful(s.nonEmpty) + } + } + + compilations.cancel() + buildTargetClasses.cancel() + diagnostics.reset() + bspSession.foreach(connection => + scribe.info(s"Disconnecting from ${connection.main.name} session...") + ) + + for { + _ <- scalaCli.stop() + optMainBsp <- bspSession match { + case None => Future.successful(None) + case Some(session) => + bspSession = None + mainBuildTargetsData.resetConnections(List.empty) + session.shutdown().map(_ => Some(session.main.name)) + } + _ <- + if (shutdownBuildServer) shutdownBsp(optMainBsp) + else Future.successful(()) + } yield BuildChange.None + } + + private def index(check: () => Unit): Future[BuildChange] = + profiledIndexWorkspace(check).map(_ => BuildChange.None) + + private def importBuildAndIndex( + session: BspSession + ): Future[BuildChange] = { + val importedBuilds0 = timerProvider.timed("Imported build") { + session.importBuilds() + } + for { + bspBuilds <- workDoneProgress.trackFuture( + Messages.importingBuild, + importedBuilds0, + ) + _ = { + val idToConnection = bspBuilds.flatMap { bspBuild => + val targets = + bspBuild.build.workspaceBuildTargets.getTargets().asScala + targets.map(t => (t.getId(), bspBuild.connection)) + } + mainBuildTargetsData.resetConnections(idToConnection) + saveProjectReferencesInfo(bspBuilds) + } + _ = compilers.cancel() + buildChange <- index(check) + } yield buildChange + } + + private def saveProjectReferencesInfo( + bspBuilds: List[BspSession.BspBuild] + ): Unit = { + val projectRefs = bspBuilds + .flatMap { session => + session.build.workspaceBuildTargets.getTargets().asScala.flatMap { + _.getBaseDirectory() match { + case null | "" => None + case path => path.toAbsolutePathSafe + } + } + } + .distinct + .filterNot(_.startWith(folder)) + if (projectRefs.nonEmpty) + DelegateSetting.writeProjectRef(folder, projectRefs) + } + + private def connectToSession(session: BspSession): Future[BuildChange] = { + scribe.info( + s"Connected to Build server: ${session.main.name} v${session.version}" + ) + cancelables.add(session) + buildToolProvider.buildTool.foreach( + workspaceReload.persistChecksumStatus(Digest.Status.Started, _) + ) + bspSession = Some(session) + isConnecting.set(false) + for { + _ <- importBuildAndIndex(session) + _ = buildToolProvider.buildTool.foreach( + workspaceReload.persistChecksumStatus(Digest.Status.Installed, _) + ) + _ = if (session.main.isBloop) + checkRunningBloopVersion(session.version) + } yield { + BuildChange.Reconnected + } + } + + private def checkRunningBloopVersion(bspServerVersion: String): Unit = { + if (doctor.isUnsupportedBloopVersion()) { + val notification = tables.dismissedNotifications.IncompatibleBloop + if (!notification.isDismissed) { + val messageParams = IncompatibleBloopVersion.params( + bspServerVersion, + BuildInfo.bloopVersion, + isChangedInSettings = userConfig.bloopVersion != None, + ) + languageClient.showMessageRequest(messageParams).asScala.foreach { + case action if action == IncompatibleBloopVersion.shutdown => + connect(new CreateSession(true)) + case action if action == IncompatibleBloopVersion.dismissForever => + notification.dismissForever() + case _ => + } + } + } + } + + def createSession(shutdownServer: Boolean): Future[BuildChange] = { + def compileAllOpenFiles: BuildChange => Future[BuildChange] = { + case change if !change.isFailed => + Future + .sequence( + compilations + .cascadeCompileFiles(buffers.open.toSeq) + .ignoreValue :: + compilers.load(buffers.open.toSeq) :: + Nil + ) + .map(_ => change) + case other => Future.successful(other) + } + + val scalaCliPaths = scalaCli.paths + + isConnecting.set(true) + (for { + _ <- disconnect(shutdownServer) + maybeSession <- timerProvider.timed( + "Connected to build server", + true, + ) { + bspConnector.connect( + buildToolProvider.buildTool, + folder, + userConfig, + shellRunner, + ) + } + result <- maybeSession match { + case Some(session) => + val result = connectToSession(session) + session.mainConnection.onReconnection { newMainConn => + val updSession = session.copy(main = newMainConn) + connect(ConnectToSession(updSession)) + .flatMap(compileAllOpenFiles) + .ignoreValue + } + result + case None => + Future.successful(BuildChange.None) + } + _ <- Future.sequence( + scalaCliPaths + .collect { + case path if (!buildTargets.belongsToBuildTarget(path.toNIO)) => + scalaCli.start(path) + } + ) + _ = initTreeView() + } yield result) + .recover { case NonFatal(e) => + disconnect(false) + val message = + "Failed to connect with build server, no functionality will work." + val details = " See logs for more details." + languageClient.showMessage( + new MessageParams(MessageType.Error, message + details) + ) + scribe.error(message, e) + BuildChange.Failed + } + .flatMap(compileAllOpenFiles) + .map { res => + buildServerPromise.trySuccess(()) + res + } + } + + private def generateBspConfigAndConnect( + buildTool: BuildServerProvider, + shutdownServer: Boolean, + ): Future[BuildChange] = { + tables.buildTool.chooseBuildTool(buildTool.executableName) + maybeChooseServer(buildTool.buildServerName, alreadySelected = false) + for { + _ <- + if (shutdownServer) disconnect(shutdownServer) + else Future.unit + status <- buildTool + .generateBspConfig( + folder, + args => bspConfigGenerator.runUnconditionally(buildTool, args), + statusBar, + ) + shouldConnect = handleGenerationStatus(buildTool, status) + status <- + if (shouldConnect) createSession(false) + else Future.successful(BuildChange.Failed) + } yield status + } + + /** + * Handles showing the user what they need to know after an attempt to + * generate a bsp config has happened. + */ + private def handleGenerationStatus( + buildTool: BuildServerProvider, + status: BspConfigGenerationStatus, + ): Boolean = status match { + case bsp.BspConfigGenerationStatus.Generated => + tables.buildServers.chooseServer(buildTool.buildServerName) + true + case bsp.BspConfigGenerationStatus.Cancelled => false + case bsp.BspConfigGenerationStatus.Failed(exit) => + exit match { + case Left(exitCode) => + scribe.error( + s"Creation of .bsp/${buildTool.buildServerName} failed with exit code: $exitCode" + ) + languageClient.showMessage( + Messages.BspProvider.genericUnableToCreateConfig + ) + case Right(message) => + languageClient.showMessage( + Messages.BspProvider.unableToCreateConfigFromMessage( + message + ) + ) + } + false + } + + val isImportInProcess = new AtomicBoolean(false) + + private def bloopInstallAndConnect( + buildTool: BloopInstallProvider, + checksum: String, + forceImport: Boolean, + shutdownServer: Boolean, + ): Future[BuildChange] = { + for { + result <- { + if (forceImport) + bloopInstall.runUnconditionally( + buildTool, + isImportInProcess, + ) + else + bloopInstall.runIfApproved( + buildTool, + checksum, + isImportInProcess, + ) + } + change <- { + if (result.isInstalled) createSession(shutdownServer) + else if (result.isFailed) { + for { + change <- + if ( + buildTools.isAutoConnectable( + buildToolProvider.optProjectRoot + ) + ) { + // 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. + createSession(shutdownServer) + } else { + languageClient.showMessage(Messages.ImportProjectFailed) + Future.successful(BuildChange.Failed) + } + } yield change + } else Future.successful(BuildChange.None) + } + } yield change + } + } +} + +sealed trait ConnectKind +object SlowConnect extends ConnectKind + +sealed trait ConnectRequest extends ConnectKind + +case class Disconnect(shutdownBuildServer: Boolean) extends ConnectRequest +case class Index(check: () => Unit) extends ConnectRequest +case class ImportBuildAndIndex(bspSession: BspSession) extends ConnectRequest +case class ConnectToSession(bspSession: BspSession) extends ConnectRequest +case class CreateSession(shutdownBuildServer: Boolean = false) + extends ConnectRequest +case class GenerateBspConfigAndConnect( + buildTool: BuildServerProvider, + shutdownServer: Boolean = false, +) extends ConnectRequest +case class BloopInstallAndConnect( + buildTool: BloopInstallProvider, + checksum: String, + forceImport: Boolean, + shutdownServer: Boolean, +) extends ConnectRequest diff --git a/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala index c2f9a0d698d..b607f255433 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/FallbackMetalsLspService.scala @@ -6,6 +6,7 @@ import java.util.concurrent.atomic.AtomicReference import scala.concurrent.ExecutionContextExecutorService import scala.concurrent.Future +import scala.concurrent.Promise import scala.meta.internal.builds.ShellRunner import scala.meta.internal.metals.Indexer.BuildTool @@ -24,19 +25,19 @@ import org.eclipse.lsp4j.InitializeParams class FallbackMetalsLspService( ec: ExecutionContextExecutorService, - sh: ScheduledExecutorService, + override val sh: ScheduledExecutorService, serverInputs: MetalsServerInputs, - languageClient: ConfiguredLanguageClient, + override val languageClient: ConfiguredLanguageClient, initializeParams: InitializeParams, - clientConfig: ClientConfiguration, - statusBar: StatusBar, + override val clientConfig: ClientConfiguration, + override val statusBar: StatusBar, focusedDocument: () => Option[AbsolutePath], shellRunner: ShellRunner, - timerProvider: TimerProvider, - folder: AbsolutePath, + override val timerProvider: TimerProvider, + override val folder: AbsolutePath, folderVisibleName: Option[String], headDoctor: HeadDoctor, - workDoneProgress: WorkDoneProgress, + override val workDoneProgress: WorkDoneProgress, bspStatus: BspStatus, ) extends MetalsLspService( ec, @@ -57,7 +58,7 @@ class FallbackMetalsLspService( maxScalaCliServers = 10, ) { - buildServerPromise.success(()) + val buildServerPromise: Promise[Unit] = Promise.successful(()) indexingPromise.success(()) private val files: AtomicReference[Set[AbsolutePath]] = new AtomicReference( @@ -73,7 +74,9 @@ class FallbackMetalsLspService( override val projectInfo: MetalsServiceInfo = MetalsServiceInfo.FallbackService - protected def buildData(): Seq[BuildTool] = + override val indexer: Indexer = Indexer(this) + + def buildData(): Seq[BuildTool] = scalaCli.lastImportedBuilds.map { case (lastImportedBuild, buildTargetsData) => Indexer diff --git a/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala b/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala new file mode 100644 index 00000000000..aee813d94cf --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/IndexProviders.scala @@ -0,0 +1,45 @@ +package scala.meta.internal.metals + +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.atomic.AtomicReference + +import scala.concurrent.ExecutionContextExecutorService +import scala.concurrent.Promise + +import scala.meta.internal.implementation.ImplementationProvider +import scala.meta.internal.metals.clients.language.DelegatingLanguageClient +import scala.meta.internal.metals.debug.BuildTargetClasses +import scala.meta.internal.metals.watcher.FileWatcher +import scala.meta.internal.mtags.OnDemandSymbolIndex +import scala.meta.io.AbsolutePath + +import ch.epfl.scala.bsp4j.BuildTargetIdentifier + +trait IndexProviders { + def languageClient: DelegatingLanguageClient + def executionContext: ExecutionContextExecutorService + def tables: Tables + def statusBar: StatusBar + def workDoneProgress: WorkDoneProgress + def timerProvider: TimerProvider + def indexingPromise: Promise[Unit] + def buildData(): Seq[Indexer.BuildTool] + def clientConfig: ClientConfiguration + def definitionIndex: OnDemandSymbolIndex + def referencesProvider: ReferenceProvider + def workspaceSymbols: WorkspaceSymbolProvider + def buildTargets: BuildTargets + def semanticDBIndexer: SemanticdbIndexer + def fileWatcher: FileWatcher + def focusedDocument: Option[AbsolutePath] + def focusedDocumentBuildTarget: AtomicReference[BuildTargetIdentifier] + def buildTargetClasses: BuildTargetClasses + def userConfig: UserConfiguration + def sh: ScheduledExecutorService + def symbolDocs: Docstrings + def scalaVersionSelector: ScalaVersionSelector + def sourceMapper: SourceMapper + def folder: AbsolutePath + def implementationProvider: ImplementationProvider + def resetService(): Unit +} 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 dc7fb063c7e..d9d8dcfa93b 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala @@ -1,9 +1,7 @@ package scala.meta.internal.metals import java.util.concurrent.ForkJoinPool -import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicReference import java.{util => ju} import scala.build.bsp.WrappedSourceItem @@ -13,27 +11,16 @@ import scala.collection.parallel.CollectionConverters._ import scala.collection.parallel.ForkJoinTaskSupport import scala.concurrent.ExecutionContextExecutorService import scala.concurrent.Future -import scala.concurrent.Promise import scala.util.control.NonFatal import scala.meta.Dialect import scala.meta.dialects._ import scala.meta.inputs.Input import scala.meta.internal.bsp.BspSession -import scala.meta.internal.bsp.BuildChange -import scala.meta.internal.builds.BspOnly -import scala.meta.internal.builds.BuildTool -import scala.meta.internal.builds.Digest.Status import scala.meta.internal.builds.WorkspaceReload -import scala.meta.internal.implementation.ImplementationProvider import scala.meta.internal.metals.MetalsEnrichments._ -import scala.meta.internal.metals.clients.language.DelegatingLanguageClient -import scala.meta.internal.metals.debug.BuildTargetClasses -import scala.meta.internal.metals.watcher.FileWatcher import scala.meta.internal.mtags.IndexingResult -import scala.meta.internal.mtags.OnDemandSymbolIndex import scala.meta.internal.semanticdb.Scala._ -import scala.meta.internal.worksheets.WorksheetProvider import scala.meta.io.AbsolutePath import ch.epfl.scala.{bsp4j => b} @@ -46,146 +33,49 @@ import org.eclipse.lsp4j.Position * Coordinates build target data fetching and caching, and the re-computation of various * indexes based on it. */ -final case class Indexer( - workspaceReload: () => WorkspaceReload, - check: () => Unit, - languageClient: DelegatingLanguageClient, - bspSession: () => Option[BspSession], - executionContext: ExecutionContextExecutorService, - tables: Tables, - statusBar: () => StatusBar, - workDoneProgress: WorkDoneProgress, - timerProvider: TimerProvider, - scalafixProvider: () => ScalafixProvider, - indexingPromise: () => Promise[Unit], - buildData: () => Seq[Indexer.BuildTool], - clientConfig: ClientConfiguration, - definitionIndex: OnDemandSymbolIndex, - referencesProvider: () => ReferenceProvider, - workspaceSymbols: () => WorkspaceSymbolProvider, - buildTargets: BuildTargets, - interactiveSemanticdbs: () => InteractiveSemanticdbs, - semanticDBIndexer: () => SemanticdbIndexer, - worksheetProvider: () => WorksheetProvider, - symbolSearch: () => MetalsSymbolSearch, - fileWatcher: () => FileWatcher, - focusedDocument: () => Option[AbsolutePath], - focusedDocumentBuildTarget: AtomicReference[b.BuildTargetIdentifier], - buildTargetClasses: BuildTargetClasses, - userConfig: () => UserConfiguration, - sh: ScheduledExecutorService, - symbolDocs: Docstrings, - scalaVersionSelector: ScalaVersionSelector, - sourceMapper: SourceMapper, - workspaceFolder: AbsolutePath, - implementationProvider: ImplementationProvider, - resetService: () => Unit, - sharedIndices: SqlSharedIndices, -)(implicit rc: ReportContext) { +case class Indexer(indexProviders: IndexProviders)(implicit rc: ReportContext) { + import indexProviders._ private implicit def ec: ExecutionContextExecutorService = executionContext + val sharedIndices: SqlSharedIndices = new SqlSharedIndices - def reloadWorkspaceAndIndex( - forceRefresh: Boolean, - buildTool: BuildTool, - checksum: String, - importBuild: BspSession => Future[Unit], - reconnectToBuildServer: () => Future[BuildChange], - ): Future[BuildChange] = { - def reloadAndIndex(session: BspSession): Future[BuildChange] = { - workspaceReload().persistChecksumStatus(Status.Started, buildTool) - - buildTool.ensurePrerequisites(workspaceFolder) - buildTool match { - case _: BspOnly => - reconnectToBuildServer() - case _ if !session.canReloadWorkspace => - reconnectToBuildServer() - case _ => - session - .workspaceReload() - .flatMap(_ => importBuild(session)) - .map { _ => - scribe.info("Correctly reloaded workspace") - profiledIndexWorkspace(check) - workspaceReload().persistChecksumStatus( - Status.Installed, - buildTool, - ) - BuildChange.Reloaded - } - .recoverWith { case NonFatal(e) => - scribe.error(s"Unable to reload workspace: ${e.getMessage()}") - workspaceReload().persistChecksumStatus(Status.Failed, buildTool) - languageClient.showMessage(Messages.ReloadProjectFailed) - Future.successful(BuildChange.Failed) - } - } - } + var bspSession: Option[BspSession] = Option.empty[BspSession] - bspSession() match { - case None => - scribe.warn( - "No build session currently active to reload. Attempting to reconnect." - ) - reconnectToBuildServer() - case Some(session) if forceRefresh => reloadAndIndex(session) - case Some(session) => - workspaceReload().oldReloadResult(checksum) match { - case Some(status) => - scribe.info(s"Skipping reload with status '${status.name}'") - Future.successful(BuildChange.None) - case None => - if (userConfig().automaticImportBuild == AutoImportBuildKind.All) { - reloadAndIndex(session) - } else { - for { - userResponse <- workspaceReload().requestReload( - buildTool, - checksum, - ) - installResult <- { - if (userResponse.isYes) { - reloadAndIndex(session) - } else { - tables.dismissedNotifications.ImportChanges - .dismiss(2, TimeUnit.MINUTES) - Future.successful(BuildChange.None) - } - } - } yield installResult - } - } - } - } + protected val workspaceReload: WorkspaceReload = new WorkspaceReload( + folder, + languageClient, + tables, + ) + + def index(check: () => Unit): Future[Unit] = profiledIndexWorkspace(check) - def profiledIndexWorkspace(check: () => Unit): Future[Unit] = { + protected def profiledIndexWorkspace(check: () => Unit): Future[Unit] = { val tracked = workDoneProgress.trackFuture( Messages.indexing, Future { timerProvider.timedThunk("indexed workspace", onlyIf = true) { try indexWorkspace(check) finally { - indexingPromise().trySuccess(()) + indexingPromise.trySuccess(()) } } }, ) tracked.foreach { _ => - statusBar().addMessage( + statusBar.addMessage( s"${clientConfig.icons().rocket} Indexing complete!" ) if (clientConfig.initialConfig.statistics.isMemory) { Memory.logMemory( List( ("definition index", definitionIndex), - ("references index", referencesProvider().index), - ("identifier index", referencesProvider().identifierIndex.index), - ("workspace symbol index", workspaceSymbols().inWorkspace), + ("references index", referencesProvider.index), + ("identifier index", referencesProvider.identifierIndex.index), + ("workspace symbol index", workspaceSymbols.inWorkspace), ("build targets", buildTargets), ( "classpath symbol index", - workspaceSymbols().inDependencies.packages, + workspaceSymbols.inDependencies.packages, ), ) ) @@ -195,7 +85,7 @@ final case class Indexer( } private def indexWorkspace(check: () => Unit): Unit = { - fileWatcher().cancel() + fileWatcher.cancel() timerProvider.timedThunk( "reset stuff", @@ -216,11 +106,11 @@ final case class Indexer( data.addWorkspaceBuildTargets(importedBuild.workspaceBuildTargets) data.addScalacOptions( importedBuild.scalacOptions, - bspSession().map(_.mainConnection), + bspSession.map(_.mainConnection), ) data.addJavacOptions( importedBuild.javacOptions, - bspSession().map(_.mainConnection), + bspSession.map(_.mainConnection), ) data.addDependencyModules( @@ -258,13 +148,13 @@ final case class Indexer( clientConfig.initialConfig.statistics.isIndex, ) { try { - fileWatcher().cancel() - fileWatcher().start() + fileWatcher.cancel() + fileWatcher.start() } catch { // note(@tgodzik) This is needed in case of ammonite // where it can rarely deletes directories while we are trying to watch them case NonFatal(e) => - fileWatcher().cancel() + fileWatcher.cancel() scribe.warn("File watching failed, indexes will not be updated.", e) } } @@ -272,13 +162,13 @@ final case class Indexer( "indexed library classpath", clientConfig.initialConfig.statistics.isIndex, ) { - workspaceSymbols().indexClasspath() + workspaceSymbols.indexClasspath() } timerProvider.timedThunk( "indexed workspace SemanticDBs", clientConfig.initialConfig.statistics.isIndex, ) { - semanticDBIndexer().onTargetRoots() + semanticDBIndexer.onTargetRoots() } for (buildTool <- allBuildTargetsData) timerProvider.timedThunk( @@ -315,7 +205,7 @@ final case class Indexer( TimeUnit.SECONDS, ) - focusedDocument().foreach { doc => + focusedDocument.foreach { doc => buildTargets .inverseSources(doc) .foreach(focusedDocumentBuildTarget.set) @@ -520,7 +410,7 @@ final case class Indexer( // remove cached symbols from Jars // that are not used val usedJars = mutable.HashSet.empty[AbsolutePath] - val jdkSources = JdkSources(userConfig().javaHome) + val jdkSources = JdkSources(userConfig.javaHome) jdkSources match { case Right(zip) => scribe.debug(s"Indexing JDK sources from $zip") @@ -629,9 +519,9 @@ final case class Indexer( .map(_.allIdentifiers) .filter(_.nonEmpty) .foreach(identifiers => - referencesProvider().addIdentifiers(source, identifiers) + referencesProvider.addIdentifiers(source, identifiers) ) - workspaceSymbols().didChange(source, symbols.toSeq, methodSymbols.toSeq) + workspaceSymbols.didChange(source, symbols.toSeq, methodSymbols.toSeq) // Since the `symbols` here are toplevel symbols, // we cannot use `symbols` for expiring the cache for all symbols in the source. 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 ea3c8866c7d..dc3cbb0f0ac 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -22,9 +22,7 @@ import scala.util.control.NonFatal import scala.meta.internal.bsp.BspSession import scala.meta.internal.bsp.ConnectionBspStatus import scala.meta.internal.builds.BspErrorHandler -import scala.meta.internal.builds.BuildToolSelector import scala.meta.internal.builds.ShellRunner -import scala.meta.internal.builds.WorkspaceReload import scala.meta.internal.implementation.ImplementationProvider import scala.meta.internal.implementation.Supermethods import scala.meta.internal.io.FileIO @@ -99,28 +97,31 @@ import org.eclipse.{lsp4j => l} */ abstract class MetalsLspService( ec: ExecutionContextExecutorService, - sh: ScheduledExecutorService, + val sh: ScheduledExecutorService, serverInputs: MetalsServerInputs, - languageClient: ConfiguredLanguageClient, + val languageClient: ConfiguredLanguageClient, initializeParams: InitializeParams, - clientConfig: ClientConfiguration, - statusBar: StatusBar, - focusedDocument: () => Option[AbsolutePath], + val clientConfig: ClientConfiguration, + val statusBar: StatusBar, + getFocusedDocument: () => Option[AbsolutePath], shellRunner: ShellRunner, - timerProvider: TimerProvider, - folder: AbsolutePath, + val timerProvider: TimerProvider, + val folder: AbsolutePath, folderVisibleName: Option[String], headDoctor: HeadDoctor, bspStatus: BspStatus, - workDoneProgress: WorkDoneProgress, + val workDoneProgress: WorkDoneProgress, maxScalaCliServers: Int, ) extends Folder(folder, folderVisibleName, isKnownMetalsProject = true) with Cancelable - with TextDocumentService { + with TextDocumentService + with IndexProviders { import serverInputs._ + def focusedDocument: Option[AbsolutePath] = getFocusedDocument() + @volatile - protected var userConfig: UserConfiguration = initialUserConfig + var userConfig: UserConfiguration = initialUserConfig protected val userConfigPromise: Promise[Unit] = Promise() ThreadPools.discardRejectedRunnables("MetalsLanguageServer.sh", sh) @@ -150,7 +151,7 @@ abstract class MetalsLspService( } } - protected implicit val executionContext: ExecutionContextExecutorService = ec + implicit val executionContext: ExecutionContextExecutorService = ec protected val embedded: Embedded = register( new Embedded(workDoneProgress) @@ -174,12 +175,11 @@ abstract class MetalsLspService( protected val fingerprints = new MutableMd5Fingerprints protected val mtags = new Mtags - protected val focusedDocumentBuildTarget = + val focusedDocumentBuildTarget = new AtomicReference[b.BuildTargetIdentifier]() - protected val definitionIndex: OnDemandSymbolIndex = newSymbolIndex() - protected val symbolDocs = new Docstrings(definitionIndex) - var bspSession: Option[BspSession] = - Option.empty[BspSession] + val definitionIndex: OnDemandSymbolIndex = newSymbolIndex() + val symbolDocs = new Docstrings(definitionIndex) + def bspSession: Option[BspSession] = indexer.bspSession protected val savedFiles = new ActiveFiles(time) protected val recentlyOpenedFiles = new ActiveFiles(time) @@ -190,17 +190,17 @@ abstract class MetalsLspService( val buildTargets: BuildTargets = BuildTargets.from(folder, mainBuildTargetsData, tables) - protected val buildTargetClasses = + val buildTargetClasses = new BuildTargetClasses(buildTargets) - protected val sourceMapper: SourceMapper = SourceMapper( + val sourceMapper: SourceMapper = SourceMapper( buildTargets, buffers, ) protected val downstreamTargets = new PreviouslyCompiledDownsteamTargets - protected val scalaVersionSelector = new ScalaVersionSelector( + val scalaVersionSelector = new ScalaVersionSelector( () => userConfig, buildTargets, ) @@ -223,7 +223,7 @@ abstract class MetalsLspService( downstreamTargets, ) var indexingPromise: Promise[Unit] = Promise[Unit]() - var buildServerPromise: Promise[Unit] = Promise[Unit]() + def buildServerPromise: Promise[Unit] val parseTrees = new BatchedFunction[AbsolutePath, Unit]( paths => CancelableFuture( @@ -278,7 +278,7 @@ abstract class MetalsLspService( connectionBspStatus, ) - protected val workspaceSymbols: WorkspaceSymbolProvider = + val workspaceSymbols: WorkspaceSymbolProvider = new WorkspaceSymbolProvider( folder, buildTargets, @@ -456,7 +456,7 @@ abstract class MetalsLspService( ) ) - protected val referencesProvider: ReferenceProvider = new ReferenceProvider( + val referencesProvider: ReferenceProvider = new ReferenceProvider( folder, semanticdbs, buffers, @@ -494,7 +494,7 @@ abstract class MetalsLspService( buildTargets, ) - protected val implementationProvider: ImplementationProvider = + val implementationProvider: ImplementationProvider = new ImplementationProvider( semanticdbs, folder, @@ -523,7 +523,7 @@ abstract class MetalsLspService( symbolHierarchyOps, ) - protected val semanticDBIndexer: SemanticdbIndexer = new SemanticdbIndexer( + val semanticDBIndexer: SemanticdbIndexer = new SemanticdbIndexer( List( referencesProvider, implementationProvider, @@ -607,17 +607,6 @@ abstract class MetalsLspService( new ClassFinder(trees), ) - protected val workspaceReload: WorkspaceReload = new WorkspaceReload( - folder, - languageClient, - tables, - ) - - protected val buildToolSelector: BuildToolSelector = new BuildToolSelector( - languageClient, - tables, - ) - def loadedPresentationCompilerCount(): Int = compilers.loadedPresentationCompilerCount() @@ -1033,7 +1022,7 @@ abstract class MetalsLspService( inlayHint: InlayHint ): CompletableFuture[InlayHint] = { CancelTokens.future { token => - focusedDocument() + focusedDocument .map(path => inlayHintResolveProvider.resolve(inlayHint, path, token)) .getOrElse(Future.successful(inlayHint)) } @@ -1265,7 +1254,7 @@ abstract class MetalsLspService( val timer = new Timer(time) val result = workspaceSymbols - .search(params.getQuery, token, focusedDocument()) + .search(params.getQuery, token, focusedDocument) .toList if (clientConfig.initialConfig.statistics.isWorkspaceSymbol) { scribe.info( @@ -1276,7 +1265,7 @@ abstract class MetalsLspService( } def workspaceSymbol(query: String): Seq[SymbolInformation] = { - workspaceSymbols.search(query, focusedDocument()) + workspaceSymbols.search(query, focusedDocument) } def indexSources(): Future[Unit] = Future { @@ -1332,7 +1321,7 @@ abstract class MetalsLspService( def cancelCompile(): Future[Unit] = Future { // We keep this in here to provide a way for clients that aren't work done progress cancel providers // to be able to cancel a long-running worksheet evaluation by canceling compilation. - if (focusedDocument().exists(_.isWorksheet)) + if (focusedDocument.exists(_.isWorksheet)) worksheetProvider.cancel() compilations.cancel() @@ -1343,7 +1332,7 @@ abstract class MetalsLspService( def getLocationForSymbol(symbol: String): Option[Location] = definitionProvider - .fromSymbol(symbol, focusedDocument()) + .fromSymbol(symbol, focusedDocument) .asScala .headOption @@ -1529,7 +1518,7 @@ abstract class MetalsLspService( compilations, workDoneProgress, buffers, - () => indexer.profiledIndexWorkspace(() => ()), + () => indexer.index(() => ()), () => diagnostics, tables, () => buildClient, @@ -1542,9 +1531,9 @@ abstract class MetalsLspService( ) ) - protected def buildData(): Seq[Indexer.BuildTool] + def buildData(): Seq[Indexer.BuildTool] - protected def resetService(): Unit = { + def resetService(): Unit = { interactiveSemanticdbs.reset() buildClient.reset() semanticDBIndexer.reset() @@ -1554,44 +1543,7 @@ abstract class MetalsLspService( def fileWatcher: FileWatcher - private val sharedIndices = new SqlSharedIndices - - protected val indexer: Indexer = Indexer( - () => workspaceReload, - check, - languageClient, - () => bspSession, - executionContext, - tables, - () => statusBar, - workDoneProgress, - timerProvider, - () => scalafixProvider, - () => indexingPromise, - buildData, - clientConfig, - definitionIndex, - () => referencesProvider, - () => workspaceSymbols, - buildTargets, - () => interactiveSemanticdbs, - () => semanticDBIndexer, - () => worksheetProvider, - () => symbolSearch, - () => fileWatcher, - focusedDocument, - focusedDocumentBuildTarget, - buildTargetClasses, - () => userConfig, - sh, - symbolDocs, - scalaVersionSelector, - sourceMapper, - folder, - implementationProvider, - resetService, - sharedIndices, - ) + protected def indexer: Indexer def projectInfo: MetalsServiceInfo @@ -1620,8 +1572,8 @@ abstract class MetalsLspService( ): Future[Unit] = { paths .find { path => - if (clientConfig.isDidFocusProvider() || focusedDocument().isDefined) { - focusedDocument().contains(path) && + if (clientConfig.isDidFocusProvider() || focusedDocument.isDefined) { + focusedDocument.contains(path) && path.isWorksheet } else { path.isWorksheet diff --git a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala index 0566cb4fc39..8013abde93b 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala @@ -1,40 +1,23 @@ package scala.meta.internal.metals -import java.nio.file.Path import java.util import java.util.concurrent.CompletableFuture import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import scala.concurrent.ExecutionContextExecutorService import scala.concurrent.Future import scala.util.Failure import scala.util.Success -import scala.util.control.NonFatal -import scala.meta.internal.bsp.BspConfigGenerationStatus.BspConfigGenerationStatus -import scala.meta.internal.bsp.BspConfigGenerationStatus.Cancelled -import scala.meta.internal.bsp.BspConfigGenerationStatus.Failed -import scala.meta.internal.bsp.BspConfigGenerationStatus.Generated import scala.meta.internal.bsp.BspConfigGenerator -import scala.meta.internal.bsp.BspConnector -import scala.meta.internal.bsp.BspServers import scala.meta.internal.bsp.BspSession import scala.meta.internal.bsp.BuildChange import scala.meta.internal.bsp.ScalaCliBspScope -import scala.meta.internal.builds.BloopInstall -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.Digest -import scala.meta.internal.builds.MillBuildTool -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.metals.Messages.IncompatibleBloopVersion import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.ammonite.Ammonite import scala.meta.internal.metals.clients.language.ConfiguredLanguageClient @@ -45,7 +28,6 @@ import scala.meta.internal.metals.watcher.FileWatcherEvent.EventType import scala.meta.internal.metals.watcher.ProjectFileWatcher import scala.meta.internal.mtags.SemanticdbPath import scala.meta.internal.mtags.Semanticdbs -import scala.meta.internal.semver.SemVer import scala.meta.internal.tvp.FolderTreeViewProvider import scala.meta.io.AbsolutePath @@ -53,26 +35,25 @@ import ch.epfl.scala.{bsp4j => b} import org.eclipse.lsp4j.DidChangeTextDocumentParams import org.eclipse.lsp4j.DidSaveTextDocumentParams import org.eclipse.lsp4j.InitializeParams -import org.eclipse.lsp4j.MessageParams import org.eclipse.lsp4j.MessageType class ProjectMetalsLspService( ec: ExecutionContextExecutorService, - sh: ScheduledExecutorService, + override val sh: ScheduledExecutorService, serverInputs: MetalsServerInputs, - languageClient: ConfiguredLanguageClient, + override val languageClient: ConfiguredLanguageClient, initializeParams: InitializeParams, - clientConfig: ClientConfiguration, - statusBar: StatusBar, + override val clientConfig: ClientConfiguration, + override val statusBar: StatusBar, focusedDocument: () => Option[AbsolutePath], shellRunner: ShellRunner, - timerProvider: TimerProvider, + override val timerProvider: TimerProvider, initTreeView: () => Unit, - folder: AbsolutePath, + override val folder: AbsolutePath, folderVisibleName: Option[String], headDoctor: HeadDoctor, bspStatus: BspStatus, - workDoneProgress: WorkDoneProgress, + override val workDoneProgress: WorkDoneProgress, maxScalaCliServers: Int, ) extends MetalsLspService( ec, @@ -103,8 +84,11 @@ class ProjectMetalsLspService( charset, ) - val isImportInProcess = new AtomicBoolean(false) - val isConnecting = new AtomicBoolean(false) + override def indexer: Indexer = connectionProvider + def buildServerPromise = connectionProvider.buildServerPromise + def connect[T](config: ConnectRequest): Future[BuildChange] = + connectionProvider.Connect.connect(config) + val willGenerateBspConfig = new AtomicReference(Set.empty[util.UUID]) def withWillGenerateBspConfig[T](body: => Future[T]): Future[T] = { @@ -128,15 +112,6 @@ class ProjectMetalsLspService( ) ) - protected val bloopInstall: BloopInstall = new BloopInstall( - folder, - languageClient, - buildTools, - tables, - shellRunner, - () => userConfig, - ) - protected val bspConfigGenerator: BspConfigGenerator = new BspConfigGenerator( folder, languageClient, @@ -166,6 +141,44 @@ class ProjectMetalsLspService( compilations.isCurrentlyCompiling, ) + val buildToolProvider: BuildToolProvider = new BuildToolProvider( + buildTools, + tables, + folder, + warnings, + languageClient, + ) + + protected val bloopServers: BloopServers = new BloopServers( + buildClient, + languageClient, + tables, + clientConfig.initialConfig, + workDoneProgress, + ) + + val connectionProvider: ConnectionProvider = new ConnectionProvider( + buildToolProvider, + compilations, + buildTools, + buffers, + compilers, + scalaCli, + bloopServers, + shellRunner, + bspConfigGenerator, + check, + doctor, + initTreeView, + diagnostics, + charset, + buildClient, + bspGlobalDirectories, + connectionBspStatus, + mainBuildTargetsData, + this, + ) + protected val onBuildChanged: BatchedFunction[AbsolutePath, Unit] = BatchedFunction.fromFuture[AbsolutePath, Unit]( onBuildChangedUnbatched, @@ -189,7 +202,7 @@ class ProjectMetalsLspService( () => tables.buildTool.selectedBuildTool(), buildTargets, () => bspSession, - () => bspConnector.resolve(buildTool), + connectionProvider.resolveBsp, buildTools, ) @@ -199,41 +212,6 @@ class ProjectMetalsLspService( // (require ./mill or ./.mill-version) buildTools.isMill - protected val bloopServers: BloopServers = new BloopServers( - buildClient, - languageClient, - tables, - clientConfig.initialConfig, - workDoneProgress, - ) - - protected val bspServers: BspServers = new BspServers( - folder, - charset, - languageClient, - buildClient, - tables, - bspGlobalDirectories, - clientConfig.initialConfig, - () => userConfig, - workDoneProgress, - ) - - protected val bspConnector: BspConnector = new BspConnector( - bloopServers, - bspServers, - buildTools, - languageClient, - tables, - () => userConfig, - statusBar, - workDoneProgress, - bspConfigGenerator, - () => bspSession.map(_.mainConnection), - restartBspServer, - connectionBspStatus, - ) - override def didChange( params: DidChangeTextDocumentParams ): CompletableFuture[Unit] = { @@ -276,150 +254,6 @@ class ProjectMetalsLspService( .ignoreValue } - def saveProjectReferencesInfo(bspBuilds: List[BspSession.BspBuild]): Unit = { - val projectRefs = bspBuilds - .flatMap { session => - session.build.workspaceBuildTargets.getTargets().asScala.flatMap { - _.getBaseDirectory() match { - case null | "" => None - case path => path.toAbsolutePathSafe - } - } - } - .distinct - .filterNot(_.startWith(path)) - if (projectRefs.nonEmpty) - DelegateSetting.writeProjectRef(path, projectRefs) - } - - protected def importBuild(session: BspSession): Future[Unit] = { - val importedBuilds0 = timerProvider.timed("Imported build") { - session.importBuilds() - } - for { - bspBuilds <- workDoneProgress.trackFuture( - Messages.importingBuild, - importedBuilds0, - ) - _ = { - val idToConnection = bspBuilds.flatMap { bspBuild => - val targets = - bspBuild.build.workspaceBuildTargets.getTargets().asScala - targets.map(t => (t.getId(), bspBuild.connection)) - } - mainBuildTargetsData.resetConnections(idToConnection) - saveProjectReferencesInfo(bspBuilds) - } - } yield compilers.cancel() - } - - def slowConnectToBuildServer( - forceImport: Boolean - ): Future[BuildChange] = { - val chosenBuildServer = tables.buildServers.selectedServer() - def useBuildToolBsp(buildTool: BloopInstallProvider) = - buildTool match { - case _: BuildServerProvider => userConfig.defaultBspToBuildTool - case _ => false - } - - def isSelected(buildTool: BuildTool) = - buildTool match { - case _: BuildServerProvider => - chosenBuildServer.contains(buildTool.buildServerName) - case _ => false - } - - supportedBuildTool().flatMap { - case Some(BuildTool.Found(buildTool: BloopInstallProvider, digest)) - if chosenBuildServer.contains(BloopServers.name) || - chosenBuildServer.isEmpty && !useBuildToolBsp(buildTool) => - slowConnectToBloopServer(forceImport, buildTool, digest) - case Some(found) - if isSelected(found.buildTool) && - found.buildTool.isBspGenerated(folder) => - indexer.reloadWorkspaceAndIndex( - forceImport, - found.buildTool, - found.digest, - importBuild, - reconnectToBuildServer = () => - if (!isConnecting.get()) { - quickConnectToBuildServer() - } else { - scribe.warn("Cannot reload build session, still connecting...") - Future.successful(BuildChange.None) - }, - ) - case Some(BuildTool.Found(buildTool: BuildServerProvider, _)) => - slowConnectToBuildToolBsp(buildTool, forceImport, isSelected(buildTool)) - // Used when there are multiple `.bsp/.json` configs and a known build tool (e.g. sbt) - case Some(BuildTool.Found(buildTool, _)) - if buildTool.isBspGenerated(folder) => - maybeChooseServer(buildTool.buildServerName, isSelected(buildTool)) - quickConnectToBuildServer() - // Used in tests, `.bloop` folder exists but no build tool is detected - case _ => quickConnectToBuildServer() - } - } - - protected def slowConnectToBuildToolBsp( - buildTool: BuildServerProvider, - forceImport: Boolean, - isSelected: Boolean, - ): Future[BuildChange] = { - val notification = tables.dismissedNotifications.ImportChanges - if (buildTool.isBspGenerated(folder)) { - maybeChooseServer(buildTool.buildServerName, isSelected) - quickConnectToBuildServer() - } else if ( - userConfig.shouldAutoImportNewProject || forceImport || isSelected || - buildTool.isInstanceOf[ScalaCliBuildTool] - ) { - maybeChooseServer(buildTool.buildServerName, isSelected) - 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) { - maybeChooseServer(buildTool.buildServerName, isSelected) - generateBspAndConnect(buildTool) - } else { - notification.dismiss(2, util.concurrent.TimeUnit.MINUTES) - Future.successful(BuildChange.None) - } - } - } - } - - protected def maybeChooseServer(name: String, alreadySelected: Boolean): Any = - if (alreadySelected) Future.successful(()) - else tables.buildServers.chooseServer(name) - - protected def generateBspAndConnect( - buildTool: BuildServerProvider - ): Future[BuildChange] = - withWillGenerateBspConfig { - 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. @@ -434,78 +268,66 @@ class ProjectMetalsLspService( } else Future.successful(()) } - protected def slowConnectToBloopServer( - forceImport: Boolean, - buildTool: BloopInstallProvider, - checksum: String, - ): Future[BuildChange] = - for { - result <- { - if (forceImport) - bloopInstall.runUnconditionally(buildTool, isImportInProcess) - else bloopInstall.runIfApproved(buildTool, checksum, isImportInProcess) - } - change <- { - if (result.isInstalled) quickConnectToBuildServer() - else if (result.isFailed) { - for { - change <- - if (buildTools.isAutoConnectable(optProjectRoot)) { - // 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 - - override def optProjectRoot: Option[AbsolutePath] = - buildTool.map(_.projectRoot).orElse(buildTools.bloopProject) - - def quickConnectToBuildServer(): Future[BuildChange] = - for { - change <- - if (!buildTools.isAutoConnectable(optProjectRoot)) { - scribe.warn("Build server is not auto-connectable.") - Future.successful(BuildChange.None) - } else { - autoConnectToBuildServer() - } - } yield { - buildServerPromise.trySuccess(()) - change - } - - def fullConnect(): Future[Unit] = { - buildTools.initialize() - for { - _ <- - if (buildTools.isAutoConnectable(optProjectRoot)) - autoConnectToBuildServer() - else slowConnectToBuildServer(forceImport = false) - } yield buildServerPromise.trySuccess(()) - } - protected def onInitialized(): Future[Unit] = withWillGenerateBspConfig { for { _ <- maybeSetupScalaCli() - _ <- fullConnect() + _ <- connectionProvider.fullConnect() } yield () } + def onBuildChangedUnbatched( + paths: Seq[AbsolutePath] + ): Future[Unit] = + if (willGenerateBspConfig.get().nonEmpty) Future.unit + else { + val changedBuilds = paths.flatMap(buildTools.isBuildRelated) + tables.buildTool.selectedBuildTool() match { + // no build tool and new added + case None if changedBuilds.nonEmpty => + scribe.info(s"Detected new build tool in $path") + connectionProvider.fullConnect() + // used build tool changed + case Some(chosenBuildTool) if changedBuilds.contains(chosenBuildTool) => + connectionProvider + .slowConnectToBuildServer(forceImport = false) + .ignoreValue + // maybe new build tool added + case Some(chosenBuildTool) if changedBuilds.nonEmpty => + onBuildToolsAdded(chosenBuildTool, changedBuilds) + case _ => Future.unit + } + } + + private def onBuildToolsAdded( + currentBuildToolName: String, + newBuildToolsChanged: Seq[String], + ): Future[Unit] = { + val supportedBuildTools = buildTools.loadSupported() + val maybeBuildChange = for { + currentBuildTool <- supportedBuildTools.find( + _.executableName == currentBuildToolName + ) + newBuildTool <- newBuildToolsChanged + .filter(buildTools.newBuildTool) + .flatMap(addedBuildName => + supportedBuildTools.find( + _.executableName == addedBuildName + ) + ) + .headOption + } yield { + buildToolProvider + .onNewBuildToolAdded(newBuildTool, currentBuildTool) + .flatMap { switch => + if (switch) + connectionProvider.slowConnectToBuildServer(forceImport = false) + else Future.successful(BuildChange.None) + } + }.ignoreValue + maybeBuildChange.getOrElse(Future.unit) + } + protected def updateBspJavaHome(session: BspSession): Future[Any] = { if (session.main.isBazel) { languageClient.showMessage( @@ -522,31 +344,14 @@ class ProjectMetalsLspService( .asScala .flatMap { case Messages.ProjectJavaHomeUpdate.restart => - buildTool match { - case Some(sbt: SbtBuildTool) if session.main.isSbt => - for { - _ <- disconnectOldBuildServer() - _ <- sbt.shutdownBspServer(shellRunner) - _ <- sbt.generateBspConfig( - folder, - bspConfigGenerator.runUnconditionally(sbt, _), - statusBar, - ) - _ <- autoConnectToBuildServer() - } yield () - case Some(mill: MillBuildTool) if session.main.isMill => - for { - _ <- mill.generateBspConfig( - folder, - bspConfigGenerator.runUnconditionally(mill, _), - statusBar, - ) - _ <- autoConnectToBuildServer() - } yield () - case _ if session.main.isBloop => - slowConnectToBuildServer(forceImport = true) - case _ => Future.successful(()) - } + if (session.main.isBloop) + connectionProvider.slowConnectToBuildServer(forceImport = true) + else + buildToolProvider.buildTool + .map { case bt: BuildServerProvider => + connect(GenerateBspConfigAndConnect(bt, true)) + } + .getOrElse(Future.unit) case Messages.ProjectJavaHomeUpdate.notNow => Future.successful(()) } @@ -575,14 +380,7 @@ class ProjectMetalsLspService( .flatMap { case FileOutOfScalaCliBspScope.regenerateAndRestart => val buildTool = ScalaCliBuildTool(folder, folder, () => userConfig) - for { - _ <- buildTool.generateBspConfig( - folder, - bspConfigGenerator.runUnconditionally(buildTool, _), - statusBar, - ) - _ <- quickConnectToBuildServer() - } yield () + connect(GenerateBspConfigAndConnect(buildTool)).ignoreValue case _ => Future.successful(()) } } else Future.successful(()) @@ -632,47 +430,6 @@ class ProjectMetalsLspService( } } - def restartBspServer(): Future[Boolean] = { - def emitMessage(msg: String) = { - languageClient.showMessage(new MessageParams(MessageType.Warning, msg)) - } - // This is for `bloop` and `sbt`, for which `build/shutdown` doesn't shutdown the server. - val shutdownBsp = - bspSession match { - case Some(session) if session.main.isBloop => - for { - _ <- disconnectOldBuildServer() - } yield bloopServers.shutdownServer() - case Some(session) if session.main.isSbt => - for { - res <- buildTool match { - case Some(sbt: SbtBuildTool) => - for { - _ <- disconnectOldBuildServer() - code <- sbt.shutdownBspServer(shellRunner) - } yield code == 0 - case _ => Future.successful(false) - } - } yield res - case s => Future.successful(s.nonEmpty) - } - - for { - didShutdown <- shutdownBsp - _ = if (!didShutdown) { - bspSession match { - case Some(session) => - emitMessage( - s"Could not shutdown ${session.main.name} server. Will try to reconnect." - ) - case None => - emitMessage("No build server connected. Will try to connect.") - } - } - _ <- autoConnectToBuildServer() - } yield didShutdown - } - private val ammonite: Ammonite = register { val amm = new Ammonite( buffers, @@ -684,7 +441,7 @@ class ProjectMetalsLspService( languageClient, buildClient, () => userConfig, - () => indexer.profiledIndexWorkspace(() => ()), + () => indexer.index(() => ()), () => folder, focusedDocument, clientConfig.initialConfig, @@ -699,14 +456,14 @@ class ProjectMetalsLspService( tables, languageClient, headDoctor.executeRefreshDoctor, - () => slowConnectToBuildServer(forceImport = true), + () => connectionProvider.slowConnectToBuildServer(forceImport = true), () => switchBspServer(), ) def projectInfo: MetalsServiceInfo = MetalsServiceInfo.ProjectService( () => bspSession, - () => bspConnector.resolve(buildTool), + connectionProvider.resolveBsp, buildTools, connectionBspStatus, () => getProjectsJavaInfo, @@ -739,14 +496,14 @@ class ProjectMetalsLspService( def switchBspServer(): Future[Unit] = withWillGenerateBspConfig { for { - isSwitched <- bspConnector.switchBuildServer( - folder, - () => slowConnectToBuildServer(forceImport = true), - ) - _ <- { - if (isSwitched) quickConnectToBuildServer() - else Future.successful(()) - } + connectKind <- connectionProvider.bspConnector.switchBuildServer() + _ <- + connectKind match { + case None => Future.unit + case Some(SlowConnect) => + connectionProvider.slowConnectToBuildServer(forceImport = true) + case Some(request: ConnectRequest) => connect(request) + } } yield () } @@ -762,244 +519,28 @@ class ProjectMetalsLspService( case buildTool: BuildServerProvider => buildTool } - def ensureAndConnect( - buildTool: BuildServerProvider, - status: BspConfigGenerationStatus, - ): Unit = - status match { - case Generated => - tables.buildServers.chooseServer(buildTool.buildServerName) - quickConnectToBuildServer().ignoreValue - case Cancelled => () - case Failed(exit) => - exit match { - case Left(exitCode) => - scribe.error( - s"Create of .bsp failed with exit code: $exitCode" - ) - languageClient.showMessage( - Messages.BspProvider.genericUnableToCreateConfig - ) - case Right(message) => - languageClient.showMessage( - Messages.BspProvider.unableToCreateConfigFromMessage( - message - ) - ) - } - } - (servers match { case Nil => scribe.warn(Messages.BspProvider.noBuildToolFound.toString()) languageClient.showMessage(Messages.BspProvider.noBuildToolFound) Future.successful(()) case buildTool :: Nil => - withWillGenerateBspConfig { - buildTool - .generateBspConfig( - folder, - args => - bspConfigGenerator.runUnconditionally( - buildTool, - args, - ), - statusBar, - ) - .map(status => ensureAndConnect(buildTool, status)) - } - case buildTools => - withWillGenerateBspConfig { - bspConfigGenerator - .chooseAndGenerate(buildTools) - .map { - case ( - buildTool: BuildServerProvider, - status: BspConfigGenerationStatus, - ) => - ensureAndConnect(buildTool, status) - } - } - }) - } - - protected def buildTool: Option[BuildTool] = - for { - name <- tables.buildTool.selectedBuildTool() - buildTool <- buildTools.current().find(_.executableName == name) - if isCompatibleVersion(buildTool) - } yield buildTool - - private def isCompatibleVersion(buildTool: BuildTool): Boolean = { - buildTool match { - case buildTool: VersionRecommendation => - SemVer.isCompatibleVersion( - buildTool.minimumVersion, - buildTool.version, - ) - case _ => true - } - } - - /** - * Checks if the version of the build tool is compatible with the version that - * metals expects and returns the current digest of the build tool. - */ - private def verifyBuildTool(buildTool: BuildTool): BuildTool.Verified = { - buildTool match { - case buildTool: VersionRecommendation - if !isCompatibleVersion(buildTool) => - BuildTool.IncompatibleVersion(buildTool) - case _ => - buildTool.digestWithRetry(folder) match { - case Some(digest) => - BuildTool.Found(buildTool, digest) - case None => BuildTool.NoChecksum(buildTool, folder) - } - } - } - - def supportedBuildTool(): Future[Option[BuildTool.Found]] = { - buildTools.loadSupported() match { - case Nil => { - if (!buildTools.isAutoConnectable()) { - warnings.noBuildTool() - } - Future(None) - } + connect( + GenerateBspConfigAndConnect(buildTool) + ).ignoreValue case buildTools => for { - buildTool <- buildToolSelector.checkForChosenBuildTool( + Some(buildTool) <- bspConfigGenerator.chooseBuildServerProvider( buildTools ) - } yield { - buildTool.flatMap { bt => - verifyBuildTool(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 autoConnectToBuildServer(): Future[BuildChange] = { - def compileAllOpenFiles: BuildChange => Future[BuildChange] = { - case change if !change.isFailed => - Future - .sequence( - compilations - .cascadeCompileFiles(buffers.open.toSeq) - .ignoreValue :: - compilers.load(buffers.open.toSeq) :: - Nil + _ <- connect( + GenerateBspConfigAndConnect(buildTool) ) - .map(_ => change) - case other => Future.successful(other) - } - - val scalaCliPaths = scalaCli.paths - - isConnecting.set(true) - (for { - _ <- disconnectOldBuildServer() - maybeSession <- timerProvider.timed("Connected to build server", true) { - bspConnector.connect( - buildTool, - folder, - userConfig, - shellRunner, - ) - } - result <- maybeSession match { - case Some(session) => - val result = connectToNewBuildServer(session) - session.mainConnection.onReconnection { newMainConn => - val updSession = session.copy(main = newMainConn) - connectToNewBuildServer(updSession) - .flatMap(compileAllOpenFiles) - .ignoreValue - } - result - case None => - Future.successful(BuildChange.None) - } - _ <- Future.sequence( - scalaCliPaths - .collect { - case path if (!conflictsWithMainBsp(path.toNIO)) => - scalaCli.start(path) - } - ) - _ = initTreeView() - } yield result) - .recover { case NonFatal(e) => - disconnectOldBuildServer() - val message = - "Failed to connect with build server, no functionality will work." - val details = " See logs for more details." - languageClient.showMessage( - new MessageParams(MessageType.Error, message + details) - ) - scribe.error(message, e) - BuildChange.Failed - } - .flatMap(compileAllOpenFiles) - } - - def disconnectOldBuildServer(): Future[Unit] = { - compilations.cancel() - buildTargetClasses.cancel() - diagnostics.reset() - bspSession.foreach(connection => - scribe.info(s"Disconnecting from ${connection.main.name} session...") - ) - - for { - _ <- scalaCli.stop() - _ <- bspSession match { - case None => Future.successful(()) - case Some(session) => - bspSession = None - mainBuildTargetsData.resetConnections(List.empty) - session.shutdown() - } - } yield () - } - - protected def connectToNewBuildServer( - session: BspSession - ): Future[BuildChange] = { - scribe.info( - s"Connected to Build server: ${session.main.name} v${session.version}" - ) - cancelables.add(session) - buildTool.foreach( - workspaceReload.persistChecksumStatus(Digest.Status.Started, _) - ) - bspSession = Some(session) - isConnecting.set(false) - for { - _ <- importBuild(session) - _ <- indexer.profiledIndexWorkspace(check) - _ = buildTool.foreach( - workspaceReload.persistChecksumStatus(Digest.Status.Installed, _) - ) - _ = if (session.main.isBloop) checkRunningBloopVersion(session.version) - } yield { - BuildChange.Reconnected - } + } yield () + }) } - protected def buildData(): Seq[Indexer.BuildTool] = + def buildData(): Seq[Indexer.BuildTool] = Seq( Indexer.BuildTool( "main", @@ -1021,27 +562,21 @@ class ProjectMetalsLspService( def resetWorkspace(): Future[Unit] = for { - _ <- disconnectOldBuildServer() - shouldImport = optProjectRoot match { + _ <- connect(Disconnect(true)) + _ = optProjectRoot match { case Some(path) if buildTools.isBloop(path) => - bloopServers.shutdownServer() clearBloopDir(path) - false case Some(path) if buildTools.isBazelBsp => clearFolders( path.resolve(Directories.bazelBsp), path.resolve(Directories.bsp), ) - true case Some(path) if buildTools.isBsp => clearFolders(path.resolve(Directories.bsp)) - true - case _ => false + case _ => } _ = tables.cleanAll() - _ <- - if (shouldImport) slowConnectToBuildServer(true) - else autoConnectToBuildServer().map(_ => ()) + _ <- connectionProvider.fullConnect() } yield () val treeView = @@ -1092,8 +627,7 @@ class ProjectMetalsLspService( case None => scribe.warn("No build server connected") case Some(session) => for { - _ <- importBuild(session) - _ <- indexer.profiledIndexWorkspace(check) + _ <- connect(ImportBuildAndIndex(session)) } { focusedDocument().foreach(path => compilations.compileFile(path)) } @@ -1110,7 +644,7 @@ class ProjectMetalsLspService( if (userConfig.customProjectRoot != old.customProjectRoot) { tables.buildTool.reset() tables.buildServers.reset() - fullConnect() + connectionProvider.fullConnect() } else Future.successful(()) val resetDecorations = @@ -1127,14 +661,14 @@ class ProjectMetalsLspService( session.version, userConfig.bloopVersion.nonEmpty, old.bloopVersion.isDefined, - () => autoConnectToBuildServer, + () => connect(CreateSession()), ) .flatMap { _ => userConfig.bloopJvmProperties .map( bloopServers.ensureDesiredJvmSettings( _, - () => autoConnectToBuildServer(), + () => connect(CreateSession()), ) ) .getOrElse(Future.unit) @@ -1170,76 +704,6 @@ class ProjectMetalsLspService( } yield () } - protected def checkRunningBloopVersion(bspServerVersion: String): Unit = { - if (doctor.isUnsupportedBloopVersion()) { - val notification = tables.dismissedNotifications.IncompatibleBloop - if (!notification.isDismissed) { - val messageParams = IncompatibleBloopVersion.params( - bspServerVersion, - BuildInfo.bloopVersion, - isChangedInSettings = userConfig.bloopVersion != None, - ) - languageClient.showMessageRequest(messageParams).asScala.foreach { - case action if action == IncompatibleBloopVersion.shutdown => - bloopServers.shutdownServer() - autoConnectToBuildServer() - case action if action == IncompatibleBloopVersion.dismissForever => - notification.dismissForever() - case _ => - } - } - } - } - - protected def onBuildChangedUnbatched( - paths: Seq[AbsolutePath] - ): Future[Unit] = - if (willGenerateBspConfig.get().nonEmpty) Future.unit - else { - val changedBuilds = paths.flatMap(buildTools.isBuildRelated) - tables.buildTool.selectedBuildTool() match { - // no build tool and new added - case None if changedBuilds.nonEmpty => - scribe.info(s"Detected new build tool in $path") - fullConnect() - // used build tool changed - case Some(chosenBuildTool) if changedBuilds.contains(chosenBuildTool) => - slowConnectToBuildServer(forceImport = false).ignoreValue - // maybe new build tool added - case Some(chosenBuildTool) if changedBuilds.nonEmpty => - onBuildToolsAdded(chosenBuildTool, changedBuilds) - case _ => Future.unit - } - } - - protected def onBuildToolsAdded( - currentBuildToolName: String, - newBuildToolsChanged: Seq[String], - ): Future[Unit] = { - val supportedBuildTools = buildTools.loadSupported() - val maybeBuildChange = for { - currentBuildTool <- supportedBuildTools.find( - _.executableName == currentBuildToolName - ) - newBuildTool <- newBuildToolsChanged - .filter(buildTools.newBuildTool) - .flatMap(addedBuildName => - supportedBuildTools.find( - _.executableName == addedBuildName - ) - ) - .headOption - } yield { - buildToolSelector - .onNewBuildToolAdded(newBuildTool, currentBuildTool) - .flatMap { switch => - if (switch) slowConnectToBuildServer(forceImport = false) - else Future.successful(BuildChange.None) - } - }.ignoreValue - maybeBuildChange.getOrElse(Future.unit) - } - def maybeImportScript(path: AbsolutePath): Option[Future[Unit]] = { val scalaCliPath = scalaCliDirOrFile(path) if ( @@ -1351,15 +815,9 @@ class ProjectMetalsLspService( def scalaCliDirOrFile(path: AbsolutePath): AbsolutePath = { val dir = path.parent val nioDir = dir.toNIO - if (conflictsWithMainBsp(nioDir)) path else dir + if (buildTargets.belongsToBuildTarget(nioDir)) path else dir } - protected def conflictsWithMainBsp(nioDir: Path): Boolean = - buildTargets.sourceItems.filter(_.exists).exists { item => - val nioItem = item.toNIO - nioDir.startsWith(nioItem) || nioItem.startsWith(nioDir) - } - override def startScalaCli(path: AbsolutePath): Future[Unit] = { super.startScalaCli(scalaCliDirOrFile(path)) } @@ -1406,7 +864,7 @@ class ProjectMetalsLspService( _ <- maybeImportScript(path).getOrElse(load()) } yield () - override protected def resetService(): Unit = { + override def resetService(): Unit = { super.resetService() treeView.reset() } diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala index f291edb4bcd..51cc95ff5b8 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala @@ -754,7 +754,7 @@ class WorkspaceLspService( foreachSeqIncludeFallback(_.indexSources(), ignoreValue = true) case ServerCommands.RestartBuildServer() => onCurrentFolder( - _.restartBspServer().ignoreValue, + _.connect(CreateSession(shutdownBuildServer = true)).ignoreValue, ServerCommands.RestartBuildServer.title, ).asJavaObject case ServerCommands.GenerateBspConfig() => @@ -764,19 +764,19 @@ class WorkspaceLspService( ).asJavaObject case ServerCommands.ImportBuild() => onCurrentFolder( - _.slowConnectToBuildServer(forceImport = true), + _.connectionProvider.slowConnectToBuildServer(forceImport = true), ServerCommands.ImportBuild.title, default = () => BuildChange.None, ).asJavaObject case ServerCommands.ConnectBuildServer() => onCurrentFolder( - _.quickConnectToBuildServer(), + _.connectionProvider.quickConnectToBuildServer(), ServerCommands.ConnectBuildServer.title, default = () => BuildChange.None, ).asJavaObject case ServerCommands.DisconnectBuildServer() => onCurrentFolder( - _.disconnectOldBuildServer(), + _.connect(Disconnect(shutdownBuildServer = false)).ignoreValue, ServerCommands.DisconnectBuildServer.title, ).asJavaObject case ServerCommands.DecodeFile(uri) => diff --git a/tests/slow/src/test/scala/tests/bazel/BazelLspSuite.scala b/tests/slow/src/test/scala/tests/bazel/BazelLspSuite.scala index 553284dec2c..2f1fd0cb7f8 100644 --- a/tests/slow/src/test/scala/tests/bazel/BazelLspSuite.scala +++ b/tests/slow/src/test/scala/tests/bazel/BazelLspSuite.scala @@ -107,7 +107,7 @@ class BazelLspSuite } _ <- server.didSave(s"BUILD")(identity) _ = assertNoDiff(client.workspaceMessageRequests, "") - _ = server.server.buildServerPromise = Promise() + _ = server.headServer.connectionProvider.buildServerPromise = Promise() _ <- server.executeCommand(ServerCommands.GenerateBspConfig) // We need to wait a bit just to ensure the connection is made _ <- server.server.buildServerPromise.future @@ -160,7 +160,7 @@ class BazelLspSuite } _ <- server.didSave(s"BUILD")(identity) _ = assertNoDiff(client.workspaceMessageRequests, "") - _ = server.server.buildServerPromise = Promise() + _ = server.headServer.connectionProvider.buildServerPromise = Promise() _ <- server.executeCommand(ServerCommands.ImportBuild) // We need to wait a bit just to ensure the connection is made _ <- server.server.buildServerPromise.future @@ -182,7 +182,7 @@ class BazelLspSuite | Test <- NOT SUPPORTED | Compile""".stripMargin _ = assertNoDiff(result, expectedTarget) - _ = server.server.buildServerPromise = Promise() + _ = server.headServer.connectionProvider.buildServerPromise = Promise() _ = client.resetWorkspace = new MessageActionItem(Messages.ResetWorkspace.resetWorkspace) _ <- server.executeCommand(ServerCommands.ResetWorkspace) diff --git a/tests/slow/src/test/scala/tests/gradle/GradleLspSuite.scala b/tests/slow/src/test/scala/tests/gradle/GradleLspSuite.scala index e96e4a6288f..6eaaed7d738 100644 --- a/tests/slow/src/test/scala/tests/gradle/GradleLspSuite.scala +++ b/tests/slow/src/test/scala/tests/gradle/GradleLspSuite.scala @@ -90,7 +90,7 @@ class GradleLspSuite extends BaseImportSuite("gradle-import") { ) _ <- server.server.indexingPromise.future _ = assert(server.server.bspSession.get.main.isBloop) - buildTool <- server.headServer.supportedBuildTool() + buildTool <- server.headServer.buildToolProvider.supportedBuildTool() _ = assertEquals(buildTool.get.buildTool.executableName, "gradle") _ = assertEquals( buildTool.get.buildTool.projectRoot, diff --git a/tests/slow/src/test/scala/tests/mill/MillServerSuite.scala b/tests/slow/src/test/scala/tests/mill/MillServerSuite.scala index fb202c449b8..c0d62627182 100644 --- a/tests/slow/src/test/scala/tests/mill/MillServerSuite.scala +++ b/tests/slow/src/test/scala/tests/mill/MillServerSuite.scala @@ -109,7 +109,7 @@ class MillServerSuite // below it won't wait until it reconnects to Mill like we want, so we set it back // and then it will be completed after the BSP config generation and the server // connects. - _ = server.server.buildServerPromise = Promise() + _ = server.headServer.connectionProvider.buildServerPromise = Promise() // At this point, we want to use mill-bsp server, so create the mill-bsp.json file. _ <- server.executeCommand(ServerCommands.GenerateBspConfig) // We need to wait a bit just to ensure the connection is made @@ -158,7 +158,7 @@ class MillServerSuite ) _ = client.messageRequests.clear() // restart _ = assert(!millBspConfig.exists) - _ = server.server.buildServerPromise = Promise() + _ = server.headServer.connectionProvider.buildServerPromise = Promise() _ <- server.executeCommand(ServerCommands.GenerateBspConfig) _ <- server.server.buildServerPromise.future _ = assert(millBspConfig.exists) diff --git a/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala b/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala index f7244c98305..86f40b1fa21 100644 --- a/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala +++ b/tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala @@ -5,6 +5,7 @@ import scala.concurrent.Promise import scala.meta.internal.builds.SbtBuildTool import scala.meta.internal.builds.SbtDigest +import scala.meta.internal.metals.CreateSession import scala.meta.internal.metals.Messages import scala.meta.internal.metals.Messages.ImportBuildChanges import scala.meta.internal.metals.MetalsEnrichments._ @@ -294,7 +295,7 @@ class SbtServerSuite |""".stripMargin ) _ = buffer.clear() - _ <- server.headServer.restartBspServer() + _ <- server.headServer.connect(CreateSession(shutdownBuildServer = true)) } yield { val logs = buffer.result() assert(logs.contains("sbt server started")) diff --git a/tests/unit/src/test/scala/tests/WorkspaceFoldersSuite.scala b/tests/unit/src/test/scala/tests/WorkspaceFoldersSuite.scala index de29a5fe7c4..b50fce462c0 100644 --- a/tests/unit/src/test/scala/tests/WorkspaceFoldersSuite.scala +++ b/tests/unit/src/test/scala/tests/WorkspaceFoldersSuite.scala @@ -81,6 +81,7 @@ class WorkspaceFoldersSuite _ = assert(server.fullServer.nonScalaProjects.length == 1) _ = writeLayout( s"""|$newScalaFile + |//> using scala ${V.scala213} |package a |object O { | val i: Int = "aaa" @@ -92,8 +93,9 @@ class WorkspaceFoldersSuite _ = assert(server.fullServer.folderServices.length == 2) _ = assertNoDiff( server.client.pathDiagnostics(s"notYetScalaProject$newScalaFile"), - s"""|notYetScalaProject$newScalaFile:3:15: error: Found: ("aaa" : String) - |Required: Int + s"""|notYetScalaProject/a/src/main/scala/A.scala:4:15: error: type mismatch; + | found : String("aaa") + | required: Int | val i: Int = "aaa" | ^^^^^ |""".stripMargin,