diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index d478402a9d6..823bad7c85d 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -21,7 +21,10 @@ use node_runtime::NodeRuntime; use project::{ ProjectPath, debugger::session::ThreadId, - lsp_store::{FormatTrigger, LspFormatTarget}, + lsp_store::{ + FormatTrigger, LspFormatTarget, + log_store::{self, GlobalLogStore}, + }, trusted_worktrees::{PathTrust, TrustedWorktrees}, }; use remote::RemoteClient; @@ -836,6 +839,150 @@ async fn test_ssh_collaboration_formatting_with_prettier( ); } +#[gpui::test(iterations = 10)] +async fn test_ssh_restarting_language_server_replaces_remote_status( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + server_cx: &mut TestAppContext, +) { + cx_a.set_name("a"); + server_cx.set_name("server"); + + cx_a.update(|cx| { + release_channel::init(semver::Version::new(0, 0, 0), cx); + }); + server_cx.update(|cx| { + release_channel::init(semver::Version::new(0, 0, 0), cx); + }); + + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let log_store = cx_a.update(|cx| log_store::init(false, cx)); + + let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx); + let remote_fs = FakeFs::new(server_cx.executor()); + remote_fs + .insert_tree(path!("/project"), json!({ "a.rs": "fn main() {}" })) + .await; + + client_a.language_registry().add(rust_lang()); + + server_cx.update(HeadlessProject::init); + let languages = Arc::new(LanguageRegistry::new(server_cx.executor())); + languages.add(rust_lang()); + let mut fake_language_servers = languages.register_fake_lsp( + "Rust", + FakeLspAdapter { + name: "the-language-server", + ..Default::default() + }, + ); + let _headless_project = server_cx.new(|cx| { + HeadlessProject::new( + HeadlessAppState { + session: server_ssh, + fs: remote_fs.clone(), + http_client: Arc::new(BlockedHttpClient), + node_runtime: NodeRuntime::unavailable(), + languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), + startup_time: std::time::Instant::now(), + }, + false, + cx, + ) + }); + + let client_ssh = RemoteClient::connect_mock(opts, cx_a).await; + let (project_a, worktree_id) = client_a + .build_ssh_project(path!("/project"), client_ssh, false, cx_a) + .await; + log_store.update(cx_a, |log_store, cx| log_store.add_project(&project_a, cx)); + + let (buffer, _handle) = project_a + .update(cx_a, |project, cx| { + project.open_buffer_with_lsp((worktree_id, rel_path("a.rs")), cx) + }) + .await + .unwrap(); + + let first_server = fake_language_servers.next().await.unwrap(); + let first_server_id = first_server.server.server_id(); + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, cx| { + let statuses = project.language_server_statuses(cx).collect::>(); + assert_eq!(statuses.len(), 1); + assert_eq!(statuses[0].0, first_server_id); + assert_eq!(statuses[0].1.name.0, "the-language-server"); + }); + cx_a.read_global::(|global, cx| { + let log_store = global.0.read(cx); + let matching_server_ids = log_store + .language_servers + .iter() + .filter_map(|(server_id, state)| { + state + .name + .as_ref() + .is_some_and(|name| name.0 == "the-language-server") + .then_some(*server_id) + }) + .collect::>(); + assert_eq!(matching_server_ids, vec![first_server_id]); + }); + + project_a.update(cx_a, |project, cx| { + project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), cx); + }); + + let restarted_server = fake_language_servers.next().await.unwrap(); + let restarted_server_id = restarted_server.server.server_id(); + assert_ne!(restarted_server_id, first_server_id); + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, cx| { + let statuses = project.language_server_statuses(cx).collect::>(); + assert_eq!( + statuses.len(), + 1, + "restarting a remote language server should replace the previous status entry" + ); + assert_eq!( + statuses[0].0, restarted_server_id, + "restarting a remote language server should publish the replacement server id" + ); + assert_ne!( + statuses[0].0, first_server_id, + "restarting a remote language server should remove the previous server id" + ); + assert_eq!(statuses[0].1.name.0, "the-language-server"); + }); + cx_a.read_global::(|global, cx| { + let log_store = global.0.read(cx); + let matching_server_ids = log_store + .language_servers + .iter() + .filter_map(|(server_id, state)| { + state + .name + .as_ref() + .is_some_and(|name| name.0 == "the-language-server") + .then_some(*server_id) + }) + .collect::>(); + assert_eq!( + matching_server_ids, + vec![restarted_server_id], + "restarting a remote language server should replace the old log store entry" + ); + assert!( + !log_store.language_servers.contains_key(&first_server_id), + "restarting a remote language server should remove the previous log store entry" + ); + }); +} + #[gpui::test] async fn test_remote_server_debugger( cx_a: &mut TestAppContext, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 01aab2be7ac..3fd51117d54 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -9805,6 +9805,15 @@ impl LspStore { lsp_store.disk_based_diagnostics_finished(language_server_id, cx) } + proto::update_language_server::Variant::Removed(_) => { + lsp_store + .language_server_statuses + .remove(&language_server_id); + lsp_store.cleanup_lsp_data(language_server_id); + cx.emit(LspStoreEvent::LanguageServerRemoved(language_server_id)); + cx.notify(); + } + non_lsp @ proto::update_language_server::Variant::StatusUpdate(_) | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) | non_lsp @ proto::update_language_server::Variant::MetadataUpdated(_) => { diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 813f9e9ec65..dfd2d967117 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -577,6 +577,7 @@ message UpdateLanguageServer { StatusUpdate status_update = 9; RegisteredForBuffer registered_for_buffer = 10; ServerMetadataUpdated metadata_updated = 11; + ServerRemoved removed = 12; } } @@ -613,6 +614,8 @@ message LspDiskBasedDiagnosticsUpdating {} message LspDiskBasedDiagnosticsUpdated {} +message ServerRemoved {} + message StatusUpdate { optional string message = 1; oneof status { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 7b0fc0356a1..1b7eb82aac5 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -416,6 +416,16 @@ impl HeadlessProject { log_store.remove_language_server(*id, cx); }); } + self.session + .send(proto::UpdateLanguageServer { + project_id: REMOTE_SERVER_PROJECT_ID, + server_name: None, + language_server_id: id.to_proto(), + variant: Some(proto::update_language_server::Variant::Removed( + proto::ServerRemoved {}, + )), + }) + .log_err(); } LspStoreEvent::LanguageServerUpdate { language_server_id,