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 c662f3f7e0c..69699100fec 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala @@ -12,6 +12,7 @@ import scala.meta.internal.builds.SbtBuildTool import scala.meta.internal.builds.ShellRunner import scala.meta.internal.metals.BloopServers import scala.meta.internal.metals.BuildServerConnection +import scala.meta.internal.metals.ConnectionBspStatus import scala.meta.internal.metals.Messages import scala.meta.internal.metals.Messages.BspSwitch import scala.meta.internal.metals.MetalsEnrichments._ @@ -36,6 +37,7 @@ class BspConnector( bspConfigGenerator: BspConfigGenerator, currentConnection: () => Option[BuildServerConnection], restartBspServer: () => Future[Boolean], + bspStatus: ConnectionBspStatus, )(implicit ec: ExecutionContext) { /** @@ -83,6 +85,7 @@ class BspConnector( bspTraceRoot: AbsolutePath, addLivenessMonitor: Boolean, ): Future[Option[BuildServerConnection]] = { + def bspStatusOpt = Option.when(addLivenessMonitor)(bspStatus) scribe.info("Attempting to connect to the build server...") resolve() match { case ResolvedNone => @@ -94,7 +97,7 @@ class BspConnector( projectRoot, bspTraceRoot, userConfiguration, - addLivenessMonitor, + bspStatusOpt, ) .map(Some(_)) case ResolvedBspOne(details) @@ -118,7 +121,7 @@ class BspConnector( projectRoot, bspTraceRoot, details, - addLivenessMonitor, + bspStatusOpt, ) _ <- if (shouldReload) connection.workspaceReload() @@ -130,7 +133,7 @@ class BspConnector( case ResolvedBspOne(details) => tables.buildServers.chooseServer(details.getName()) bspServers - .newServer(projectRoot, bspTraceRoot, details, addLivenessMonitor) + .newServer(projectRoot, bspTraceRoot, details, bspStatusOpt) .map(Some(_)) case ResolvedMultiple(_, availableServers) => val distinctServers = availableServers @@ -167,7 +170,7 @@ class BspConnector( projectRoot, bspTraceRoot, item, - addLivenessMonitor, + bspStatusOpt, ) } yield Some(conn) } diff --git a/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala b/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala index 1f00eaf0bae..b3a688f1a8c 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala @@ -14,6 +14,7 @@ import scala.meta.internal.io.FileIO import scala.meta.internal.metals.BuildServerConnection import scala.meta.internal.metals.Cancelable import scala.meta.internal.metals.ClosableOutputStream +import scala.meta.internal.metals.ConnectionBspStatus import scala.meta.internal.metals.JdkSources import scala.meta.internal.metals.MetalsBuildClient import scala.meta.internal.metals.MetalsEnrichments._ @@ -69,7 +70,7 @@ final class BspServers( projectDirectory: AbsolutePath, bspTraceRoot: AbsolutePath, details: BspConnectionDetails, - addLivenessMonitor: Boolean, + bspStatusOpt: Option[ConnectionBspStatus], ): Future[BuildServerConnection] = { def newConnection(): Future[SocketConnection] = { @@ -143,7 +144,7 @@ final class BspServers( tables.dismissedNotifications.ReconnectBsp, config, details.getName(), - addLivenessMonitor, + bspStatusOpt, ) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala b/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala index 9be67e903c5..7da15777621 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala @@ -81,7 +81,7 @@ final class BloopServers( projectRoot: AbsolutePath, bspTraceRoot: AbsolutePath, userConfiguration: UserConfiguration, - addLivenessMonitor: Boolean, + bspStatusOpt: Option[ConnectionBspStatus], ): Future[BuildServerConnection] = { val bloopVersion = userConfiguration.currentBloopVersion BuildServerConnection @@ -95,7 +95,7 @@ final class BloopServers( tables.dismissedNotifications.ReconnectBsp, config, name, - addLivenessMonitor, + bspStatusOpt, ) } diff --git a/metals/src/main/scala/scala/meta/internal/metals/BspStatus.scala b/metals/src/main/scala/scala/meta/internal/metals/BspStatus.scala new file mode 100644 index 00000000000..546ea5d5d55 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/BspStatus.scala @@ -0,0 +1,36 @@ +package scala.meta.internal.metals + +import java.util.Collections +import java.util.concurrent.atomic.AtomicReference + +import scala.meta.internal.metals.clients.language.MetalsLanguageClient +import scala.meta.internal.metals.clients.language.MetalsStatusParams +import scala.meta.io.AbsolutePath + +class BspStatus(client: MetalsLanguageClient, isBspStatusProvider: Boolean) { + val focusedFolder: AtomicReference[Option[AbsolutePath]] = + new AtomicReference(None) + val messages: java.util.Map[AbsolutePath, MetalsStatusParams] = + Collections.synchronizedMap( + new java.util.HashMap[AbsolutePath, MetalsStatusParams] + ) + + def status(folder: AbsolutePath, params: MetalsStatusParams): Unit = { + messages.put(folder, params) + if (focusedFolder.get().isEmpty || focusedFolder.get().contains(folder)) { + client.metalsStatus(params) + } + } + + def focus(folder: AbsolutePath): Unit = { + if (isBspStatusProvider) { + val prev = focusedFolder.getAndSet(Some(folder)) + if (!prev.contains(folder)) { + client.metalsStatus( + messages.getOrDefault(folder, ConnectionBspStatus.disconnectedParams) + ) + } + } + } + +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala index 46f9364836b..ab6bac53cee 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala @@ -444,7 +444,7 @@ object BuildServerConnection { reconnectNotification: DismissedNotifications#Notification, config: MetalsServerConfig, serverName: String, - addLivenessMonitor: Boolean = false, + bspStatusOpt: Option[ConnectionBspStatus] = None, retry: Int = 5, supportsWrappedSources: Option[Boolean] = None, )(implicit @@ -454,12 +454,8 @@ object BuildServerConnection { def setupServer(): Future[LauncherConnection] = { connect().map { case conn @ SocketConnection(_, output, input, _, _) => val tracePrinter = Trace.setupTracePrinter("BSP", bspTraceRoot) - val bspStatusOpt = - if (addLivenessMonitor) - Some(new ConnectionBspStatus(languageClient, serverName, config.icons)) - else None val requestMonitorOpt = - bspStatusOpt.map(new RequestMonitorImpl(_)) + bspStatusOpt.map(new RequestMonitorImpl(_, serverName)) val wrapper: MessageConsumer => MessageConsumer = requestMonitorOpt.map(_.wrapper).getOrElse(identity) val launcher = @@ -497,6 +493,7 @@ object BuildServerConnection { config.metalsToIdleTime, config.pingInterval, bspStatus, + serverName, ) LauncherConnection( @@ -535,7 +532,7 @@ object BuildServerConnection { reconnectNotification, config, serverName, - addLivenessMonitor, + bspStatusOpt, retry - 1, ) } else { diff --git a/metals/src/main/scala/scala/meta/internal/metals/ConnectionBspStatus.scala b/metals/src/main/scala/scala/meta/internal/metals/ConnectionBspStatus.scala index e9bec2c11af..63f7d75690e 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ConnectionBspStatus.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ConnectionBspStatus.scala @@ -2,26 +2,31 @@ package scala.meta.internal.metals import java.util.concurrent.atomic.AtomicBoolean -import scala.meta.internal.metals.clients.language.MetalsLanguageClient import scala.meta.internal.metals.clients.language.MetalsStatusParams import scala.meta.internal.metals.clients.language.StatusType +import scala.meta.io.AbsolutePath class ConnectionBspStatus( - client: MetalsLanguageClient, - serverName: String, + bspStatus: BspStatus, + folderPath: AbsolutePath, icons: Icons, ) { private val isServerResponsive = new AtomicBoolean(false) + val status: MetalsStatusParams => Unit = bspStatus.status(folderPath, _) - def connected(): Unit = + def connected(serverName: String): Unit = if (isServerResponsive.compareAndSet(false, true)) - client.metalsStatus(ConnectionBspStatus.connectedParams(serverName, icons)) - def noResponse(): Unit = + status(ConnectionBspStatus.connectedParams(serverName, icons)) + def noResponse(serverName: String): Unit = if (isServerResponsive.compareAndSet(true, false)) { scribe.debug("server liveness monitor detected no response") - client.metalsStatus(ConnectionBspStatus.noResponseParams(serverName, icons)) + status(ConnectionBspStatus.noResponseParams(serverName, icons)) } - def disconnected(): Unit = client.metalsStatus(ConnectionBspStatus.disconnectedParams) + + def disconnected(): Unit = { + isServerResponsive.set(false) + status(ConnectionBspStatus.disconnectedParams) + } def isBuildServerResponsive: Boolean = isServerResponsive.get() } 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 8ba4ebc850d..57b5a32c1d5 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -130,6 +130,7 @@ class MetalsLspService( folder: AbsolutePath, folderVisibleName: Option[String], headDoctor: HeadDoctor, + bspStatus: BspStatus, ) extends Folder(folder, folderVisibleName, isKnownMetalsProject = true) with Cancelable with TextDocumentService { @@ -423,6 +424,9 @@ class MetalsLspService( clientConfig.initialConfig, ) + private val connectionBspStatus = + new ConnectionBspStatus(bspStatus, folder, clientConfig.icons()) + private val bspServers: BspServers = new BspServers( folder, charset, @@ -445,6 +449,7 @@ class MetalsLspService( bspConfigGenerator, () => bspSession.map(_.mainConnection), restartBspServer, + connectionBspStatus, ) private val workspaceSymbols: WorkspaceSymbolProvider = diff --git a/metals/src/main/scala/scala/meta/internal/metals/ServerLivenessMonitor.scala b/metals/src/main/scala/scala/meta/internal/metals/ServerLivenessMonitor.scala index 3653180622f..6ca9f9a5445 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ServerLivenessMonitor.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ServerLivenessMonitor.scala @@ -18,7 +18,8 @@ trait RequestMonitor { def lastIncoming: Option[Long] } -class RequestMonitorImpl(bspStatus: ConnectionBspStatus) extends RequestMonitor { +class RequestMonitorImpl(bspStatus: ConnectionBspStatus, serverName: String) + extends RequestMonitor { @volatile private var lastOutgoing_ : Option[Long] = None @volatile private var lastIncoming_ : Option[Long] = None @@ -40,7 +41,7 @@ class RequestMonitorImpl(bspStatus: ConnectionBspStatus) extends RequestMonitor private def outgoingMessage() = lastOutgoing_ = now private def incomingMessage(): Unit = { - bspStatus.connected() + bspStatus.connected(serverName) lastIncoming_ = now } private def now = Some(System.currentTimeMillis()) @@ -55,6 +56,7 @@ class ServerLivenessMonitor( metalsIdleInterval: Duration, pingInterval: Duration, bspStatus: ConnectionBspStatus, + serverName: String, ) { @volatile private var lastPing: Long = 0 val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1) @@ -70,7 +72,7 @@ class ServerLivenessMonitor( def notResponding = lastIncoming > (pingInterval.toMillis * 2) if (!metalsIsIdle) { if (lastPingOk && notResponding) { - bspStatus.noResponse() + bspStatus.noResponse(serverName) } scribe.debug("server liveness monitor: pinging build server...") lastPing = now 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 849674a6a39..fd4d08e26ef 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala @@ -22,6 +22,7 @@ import scala.meta.internal.metals.MetalsLspService import scala.meta.internal.metals.WindowStateDidChangeParams import scala.meta.internal.metals.clients.language.ConfiguredLanguageClient import scala.meta.internal.metals.clients.language.MetalsLanguageClient +import scala.meta.internal.metals.config.StatusBarState import scala.meta.internal.metals.debug.BuildTargetNotFoundException import scala.meta.internal.metals.debug.BuildTargetUndefinedException import scala.meta.internal.metals.debug.DebugProvider @@ -150,6 +151,18 @@ class WorkspaceLspService( languageClient, ) + private val bspStatus = new BspStatus( + languageClient, + isBspStatusProvider = clientConfig.bspStatusBarState() == StatusBarState.On, + ) + + def setFocusedDocument(newFocusedDocument: Option[AbsolutePath]): Unit = { + newFocusedDocument + .flatMap(getServiceForOpt) + .foreach(service => bspStatus.focus(service.path)) + focusedDocument = newFocusedDocument + } + def createService(folder: Folder): MetalsLspService = folder match { case Folder(uri, name) => @@ -168,6 +181,7 @@ class WorkspaceLspService( uri, name, doctor, + bspStatus, ) } @@ -318,7 +332,7 @@ class WorkspaceLspService( focusedDocument.foreach(recentlyFocusedFiles.add) val uri = params.getTextDocument.getUri val path = uri.toAbsolutePath - focusedDocument = Some(path) + setFocusedDocument(Some(path)) val service = getServiceForOpt(path) .orElse { if (path.filename.isScalaOrJavaFilename) { @@ -338,7 +352,7 @@ class WorkspaceLspService( override def didClose(params: DidCloseTextDocumentParams): Unit = { val path = params.getTextDocument.getUri.toAbsolutePath if (focusedDocument.contains(path)) { - focusedDocument = recentlyFocusedFiles.pollRecent() + setFocusedDocument(recentlyFocusedFiles.pollRecent()) } getServiceFor(params.getTextDocument().getUri()).didClose(params) } @@ -605,7 +619,7 @@ class WorkspaceLspService( } uriOpt match { case Some(uri) => - focusedDocument = Some(uri.toAbsolutePath) + setFocusedDocument(Some(uri.toAbsolutePath)) getServiceFor(uri).didFocus(uri) case None => CompletableFuture.completedFuture(DidFocusResult.NoBuildTarget) diff --git a/tests/unit/src/test/scala/tests/BspStatusSuite.scala b/tests/unit/src/test/scala/tests/BspStatusSuite.scala new file mode 100644 index 00000000000..b0525ef79e5 --- /dev/null +++ b/tests/unit/src/test/scala/tests/BspStatusSuite.scala @@ -0,0 +1,56 @@ +package tests + +import java.nio.file.Paths + +import scala.meta.internal.metals.BspStatus +import scala.meta.internal.metals.clients.language.MetalsStatusParams +import scala.meta.internal.metals.clients.language.NoopLanguageClient +import scala.meta.io.AbsolutePath + +class BspStatusSuite extends BaseSuite { + + test("basic") { + val client = new StatusClient + val bspStatus = new BspStatus(client, isBspStatusProvider = true) + val folder1 = AbsolutePath(Paths.get(".")).resolve("folder1") + val folder2 = AbsolutePath(Paths.get(".")).resolve("folder2") + bspStatus.status(folder1, new MetalsStatusParams("some text")) + assertEquals(client.status, "some text") + bspStatus.status(folder2, new MetalsStatusParams("other text")) + assertEquals(client.status, "other text") + bspStatus.focus(folder1) + assertEquals(client.status, "some text") + bspStatus.status(folder2, new MetalsStatusParams("some other other text")) + assertEquals(client.status, "some text") + bspStatus.focus(folder1) + assertEquals(client.status, "some text") + bspStatus.focus(folder2) + assertEquals(client.status, "some other other text") + } + + test("no-bsp-status") { + val client = new StatusClient + val bspStatus = new BspStatus(client, isBspStatusProvider = false) + val folder1 = AbsolutePath(Paths.get(".")).resolve("folder1") + val folder2 = AbsolutePath(Paths.get(".")).resolve("folder2") + bspStatus.status(folder1, new MetalsStatusParams("some text")) + assertEquals(client.status, "some text") + bspStatus.status(folder2, new MetalsStatusParams("other text")) + assertEquals(client.status, "other text") + bspStatus.focus(folder1) + assertEquals(client.status, "other text") + bspStatus.status(folder2, new MetalsStatusParams("some other other text")) + assertEquals(client.status, "some other other text") + bspStatus.focus(folder1) + assertEquals(client.status, "some other other text") + } + +} + +class StatusClient extends NoopLanguageClient { + + var status: String = "" + + override def metalsStatus(params: MetalsStatusParams): Unit = + status = params.text +} diff --git a/tests/unit/src/test/scala/tests/ServerLivenessMonitorSuite.scala b/tests/unit/src/test/scala/tests/ServerLivenessMonitorSuite.scala index e609b400e3b..04cd54ae0d0 100644 --- a/tests/unit/src/test/scala/tests/ServerLivenessMonitorSuite.scala +++ b/tests/unit/src/test/scala/tests/ServerLivenessMonitorSuite.scala @@ -1,5 +1,6 @@ package tests +import java.nio.file.Paths import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicReference @@ -8,12 +9,14 @@ import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContextExecutorService import scala.concurrent.duration.Duration +import scala.meta.internal.metals.BspStatus import scala.meta.internal.metals.ConnectionBspStatus import scala.meta.internal.metals.Icons import scala.meta.internal.metals.RequestMonitor import scala.meta.internal.metals.ServerLivenessMonitor import scala.meta.internal.metals.clients.language.MetalsStatusParams import scala.meta.internal.metals.clients.language.NoopLanguageClient +import scala.meta.io.AbsolutePath class ServerLivenessMonitorSuite extends BaseSuite { implicit val ex: ExecutionContextExecutorService = @@ -23,15 +26,21 @@ class ServerLivenessMonitorSuite extends BaseSuite { val pingInterval = Duration("3s") val server = new ResponsiveServer(pingInterval) val client = new CountMessageRequestsClient - val bspStatus = new ConnectionBspStatus(client, "responsive-server", Icons.default) + val bspStatus = new BspStatus(client, isBspStatusProvider = true) + val connectionBspStatus = new ConnectionBspStatus( + bspStatus, + AbsolutePath(Paths.get(".")), + Icons.default, + ) val livenessMonitor = new ServerLivenessMonitor( server, () => server.sendRequest(true), metalsIdleInterval = pingInterval * 4, pingInterval, - bspStatus, + connectionBspStatus, + "responsive-server", ) - bspStatus.connected() + connectionBspStatus.connected("responsive-server") Thread.sleep(pingInterval.toMillis * 3 / 2) assert(livenessMonitor.metalsIsIdle) server.sendRequest(false) @@ -89,7 +98,10 @@ class CountMessageRequestsClient extends NoopLanguageClient { override def metalsStatus(params: MetalsStatusParams): Unit = if ( - params == ConnectionBspStatus.noResponseParams("responsive-server", Icons.default) + params == ConnectionBspStatus.noResponseParams( + "responsive-server", + Icons.default, + ) ) { showMessageRequests += 1 }