diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index dccc0bcafc..d3a3458876 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -7,8 +7,7 @@ use gpui::{ SharedString, Styled, Task, Window, actions, }; use language::{ - BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName, - LanguageServerStatusUpdate, ServerHealth, + BinaryStatus, LanguageServerId, LanguageServerName, LanguageServerStatusUpdate, ServerHealth, }; use project::{ LanguageServerProgress, LspStoreEvent, ProgressToken, Project, ProjectEnvironmentEvent, @@ -91,28 +90,11 @@ struct Content { impl ActivityIndicator { pub fn new( workspace: &mut Workspace, - languages: Arc, window: &mut Window, cx: &mut Context, ) -> Entity { let project = workspace.project().clone(); let this = cx.new(|cx| { - let mut status_events = languages.language_server_binary_statuses(); - cx.spawn(async move |this, cx| { - while let Some((name, binary_status)) = status_events.next().await { - this.update(cx, |this: &mut ActivityIndicator, cx| { - this.statuses.retain(|s| s.name != name); - this.statuses.push(ServerStatus { - name, - status: LanguageServerStatusUpdate::Binary(binary_status), - }); - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach(); - let fs = project.read(cx).fs().clone(); let mut job_events = fs.subscribe_to_jobs(); cx.spawn(async move |this, cx| { diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index d039bce3b3..06989f8136 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -927,12 +927,12 @@ async fn test_ssh_restarting_language_server_replaces_remote_status( let matching_server_ids = log_store .language_servers .iter() - .filter_map(|(server_id, state)| { + .filter_map(|(key, state)| { state .name .as_ref() .is_some_and(|name| name.0 == "the-language-server") - .then_some(*server_id) + .then_some(key.server_id) }) .collect::>(); assert_eq!(matching_server_ids, vec![first_server_id]); @@ -958,10 +958,6 @@ async fn test_ssh_restarting_language_server_replaces_remote_status( 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| { @@ -969,12 +965,12 @@ async fn test_ssh_restarting_language_server_replaces_remote_status( let matching_server_ids = log_store .language_servers .iter() - .filter_map(|(server_id, state)| { + .filter_map(|(key, state)| { state .name .as_ref() .is_some_and(|name| name.0 == "the-language-server") - .then_some(*server_id) + .then_some(key.server_id) }) .collect::>(); assert_eq!( @@ -982,10 +978,6 @@ async fn test_ssh_restarting_language_server_replaces_remote_status( 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" - ); }); } diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index 850da7a69e..ceffef8efe 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use ::lsp::LanguageServerName; use anyhow::{Context as _, Result, bail}; use async_trait::async_trait; -use gpui::{App, Task}; +use gpui::{App, EntityId, Task}; use language::LanguageName; use semver::Version; use task::{SpawnInTerminal, ZedDebugConfig}; @@ -64,6 +64,7 @@ pub trait Extension: Send + Sync + 'static { language_server_id: LanguageServerName, language_name: LanguageName, worktree: Arc, + language_server_status_source: EntityId, ) -> Result; async fn language_server_initialization_options( @@ -71,24 +72,28 @@ pub trait Extension: Send + Sync + 'static { language_server_id: LanguageServerName, language_name: LanguageName, worktree: Arc, + language_server_status_source: EntityId, ) -> Result>; async fn language_server_workspace_configuration( &self, language_server_id: LanguageServerName, worktree: Arc, + language_server_status_source: EntityId, ) -> Result>; async fn language_server_initialization_options_schema( &self, language_server_id: LanguageServerName, worktree: Arc, + language_server_status_source: EntityId, ) -> Result>; async fn language_server_workspace_configuration_schema( &self, language_server_id: LanguageServerName, worktree: Arc, + language_server_status_source: EntityId, ) -> Result>; async fn language_server_additional_initialization_options( @@ -96,6 +101,7 @@ pub trait Extension: Send + Sync + 'static { language_server_id: LanguageServerName, target_language_server_id: LanguageServerName, worktree: Arc, + language_server_status_source: EntityId, ) -> Result>; async fn language_server_additional_workspace_configuration( @@ -103,6 +109,7 @@ pub trait Extension: Send + Sync + 'static { language_server_id: LanguageServerName, target_language_server_id: LanguageServerName, worktree: Arc, + language_server_status_source: EntityId, ) -> Result>; async fn labels_for_completions( diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 6190e86294..9050017091 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Result; use fs::Fs; -use gpui::{App, Global, ReadGlobal, SharedString, Task}; +use gpui::{App, EntityId, Global, ReadGlobal, SharedString, Task}; use language::{BinaryStatus, LanguageLoader, LanguageMatcher, LanguageName}; use lsp::LanguageServerName; use parking_lot::RwLock; @@ -301,6 +301,7 @@ pub trait ExtensionLanguageServerProxy: Send + Sync + 'static { fn update_language_server_status( &self, + source: Option, language_server_id: LanguageServerName, status: BinaryStatus, ); @@ -335,6 +336,7 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy { fn update_language_server_status( &self, + source: Option, language_server_id: LanguageServerName, status: BinaryStatus, ) { @@ -342,7 +344,7 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy { return; }; - proxy.update_language_server_status(language_server_id, status) + proxy.update_language_server_status(source, language_server_id, status) } } diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index e39a57fb1a..ad6679e8fe 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -22,7 +22,7 @@ use extension::{ }; use fs::{FakeFs, Fs, RealFs, RemoveOptions}; use futures::{AsyncReadExt, FutureExt, StreamExt, io::BufReader}; -use gpui::{AppContext as _, BackgroundExecutor, Entity, TaskExt, TestAppContext}; +use gpui::{AppContext as _, BackgroundExecutor, Entity, EntityId, TaskExt, TestAppContext}; use http_client::{FakeHttpClient, Response}; use language::{ BinaryStatus, LanguageConfig, LanguageMatcher, LanguageName, LanguageRegistry, QueryFiles, @@ -826,6 +826,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); + let lsp_store_id = project.read_with(cx, |project, _| project.lsp_store().entity_id()); language_extension::init( LspAccess::ViaLspStore( project @@ -837,7 +838,8 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { ); let node_runtime = NodeRuntime::unavailable(); - let mut status_updates = language_registry.language_server_binary_statuses(); + let (mut status_updates, _status_updates_subscription) = + language_registry.language_server_binary_statuses(); struct FakeLanguageServerVersion { version: String, @@ -1093,18 +1095,25 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { ], [ ( + Some(lsp_store_id), LanguageServerName::new_static("gleam"), BinaryStatus::Starting ), ( + Some(lsp_store_id), LanguageServerName::new_static("gleam"), BinaryStatus::CheckingForUpdate ), ( + Some(lsp_store_id), LanguageServerName::new_static("gleam"), BinaryStatus::Downloading ), - (LanguageServerName::new_static("gleam"), BinaryStatus::None) + ( + Some(lsp_store_id), + LanguageServerName::new_static("gleam"), + BinaryStatus::None + ) ] ); @@ -4107,6 +4116,7 @@ impl Extension for FakeExtension { _language_server_id: LanguageServerName, _language_name: LanguageName, _worktree: Arc, + _language_server_status_source: EntityId, ) -> anyhow::Result { anyhow::bail!("not supported by FakeExtension") } @@ -4116,6 +4126,7 @@ impl Extension for FakeExtension { _language_server_id: LanguageServerName, _language_name: LanguageName, _worktree: Arc, + _language_server_status_source: EntityId, ) -> anyhow::Result> { anyhow::bail!("not supported by FakeExtension") } @@ -4124,6 +4135,7 @@ impl Extension for FakeExtension { &self, _language_server_id: LanguageServerName, _worktree: Arc, + _language_server_status_source: EntityId, ) -> anyhow::Result> { anyhow::bail!("not supported by FakeExtension") } @@ -4132,6 +4144,7 @@ impl Extension for FakeExtension { &self, _language_server_id: LanguageServerName, _worktree: Arc, + _language_server_status_source: EntityId, ) -> anyhow::Result> { anyhow::bail!("not supported by FakeExtension") } @@ -4140,6 +4153,7 @@ impl Extension for FakeExtension { &self, _language_server_id: LanguageServerName, _worktree: Arc, + _language_server_status_source: EntityId, ) -> anyhow::Result> { anyhow::bail!("not supported by FakeExtension") } @@ -4149,6 +4163,7 @@ impl Extension for FakeExtension { _language_server_id: LanguageServerName, _target_language_server_id: LanguageServerName, _worktree: Arc, + _language_server_status_source: EntityId, ) -> anyhow::Result> { anyhow::bail!("not supported by FakeExtension") } @@ -4158,6 +4173,7 @@ impl Extension for FakeExtension { _language_server_id: LanguageServerName, _target_language_server_id: LanguageServerName, _worktree: Arc, + _language_server_status_source: EntityId, ) -> anyhow::Result> { anyhow::bail!("not supported by FakeExtension") } diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index c04445f4dc..857d33e0de 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -21,7 +21,7 @@ use futures::{ }, future::BoxFuture, }; -use gpui::{App, AsyncApp, BackgroundExecutor, Task}; +use gpui::{App, AsyncApp, BackgroundExecutor, EntityId, Task}; use http_client::HttpClient; use language::LanguageName; use lsp::LanguageServerName; @@ -90,8 +90,9 @@ impl extension::Extension for WasmExtension { language_server_id: LanguageServerName, language_name: LanguageName, worktree: Arc, + status_source: EntityId, ) -> Result { - self.call(|extension, store| { + self.call_with_language_server_status_source(status_source, move |extension, store| { async move { let resource = store.data_mut().table.push(worktree)?; let command = extension @@ -116,8 +117,9 @@ impl extension::Extension for WasmExtension { language_server_id: LanguageServerName, language_name: LanguageName, worktree: Arc, + status_source: EntityId, ) -> Result> { - self.call(|extension, store| { + self.call_with_language_server_status_source(status_source, move |extension, store| { async move { let resource = store.data_mut().table.push(worktree)?; let options = extension @@ -140,8 +142,9 @@ impl extension::Extension for WasmExtension { &self, language_server_id: LanguageServerName, worktree: Arc, + status_source: EntityId, ) -> Result> { - self.call(|extension, store| { + self.call_with_language_server_status_source(status_source, move |extension, store| { async move { let resource = store.data_mut().table.push(worktree)?; let options = extension @@ -163,8 +166,9 @@ impl extension::Extension for WasmExtension { &self, language_server_id: LanguageServerName, worktree: Arc, + status_source: EntityId, ) -> Result> { - self.call(|extension, store| { + self.call_with_language_server_status_source(status_source, move |extension, store| { async move { let resource = store.data_mut().table.push(worktree)?; extension @@ -185,8 +189,9 @@ impl extension::Extension for WasmExtension { &self, language_server_id: LanguageServerName, worktree: Arc, + status_source: EntityId, ) -> Result> { - self.call(|extension, store| { + self.call_with_language_server_status_source(status_source, move |extension, store| { async move { let resource = store.data_mut().table.push(worktree)?; extension @@ -208,8 +213,9 @@ impl extension::Extension for WasmExtension { language_server_id: LanguageServerName, target_language_server_id: LanguageServerName, worktree: Arc, + status_source: EntityId, ) -> Result> { - self.call(|extension, store| { + self.call_with_language_server_status_source(status_source, move |extension, store| { async move { let resource = store.data_mut().table.push(worktree)?; let options = extension @@ -233,8 +239,9 @@ impl extension::Extension for WasmExtension { language_server_id: LanguageServerName, target_language_server_id: LanguageServerName, worktree: Arc, + status_source: EntityId, ) -> Result> { - self.call(|extension, store| { + self.call_with_language_server_status_source(status_source, move |extension, store| { async move { let resource = store.data_mut().table.push(worktree)?; let options = extension @@ -538,6 +545,7 @@ pub struct WasmState { ctx: WasiCtx, pub host: Arc, pub(crate) capability_granter: CapabilityGranter, + pub(crate) language_server_status_source: Option, } type MainThreadCall = Box FnOnce(&'a mut AsyncApp) -> LocalBoxFuture<'a, ()>>; @@ -671,6 +679,7 @@ impl WasmHost { this.granted_capabilities.clone(), manifest.clone(), ), + language_server_status_source: None, }, ); // Store will yield after 1 tick, and get a new deadline of 1 tick after each yield. @@ -872,6 +881,34 @@ impl WasmExtension { .with_context(|| format!("loading wasm extension: {}", manifest.id)) } + async fn call_with_language_server_status_source( + &self, + source: EntityId, + f: Fn, + ) -> Result + where + T: 'static + Send, + Fn: 'static + + Send + + for<'a> FnOnce(&'a mut Extension, &'a mut Store) -> BoxFuture<'a, T>, + { + self.call(move |extension, store| { + async move { + debug_assert!(store.data().language_server_status_source.is_none()); + + // The installation-status WIT methods do not receive a worktree, so expose the + // source through WasmState for the duration of this serialized extension call. + // Clear it before returning so failed calls cannot affect the next invocation. + store.data_mut().language_server_status_source = Some(source); + let result = f(extension, store).await; + store.data_mut().language_server_status_source = None; + result + } + .boxed() + }) + .await + } + pub async fn call(&self, f: Fn) -> Result where T: 'static + Send, diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs index 57dfa5b5af..582e78f813 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs @@ -148,9 +148,11 @@ impl ExtensionImports for WasmState { LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, }; - self.host - .proxy - .update_language_server_status(lsp::LanguageServerName(server_name.into()), status); + self.host.proxy.update_language_server_status( + self.language_server_status_source, + lsp::LanguageServerName(server_name.into()), + status, + ); Ok(()) } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index f2b9adf5cf..d7df044773 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -507,9 +507,11 @@ impl ExtensionImports for WasmState { LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, }; - self.host - .proxy - .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); + self.host.proxy.update_language_server_status( + self.language_server_status_source, + ::lsp::LanguageServerName(server_name.into()), + status, + ); Ok(()) } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 4fed1b313a..57d5e808f1 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -1064,9 +1064,11 @@ impl ExtensionImports for WasmState { LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, }; - self.host - .proxy - .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); + self.host.proxy.update_language_server_status( + self.language_server_status_source, + ::lsp::LanguageServerName(server_name.into()), + status, + ); Ok(()) } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8258034447..a7791c51e7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -39,7 +39,7 @@ use collections::{HashMap, HashSet}; use futures::Future; use futures::future::LocalBoxFuture; use futures::lock::OwnedMutexGuard; -use gpui::{App, AsyncApp, Entity}; +use gpui::{App, AsyncApp, Entity, EntityId}; use http_client::HttpClient; pub use language_core::{ @@ -492,6 +492,7 @@ pub trait LspAdapterDelegate: Send + Sync { fn worktree_id(&self) -> WorktreeId; fn worktree_root_path(&self) -> &Path; fn resolve_relative_path(&self, path: PathBuf) -> PathBuf; + fn status_source_id(&self) -> EntityId; fn update_status(&self, language: LanguageServerName, status: BinaryStatus); fn registered_lsp_adapters(&self) -> Vec>; async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option>; diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 22561ee834..4a3accb9d1 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -19,7 +19,7 @@ use futures::{ future::{BoxFuture, FutureExt as _}, }; use globset::GlobSet; -use gpui::{App, BackgroundExecutor}; +use gpui::{App, BackgroundExecutor, EntityId, Subscription}; use lsp::LanguageServerId; use parking_lot::{Mutex, RwLock}; use postage::watch; @@ -89,9 +89,17 @@ impl std::fmt::Display for LanguageNotFound { } } +type ServerStatus = (Option, LanguageServerName, BinaryStatus); + #[derive(Clone, Default)] struct ServerStatusSender { - txs: Arc>>>, + state: Arc>, +} + +#[derive(Default)] +struct ServerStatusSenderState { + next_subscription_id: usize, + txs: HashMap>, } pub struct LoadedLanguage { @@ -922,7 +930,17 @@ impl LanguageRegistry { } pub fn update_lsp_binary_status(&self, server_name: LanguageServerName, status: BinaryStatus) { - self.lsp_binary_status_tx.send(server_name, status); + self.lsp_binary_status_tx.send(None, server_name, status); + } + + pub fn update_lsp_binary_status_for_entity( + &self, + source: EntityId, + server_name: LanguageServerName, + status: BinaryStatus, + ) { + self.lsp_binary_status_tx + .send(Some(source), server_name, status); } pub fn next_language_server_id(&self) -> LanguageServerId { @@ -968,7 +986,10 @@ impl LanguageRegistry { pub fn language_server_binary_statuses( &self, - ) -> mpsc::UnboundedReceiver<(LanguageServerName, BinaryStatus)> { + ) -> ( + mpsc::UnboundedReceiver<(Option, LanguageServerName, BinaryStatus)>, + Subscription, + ) { self.lsp_binary_status_tx.subscribe() } } @@ -1065,14 +1086,41 @@ impl LanguageRegistryState { } impl ServerStatusSender { - fn subscribe(&self) -> mpsc::UnboundedReceiver<(LanguageServerName, BinaryStatus)> { + fn subscribe(&self) -> (mpsc::UnboundedReceiver, Subscription) { let (tx, rx) = mpsc::unbounded(); - self.txs.lock().push(tx); - rx + let subscription_id = { + let mut state = self.state.lock(); + let subscription_id = post_inc(&mut state.next_subscription_id); + state.txs.insert(subscription_id, tx); + subscription_id + }; + let state = self.state.clone(); + let subscription = Subscription::new(move || { + state.lock().txs.remove(&subscription_id); + }); + (rx, subscription) } - fn send(&self, name: LanguageServerName, status: BinaryStatus) { - let mut txs = self.txs.lock(); - txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok()); + fn send(&self, source: Option, name: LanguageServerName, status: BinaryStatus) { + let mut state = self.state.lock(); + state.txs.retain(|_, tx| { + tx.unbounded_send((source, name.clone(), status.clone())) + .is_ok() + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dropping_server_status_subscription_unregisters_sender() { + let sender = ServerStatusSender::default(); + let (_receiver, subscription) = sender.subscribe(); + assert_eq!(sender.state.lock().txs.len(), 1); + + drop(subscription); + assert!(sender.state.lock().txs.is_empty()); } } diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index d9f28100d8..378e82a160 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -122,6 +122,7 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy { fn update_language_server_status( &self, + source: Option, language_server_id: LanguageServerName, status: BinaryStatus, ) { @@ -130,8 +131,16 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy { language_server_id, status ); - self.language_registry - .update_lsp_binary_status(language_server_id, status); + if let Some(source) = source { + self.language_registry.update_lsp_binary_status_for_entity( + source, + language_server_id, + status, + ); + } else { + self.language_registry + .update_lsp_binary_status(language_server_id, status); + } } } @@ -167,6 +176,7 @@ impl DynLspInstaller for ExtensionLspAdapter { ) -> LanguageServerBinaryLocations { async move { let ret = maybe!(async move { + let language_server_status_source = delegate.status_source_id(); let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; let command = self .extension @@ -174,6 +184,7 @@ impl DynLspInstaller for ExtensionLspAdapter { self.language_server_id.clone(), self.language_name.clone(), delegate, + language_server_status_source, ) .await?; @@ -314,6 +325,7 @@ impl LspAdapter for ExtensionLspAdapter { delegate: &Arc, _: &mut AsyncApp, ) -> Result> { + let language_server_status_source = delegate.status_source_id(); let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; let json_options = self .extension @@ -321,6 +333,7 @@ impl LspAdapter for ExtensionLspAdapter { self.language_server_id.clone(), self.language_name.clone(), delegate, + language_server_status_source, ) .await?; Ok(if let Some(json_options) = json_options { @@ -339,10 +352,15 @@ impl LspAdapter for ExtensionLspAdapter { _: Option, _cx: &mut AsyncApp, ) -> Result { + let language_server_status_source = delegate.status_source_id(); let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; let json_options: Option = self .extension - .language_server_workspace_configuration(self.language_server_id.clone(), delegate) + .language_server_workspace_configuration( + self.language_server_id.clone(), + delegate, + language_server_status_source, + ) .await?; Ok(if let Some(json_options) = json_options { serde_json::from_str(&json_options).with_context(|| { @@ -359,12 +377,14 @@ impl LspAdapter for ExtensionLspAdapter { _cached_binary: OwnedMutexGuard>, _cx: &mut AsyncApp, ) -> Option { + let language_server_status_source = delegate.status_source_id(); let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; let json_schema: Option = self .extension .language_server_initialization_options_schema( self.language_server_id.clone(), delegate, + language_server_status_source, ) .await .ok() @@ -378,12 +398,14 @@ impl LspAdapter for ExtensionLspAdapter { _cached_binary: OwnedMutexGuard>, _cx: &mut AsyncApp, ) -> Option { + let language_server_status_source = delegate.status_source_id(); let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; let json_schema: Option = self .extension .language_server_workspace_configuration_schema( self.language_server_id.clone(), delegate, + language_server_status_source, ) .await .ok() @@ -396,6 +418,7 @@ impl LspAdapter for ExtensionLspAdapter { target_language_server_id: LanguageServerName, delegate: &Arc, ) -> Result> { + let language_server_status_source = delegate.status_source_id(); let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; let json_options: Option = self .extension @@ -403,6 +426,7 @@ impl LspAdapter for ExtensionLspAdapter { self.language_server_id.clone(), target_language_server_id.clone(), delegate, + language_server_status_source, ) .await?; Ok(if let Some(json_options) = json_options { @@ -424,6 +448,7 @@ impl LspAdapter for ExtensionLspAdapter { _cx: &mut AsyncApp, ) -> Result> { + let language_server_status_source = delegate.status_source_id(); let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; let json_options: Option = self .extension @@ -431,6 +456,7 @@ impl LspAdapter for ExtensionLspAdapter { self.language_server_id.clone(), target_language_server_id.clone(), delegate, + language_server_status_source, ) .await?; Ok(if let Some(json_options) = json_options { diff --git a/crates/language_tools/src/lsp_button.rs b/crates/language_tools/src/lsp_button.rs index f5c3c2e28f..49bd5053dd 100644 --- a/crates/language_tools/src/lsp_button.rs +++ b/crates/language_tools/src/lsp_button.rs @@ -346,7 +346,15 @@ impl LanguageServerState { .lsp_store .update(cx, |lsp_store, _| lsp_store.as_remote().is_some()) .unwrap_or(false); - let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector); + let has_logs = is_remote + || self.workspace.upgrade().is_some_and(|workspace| { + let project = workspace.read(cx).project(); + lsp_logs.read(cx).has_server_logs( + &server_selector, + &project.downgrade(), + &self.lsp_store, + ) + }); let (status_color, status_label) = server_info .binary_status diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index ace4a0da96..338efb35e3 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -13,7 +13,9 @@ use lsp::{ }; use project::{ LanguageServerStatus, Project, - lsp_store::log_store::{self, Event, LanguageServerKind, LogKind, LogStore, Message}, + lsp_store::log_store::{ + self, Event, LanguageServerKind, LanguageServerLogKey, LogKind, LogStore, Message, + }, search::SearchQuery, }; use proto::toggle_lsp_logs::LogType; @@ -44,6 +46,9 @@ pub fn open( workspace .update_in(cx, |workspace, window, cx| { let project = workspace.project().clone(); + let weak_project = project.downgrade(); + let project_is_local = project.read(cx).is_local(); + let weak_lsp_store = project.read(cx).lsp_store().downgrade(); let tool_log_store = log_store.clone(); let log_view = get_or_create_tool( workspace, @@ -53,22 +58,35 @@ pub fn open( move |window, cx| LspLogView::new(project, tool_log_store, window, cx), ); log_view.update(cx, |log_view, cx| { - let server_id = match server { - LanguageServerSelector::Id(id) => Some(id), - LanguageServerSelector::Name(name) => { - log_store.read(cx).language_servers.iter().find_map( - |(id, state)| { - if state.name.as_ref() == Some(&name) { - Some(*id) - } else { - None - } - }, - ) - } + let key_priority = |key: &LanguageServerLogKey| match &key.kind { + LanguageServerKind::Local { .. } if project_is_local => 0, + LanguageServerKind::Remote { .. } if !project_is_local => 0, + _ => 1, }; - if let Some(server_id) = server_id { - log_view.show_logs_for_server(server_id, window, cx); + let server_key = match server { + LanguageServerSelector::Id(id) => log_store + .read(cx) + .language_servers + .keys() + .filter(|key| { + key.server_id == id + && key.is_for_project(&weak_project, &weak_lsp_store) + }) + .min_by_key(|key| key_priority(key)) + .cloned(), + LanguageServerSelector::Name(name) => log_store + .read(cx) + .language_servers + .iter() + .filter(|(key, state)| { + key.is_for_project(&weak_project, &weak_lsp_store) + && state.name.as_ref() == Some(&name) + }) + .min_by_key(|(key, _)| key_priority(key)) + .map(|(key, _)| key.clone()), + }; + if let Some(server_key) = server_key { + log_view.show_logs_for_server(server_key, window, cx); } }); }) @@ -82,7 +100,7 @@ pub struct LspLogView { pub(crate) editor: Entity, editor_subscriptions: Vec, log_store: Entity, - current_server_id: Option, + current_server_key: Option, active_entry_kind: LogKind, project: Entity, focus_handle: FocusHandle, @@ -105,6 +123,12 @@ pub(crate) struct LogMenuItem { pub server_kind: LanguageServerKind, } +impl LogMenuItem { + fn key(&self) -> LanguageServerLogKey { + LanguageServerLogKey::new(self.server_kind.clone(), self.server_id) + } +} + actions!( dev, [ @@ -144,35 +168,38 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) -> Self { - let server_id = log_store - .read(cx) - .language_servers - .iter() - .find(|(_, server)| server.kind.project() == Some(&project.downgrade())) - .map(|(id, _)| *id); - let weak_project = project.downgrade(); + let weak_lsp_store = project.read(cx).lsp_store().downgrade(); + let server_key = log_store + .read(cx) + .server_keys_for_project(&weak_project, &weak_lsp_store) + .next(); let model_changes_subscription = cx.observe_in(&log_store, window, move |this, store, window, cx| { - let first_server_id_for_project = - store.read(cx).server_ids_for_project(&weak_project).next(); - if let Some(current_lsp) = this.current_server_id { - if !store.read(cx).language_servers.contains_key(¤t_lsp) - && let Some(server_id) = first_server_id_for_project + let first_server_key_for_project = store + .read(cx) + .server_keys_for_project(&weak_project, &weak_lsp_store) + .next(); + if let Some(current_key) = this.current_server_key.as_ref() { + let current_server_is_for_project = + store.read(cx).language_servers.contains_key(current_key) + && current_key.is_for_project(&weak_project, &weak_lsp_store); + if !current_server_is_for_project + && let Some(server_key) = first_server_key_for_project { match this.active_entry_kind { - LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx), - LogKind::Trace => this.show_trace_for_server(server_id, window, cx), - LogKind::Logs => this.show_logs_for_server(server_id, window, cx), - LogKind::ServerInfo => this.show_server_info(server_id, window, cx), + LogKind::Rpc => this.show_rpc_trace_for_server(server_key, window, cx), + LogKind::Trace => this.show_trace_for_server(server_key, window, cx), + LogKind::Logs => this.show_logs_for_server(server_key, window, cx), + LogKind::ServerInfo => this.show_server_info(server_key, window, cx), } } - } else if let Some(server_id) = first_server_id_for_project { + } else if let Some(server_key) = first_server_key_for_project { match this.active_entry_kind { - LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx), - LogKind::Trace => this.show_trace_for_server(server_id, window, cx), - LogKind::Logs => this.show_logs_for_server(server_id, window, cx), - LogKind::ServerInfo => this.show_server_info(server_id, window, cx), + LogKind::Rpc => this.show_rpc_trace_for_server(server_key, window, cx), + LogKind::Trace => this.show_trace_for_server(server_key, window, cx), + LogKind::Logs => this.show_logs_for_server(server_key, window, cx), + LogKind::ServerInfo => this.show_server_info(server_key, window, cx), } } @@ -183,8 +210,8 @@ impl LspLogView { &log_store, window, move |log_view, _, e, window, cx| match e { - Event::NewServerLogEntry { id, kind, text } => { - if log_view.current_server_id == Some(*id) + Event::NewServerLogEntry { key, kind, text } => { + if log_view.current_server_key.as_ref() == Some(key) && LogKind::from_server_log_type(kind) == log_view.active_entry_kind { log_view.editor.update(cx, |editor, cx| { @@ -234,12 +261,15 @@ impl LspLogView { }); cx.on_release(|log_view, cx| { + let project = log_view.project.downgrade(); + let lsp_store = log_view.project.read(cx).lsp_store().downgrade(); log_view.log_store.update(cx, |log_store, cx| { - for (server_id, state) in &log_store.language_servers { - if let Some(log_kind) = state.toggled_log_kind { - if let Some(log_type) = log_type(log_kind) { - send_toggle_log_message(state, *server_id, false, log_type, cx); - } + for (key, state) in &log_store.language_servers { + if key.is_for_project(&project, &lsp_store) + && let Some(log_kind) = state.toggled_log_kind + && let Some(log_type) = log_type(log_kind) + { + send_toggle_log_message(key, false, log_type, cx); } } }); @@ -252,7 +282,7 @@ impl LspLogView { editor_subscriptions, project, log_store, - current_server_id: None, + current_server_key: None, active_entry_kind: LogKind::Logs, _log_store_subscriptions: vec![ model_changes_subscription, @@ -260,8 +290,8 @@ impl LspLogView { focus_subscription, ], }; - if let Some(server_id) = server_id { - lsp_log_view.show_logs_for_server(server_id, window, cx); + if let Some(server_key) = server_key { + lsp_log_view.show_logs_for_server(server_key, window, cx); } lsp_log_view } @@ -342,55 +372,28 @@ impl LspLogView { ); (editor, vec![editor_subscription, search_subscription]) } - pub(crate) fn try_ensure_copilot_for_project(&self, cx: &mut App) { - self.log_store.update(cx, |this, cx| { - let copilot = EditPredictionStore::try_global(cx) - .and_then(|store| store.read(cx).copilot_for_project(&self.project))?; - let server = copilot.read(cx).language_server()?.clone(); - let log_subscription = this.copilot_state_for_project(&self.project.downgrade()); - if let Some(subscription_slot @ None) = log_subscription { - let weak_lsp_store = cx.weak_entity(); - let server_id = server.server_id(); - - let name = LanguageServerName::new_static("copilot"); - *subscription_slot = - Some(server.on_notification::( - move |params, cx| { - weak_lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.add_language_server_log( - server_id, - MessageType::LOG, - ¶ms.message, - cx, - ); - }) - .ok(); - }, - )); - this.add_language_server( - LanguageServerKind::Global, - server.server_id(), - Some(name), - None, - Some(server.clone()), - cx, - ); - } - - Some(()) + pub(crate) fn sync_copilot_for_project(&self, cx: &mut App) { + let server = EditPredictionStore::try_global(cx) + .and_then(|store| store.read(cx).copilot_for_project(&self.project)) + .and_then(|copilot| copilot.read(cx).language_server().cloned()); + self.log_store.update(cx, |log_store, cx| { + log_store.sync_copilot_for_project(&self.project.downgrade(), server, cx); }); } + pub(crate) fn menu_items(&self, cx: &mut App) -> Option> { - self.try_ensure_copilot_for_project(cx); + self.sync_copilot_for_project(cx); let log_store = self.log_store.read(cx); let unknown_server = LanguageServerName::new_static("unknown server"); + let project = self.project.downgrade(); + let lsp_store = self.project.read(cx).lsp_store().downgrade(); let mut rows = log_store .language_servers .iter() - .map(|(server_id, state)| match &state.kind { + .filter(|(key, _)| key.is_for_project(&project, &lsp_store)) + .map(|(key, state)| match &key.kind { LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } | LanguageServerKind::LocalSsh { .. } => { @@ -401,9 +404,9 @@ impl LspLogView { .unwrap_or_else(|| "Unknown worktree".to_string()); LogMenuItem { - server_id: *server_id, + server_id: key.server_id, server_name: state.name.clone().unwrap_or(unknown_server.clone()), - server_kind: state.kind.clone(), + server_kind: key.kind.clone(), worktree_root_name, rpc_trace_enabled: state.rpc_state.is_some(), selected_entry: self.active_entry_kind, @@ -411,42 +414,24 @@ impl LspLogView { } } - LanguageServerKind::Global => LogMenuItem { - server_id: *server_id, + LanguageServerKind::Supplementary { .. } => LogMenuItem { + server_id: key.server_id, server_name: state.name.clone().unwrap_or(unknown_server.clone()), - server_kind: state.kind.clone(), + server_kind: key.kind.clone(), worktree_root_name: "supplementary".to_string(), rpc_trace_enabled: state.rpc_state.is_some(), selected_entry: self.active_entry_kind, trace_level: lsp::TraceValue::Off, }, }) - .chain( - self.project - .read(cx) - .supplementary_language_servers(cx) - .filter_map(|(server_id, name)| { - let state = log_store.language_servers.get(&server_id)?; - Some(LogMenuItem { - server_id, - server_name: name, - server_kind: state.kind.clone(), - worktree_root_name: "supplementary".to_string(), - rpc_trace_enabled: state.rpc_state.is_some(), - selected_entry: self.active_entry_kind, - trace_level: lsp::TraceValue::Off, - }) - }), - ) .collect::>(); - rows.sort_unstable_by_key(|row| row.server_id); - rows.dedup_by_key(|row| row.server_id); + rows.sort_by_key(|row| row.server_id); Some(rows) } fn show_logs_for_server( &mut self, - server_id: LanguageServerId, + key: LanguageServerLogKey, window: &mut Window, cx: &mut Context, ) { @@ -454,16 +439,16 @@ impl LspLogView { .log_store .read(cx) .language_servers - .get(&server_id) + .get(&key) .map(|v| v.log_level) .unwrap_or(MessageType::LOG); let log_contents = self .log_store .read(cx) - .server_logs(server_id) + .server_logs(&key) .map(|v| log_contents(v, typ)); if let Some(log_contents) = log_contents { - self.current_server_id = Some(server_id); + self.current_server_key = Some(key.clone()); self.active_entry_kind = LogKind::Logs; let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx); self.editor = editor; @@ -472,26 +457,26 @@ impl LspLogView { } self.editor.read(cx).focus_handle(cx).focus(window, cx); self.log_store.update(cx, |log_store, cx| { - let state = log_store.get_language_server_state(server_id)?; + let state = log_store.get_language_server_state(&key)?; state.toggled_log_kind = Some(LogKind::Logs); - send_toggle_log_message(state, server_id, true, LogType::Log, cx); + send_toggle_log_message(&key, true, LogType::Log, cx); Some(()) }); } fn update_log_level( &self, - server_id: LanguageServerId, + key: LanguageServerLogKey, level: MessageType, window: &mut Window, cx: &mut Context, ) { let log_contents = self.log_store.update(cx, |this, _| { - if let Some(state) = this.get_language_server_state(server_id) { + if let Some(state) = this.get_language_server_state(&key) { state.log_level = level; } - this.server_logs(server_id).map(|v| log_contents(v, level)) + this.server_logs(&key).map(|v| log_contents(v, level)) }); if let Some(log_contents) = log_contents { @@ -507,31 +492,31 @@ impl LspLogView { fn show_trace_for_server( &mut self, - server_id: LanguageServerId, + key: LanguageServerLogKey, window: &mut Window, cx: &mut Context, ) { let trace_level = self .log_store .update(cx, |log_store, _| { - Some(log_store.get_language_server_state(server_id)?.trace_level) + Some(log_store.get_language_server_state(&key)?.trace_level) }) .unwrap_or(TraceValue::Messages); let log_contents = self .log_store .read(cx) - .server_trace(server_id) + .server_trace(&key) .map(|v| log_contents(v, trace_level)); if let Some(log_contents) = log_contents { - self.current_server_id = Some(server_id); + self.current_server_key = Some(key.clone()); self.active_entry_kind = LogKind::Trace; let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx); self.editor = editor; self.editor_subscriptions = editor_subscriptions; self.log_store.update(cx, |log_store, cx| { - let state = log_store.get_language_server_state(server_id)?; + let state = log_store.get_language_server_state(&key)?; state.toggled_log_kind = Some(LogKind::Trace); - send_toggle_log_message(state, server_id, true, LogType::Trace, cx); + send_toggle_log_message(&key, true, LogType::Trace, cx); Some(()) }); cx.notify(); @@ -541,18 +526,18 @@ impl LspLogView { fn show_rpc_trace_for_server( &mut self, - server_id: LanguageServerId, + key: LanguageServerLogKey, window: &mut Window, cx: &mut Context, ) { - self.toggle_rpc_trace_for_server(server_id, true, window, cx); + self.toggle_rpc_trace_for_server(key.clone(), true, window, cx); let rpc_log = self.log_store.update(cx, |log_store, _| { log_store - .enable_rpc_trace_for_language_server(server_id) + .enable_rpc_trace_for_language_server(&key) .map(|state| log_contents(&state.rpc_messages, ())) }); if let Some(rpc_log) = rpc_log { - self.current_server_id = Some(server_id); + self.current_server_key = Some(key); self.active_entry_kind = LogKind::Rpc; let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); let language = self.project.read(cx).languages().language_for_name("JSON"); @@ -585,43 +570,50 @@ impl LspLogView { fn toggle_rpc_trace_for_server( &mut self, - server_id: LanguageServerId, + key: LanguageServerLogKey, enabled: bool, window: &mut Window, cx: &mut Context, ) { self.log_store.update(cx, |log_store, cx| { if enabled { - log_store.enable_rpc_trace_for_language_server(server_id); + log_store.enable_rpc_trace_for_language_server(&key); } else { - log_store.disable_rpc_trace_for_language_server(server_id); + log_store.disable_rpc_trace_for_language_server(&key); } - if let Some(server_state) = log_store.language_servers.get(&server_id) { - send_toggle_log_message(server_state, server_id, enabled, LogType::Rpc, cx); - }; + if log_store.language_servers.contains_key(&key) { + send_toggle_log_message(&key, enabled, LogType::Rpc, cx); + } }); - if !enabled && Some(server_id) == self.current_server_id { - self.show_logs_for_server(server_id, window, cx); + if !enabled && self.current_server_key.as_ref() == Some(&key) { + self.show_logs_for_server(key, window, cx); cx.notify(); } } fn update_trace_level( &self, - server_id: LanguageServerId, + key: LanguageServerLogKey, level: TraceValue, cx: &mut Context, ) { - if let Some(server) = self - .project + let server = self + .log_store .read(cx) - .lsp_store() - .read(cx) - .language_server_for_id(server_id) - { + .language_servers + .get(&key) + .and_then(|state| state.server()) + .or_else(|| { + self.project + .read(cx) + .lsp_store() + .read(cx) + .language_server_for_id(key.server_id) + }); + if let Some(server) = server { self.log_store.update(cx, |this, _| { - if let Some(state) = this.get_language_server_state(server_id) { + if let Some(state) = this.get_language_server_state(&key) { state.trace_level = level; } }); @@ -634,35 +626,46 @@ impl LspLogView { fn show_server_info( &mut self, - server_id: LanguageServerId, + key: LanguageServerLogKey, window: &mut Window, cx: &mut Context, ) { - let Some(server_info) = self - .project + let server_id = key.server_id; + let server_info = self + .log_store .read(cx) - .lsp_store() - .update(cx, |lsp_store, _| { - lsp_store - .language_server_for_id(server_id) - .as_ref() - .map(|language_server| ServerInfo::new(language_server)) - .or_else(move || { - let capabilities = - lsp_store.lsp_server_capabilities.get(&server_id)?.clone(); - let status = lsp_store.language_server_statuses.get(&server_id)?.clone(); + .language_servers + .get(&key) + .and_then(|state| state.server()) + .as_deref() + .map(ServerInfo::new); + let server_info = server_info.or_else(|| { + self.project + .read(cx) + .lsp_store() + .update(cx, |lsp_store, _| { + lsp_store + .language_server_for_id(server_id) + .as_ref() + .map(|language_server| ServerInfo::new(language_server)) + .or_else(move || { + let capabilities = + lsp_store.lsp_server_capabilities.get(&server_id)?.clone(); + let status = + lsp_store.language_server_statuses.get(&server_id)?.clone(); - Some(ServerInfo { - id: server_id, - capabilities, - status, + Some(ServerInfo { + id: server_id, + capabilities, + status, + }) }) - }) - }) - else { + }) + }); + let Some(server_info) = server_info else { return; }; - self.current_server_id = Some(server_id); + self.current_server_key = Some(key.clone()); self.active_entry_kind = LogKind::ServerInfo; let (editor, editor_subscriptions) = Self::editor_for_server_info(server_info, window, cx); self.editor = editor; @@ -670,12 +673,12 @@ impl LspLogView { cx.notify(); self.editor.read(cx).focus_handle(cx).focus(window, cx); self.log_store.update(cx, |log_store, cx| { - let state = log_store.get_language_server_state(server_id)?; - if let Some(log_kind) = state.toggled_log_kind.take() { - if let Some(log_type) = log_type(log_kind) { - send_toggle_log_message(state, server_id, false, log_type, cx); - } - }; + let state = log_store.get_language_server_state(&key)?; + if let Some(log_kind) = state.toggled_log_kind.take() + && let Some(log_type) = log_type(log_kind) + { + send_toggle_log_message(&key, false, log_type, cx); + } Some(()) }); } @@ -691,13 +694,12 @@ fn log_type(log_kind: LogKind) -> Option { } fn send_toggle_log_message( - server_state: &log_store::LanguageServerState, - server_id: LanguageServerId, + key: &LanguageServerLogKey, enabled: bool, log_type: LogType, cx: &mut App, ) { - if let LanguageServerKind::Remote { project } = &server_state.kind { + if let LanguageServerKind::Remote { project } = &key.kind { project .update(cx, |project, cx| { if let Some((client, project_id)) = project.lsp_store().read(cx).upstream_client() { @@ -705,7 +707,7 @@ fn send_toggle_log_message( .send(proto::ToggleLspLogs { project_id, log_type: log_type as i32, - server_id: server_id.to_proto(), + server_id: key.server_id.to_proto(), enabled, }) .log_err(); @@ -790,12 +792,12 @@ impl Item for LspLogView { { Task::ready(Some(cx.new(|cx| { let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx); - if let Some(server_id) = self.current_server_id { + if let Some(server_key) = self.current_server_key.clone() { match self.active_entry_kind { - LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, window, cx), - LogKind::Trace => new_view.show_trace_for_server(server_id, window, cx), - LogKind::Logs => new_view.show_logs_for_server(server_id, window, cx), - LogKind::ServerInfo => new_view.show_server_info(server_id, window, cx), + LogKind::Rpc => new_view.show_rpc_trace_for_server(server_key, window, cx), + LogKind::Trace => new_view.show_trace_for_server(server_key, window, cx), + LogKind::Logs => new_view.show_logs_for_server(server_key, window, cx), + LogKind::ServerInfo => new_view.show_server_info(server_key, window, cx), } } new_view @@ -934,25 +936,24 @@ impl Render for LspLogToolbarItemView { return div(); }; - let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| { + let (menu_rows, current_server_key) = log_view.update(cx, |log_view, cx| { let menu_rows = log_view.menu_items(cx).unwrap_or_default(); - let current_server_id = log_view.current_server_id; - (menu_rows, current_server_id) + let current_server_key = log_view.current_server_key.clone(); + (menu_rows, current_server_key) }); - let current_server = current_server_id.and_then(|current_server_id| { - if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) { - Some(menu_rows[ix].clone()) - } else { - None - } + let current_server = current_server_key.and_then(|current_server_key| { + menu_rows + .iter() + .find(|row| row.key() == current_server_key) + .cloned() }); let available_language_servers: Vec<_> = menu_rows .into_iter() .map(|row| { ( - row.server_id, + row.key(), row.server_name, row.worktree_root_name, row.selected_entry, @@ -988,33 +989,44 @@ impl Render for LspLogToolbarItemView { move |window, cx| { let log_view = log_view.clone(); ContextMenu::build(window, cx, |mut menu, window, _| { - for (server_id, name, worktree_root, active_entry_kind) in + for (server_key, name, worktree_root, active_entry_kind) in available_language_servers.iter() { let label = format!("{name} ({worktree_root})"); - let server_id = *server_id; + let server_key = server_key.clone(); let active_entry_kind = *active_entry_kind; menu = menu.entry( label, None, window.handler_for(&log_view, move |view, window, cx| { - view.current_server_id = Some(server_id); + view.current_server_key = Some(server_key.clone()); view.active_entry_kind = active_entry_kind; match view.active_entry_kind { LogKind::Rpc => { view.toggle_rpc_trace_for_server( - server_id, true, window, cx, + server_key.clone(), + true, + window, + cx, + ); + view.show_rpc_trace_for_server( + server_key.clone(), + window, + cx, ); - view.show_rpc_trace_for_server(server_id, window, cx); - } - LogKind::Trace => { - view.show_trace_for_server(server_id, window, cx) - } - LogKind::Logs => { - view.show_logs_for_server(server_id, window, cx) } + LogKind::Trace => view.show_trace_for_server( + server_key.clone(), + window, + cx, + ), + LogKind::Logs => view.show_logs_for_server( + server_key.clone(), + window, + cx, + ), LogKind::ServerInfo => { - view.show_server_info(server_id, window, cx) + view.show_server_info(server_key.clone(), window, cx) } } cx.notify(); @@ -1028,7 +1040,7 @@ impl Render for LspLogToolbarItemView { }); let view_selector = current_server.map(|server| { - let server_id = server.server_id; + let server_key = server.key(); let rpc_trace_enabled = server.rpc_trace_enabled; let log_view = log_view.clone(); let label = match server.selected_entry { @@ -1049,25 +1061,34 @@ impl Render for LspLogToolbarItemView { .menu(move |window, cx| { let log_toolbar_view = log_toolbar_view.upgrade()?; let log_view = log_view.clone(); + let server_key = server_key.clone(); Some(ContextMenu::build(window, cx, move |this, window, _| { this.entry( SERVER_LOGS, None, - window.handler_for(&log_view, move |view, window, cx| { - view.show_logs_for_server(server_id, window, cx); + window.handler_for(&log_view, { + let server_key = server_key.clone(); + move |view, window, cx| { + view.show_logs_for_server(server_key.clone(), window, cx); + } }), ) .entry( SERVER_TRACE, None, - window.handler_for(&log_view, move |view, window, cx| { - view.show_trace_for_server(server_id, window, cx); + window.handler_for(&log_view, { + let server_key = server_key.clone(); + move |view, window, cx| { + view.show_trace_for_server(server_key.clone(), window, cx); + } }), ) .custom_entry( { let log_toolbar_view = log_toolbar_view.clone(); + let server_key = server_key.clone(); move |window, _| { + let server_key = server_key.clone(); h_flex() .w_full() .justify_between() @@ -1090,7 +1111,10 @@ impl Render for LspLogToolbarItemView { ToggleState::Selected ); view.toggle_rpc_logging_for_server( - server_id, enabled, window, cx, + server_key.clone(), + enabled, + window, + cx, ); cx.stop_propagation(); }, @@ -1100,15 +1124,21 @@ impl Render for LspLogToolbarItemView { .into_any_element() } }, - window.handler_for(&log_view, move |view, window, cx| { - view.show_rpc_trace_for_server(server_id, window, cx); + window.handler_for(&log_view, { + let server_key = server_key.clone(); + move |view, window, cx| { + view.show_rpc_trace_for_server(server_key.clone(), window, cx); + } }), ) .entry( SERVER_INFO, None, - window.handler_for(&log_view, move |view, window, cx| { - view.show_server_info(server_id, window, cx); + window.handler_for(&log_view, { + let server_key = server_key.clone(); + move |view, window, cx| { + view.show_server_info(server_key.clone(), window, cx); + } }), ) })) @@ -1146,14 +1176,17 @@ impl Render for LspLogToolbarItemView { let log_view = log_view; move |window, cx| { - let id = log_view.read(cx).current_server_id?; + let key = + log_view.read(cx).current_server_key.clone()?; let trace_level = log_view.update(cx, |this, cx| { this.log_store.update(cx, |this, _| { Some( - this.get_language_server_state(id)? - .trace_level, + this.get_language_server_state( + &key, + )? + .trace_level, ) }) })?; @@ -1173,11 +1206,12 @@ impl Render for LspLogToolbarItemView { let log_view = log_view.clone(); move |_, cx| { log_view.update(cx, |this, cx| { - if let Some(id) = - this.current_server_id + if let Some(key) = this + .current_server_key + .clone() { this.update_trace_level( - id, option, cx, + key, option, cx, ); } }); @@ -1216,14 +1250,17 @@ impl Render for LspLogToolbarItemView { let log_view = log_view; move |window, cx| { - let id = log_view.read(cx).current_server_id?; + let key = + log_view.read(cx).current_server_key.clone()?; let log_level = log_view.update(cx, |this, cx| { this.log_store.update(cx, |this, _| { Some( - this.get_language_server_state(id)? - .log_level, + this.get_language_server_state( + &key, + )? + .log_level, ) }) })?; @@ -1244,11 +1281,12 @@ impl Render for LspLogToolbarItemView { let log_view = log_view.clone(); move |window, cx| { log_view.update(cx, |this, cx| { - if let Some(id) = - this.current_server_id + if let Some(key) = this + .current_server_key + .clone() { this.update_log_level( - id, option, window, cx, + key, option, window, cx, ); } }); @@ -1328,19 +1366,19 @@ impl LspLogToolbarItemView { fn toggle_rpc_logging_for_server( &mut self, - id: LanguageServerId, + key: LanguageServerLogKey, enabled: bool, window: &mut Window, cx: &mut Context, ) { if let Some(log_view) = &self.log_view { log_view.update(cx, |log_view, cx| { - log_view.toggle_rpc_trace_for_server(id, enabled, window, cx); - if !enabled && Some(id) == log_view.current_server_id { - log_view.show_logs_for_server(id, window, cx); + log_view.toggle_rpc_trace_for_server(key.clone(), enabled, window, cx); + if !enabled && log_view.current_server_key.as_ref() == Some(&key) { + log_view.show_logs_for_server(key.clone(), window, cx); cx.notify(); } else if enabled { - log_view.show_rpc_trace_for_server(id, window, cx); + log_view.show_rpc_trace_for_server(key.clone(), window, cx); cx.notify(); } window.focus(&log_view.focus_handle, cx); diff --git a/crates/language_tools/src/lsp_log_view_tests.rs b/crates/language_tools/src/lsp_log_view_tests.rs index 326512d374..33512fbe19 100644 --- a/crates/language_tools/src/lsp_log_view_tests.rs +++ b/crates/language_tools/src/lsp_log_view_tests.rs @@ -5,16 +5,323 @@ use crate::lsp_log_view::LogMenuItem; use super::*; use futures::StreamExt; use gpui::{AppContext as _, TestAppContext, VisualTestContext}; -use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; +use language::{ + FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageServerId, tree_sitter_rust, +}; use lsp::LanguageServerName; use project::{ FakeFs, Project, - lsp_store::log_store::{LanguageServerKind, LogKind, LogStore}, + lsp_store::log_store::{LanguageServerKind, LanguageServerLogKey, LogKind, LogStore}, }; use serde_json::json; use settings::SettingsStore; use util::path; +#[gpui::test] +async fn test_lsp_log_view_filters_servers_from_other_projects(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/current-project"), json!({ "test.rs": "" })) + .await; + fs.insert_tree(path!("/other-project"), json!({ "test.rs": "" })) + .await; + + let project = Project::test(fs.clone(), [path!("/current-project").as_ref()], cx).await; + let other_project = Project::test(fs.clone(), [path!("/other-project").as_ref()], cx).await; + let worktree_id = project.read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + let current_lsp_store = project + .read_with(cx, |project, _| project.lsp_store()) + .downgrade(); + let other_lsp_store = other_project + .read_with(cx, |project, _| project.lsp_store()) + .downgrade(); + + let current_ssh_server_id = LanguageServerId(99); + let current_server_id = LanguageServerId(100); + let log_store = cx.new(|cx| LogStore::new(false, cx)); + log_store.update(cx, |store, cx| { + store.add_language_server( + LanguageServerKind::LocalSsh { + lsp_store: current_lsp_store, + }, + current_ssh_server_id, + Some(LanguageServerName::new_static("current-ssh-server")), + Some(worktree_id), + None, + cx, + ); + store.add_language_server( + LanguageServerKind::Local { + project: project.downgrade(), + }, + current_server_id, + Some(LanguageServerName::new_static("current-server")), + Some(worktree_id), + None, + cx, + ); + store.add_language_server( + LanguageServerKind::Remote { + project: other_project.downgrade(), + }, + current_server_id, + Some(LanguageServerName::new_static("other-remote-server")), + None, + None, + cx, + ); + store.add_language_server( + LanguageServerKind::Supplementary { + project: other_project.downgrade(), + }, + current_server_id, + Some(LanguageServerName::new_static("other-supplementary-server")), + None, + None, + cx, + ); + store.add_language_server( + LanguageServerKind::LocalSsh { + lsp_store: other_lsp_store, + }, + current_ssh_server_id, + Some(LanguageServerName::new_static("other-ssh-server")), + None, + None, + cx, + ); + }); + assert_eq!( + log_store.read_with(cx, |store, _| store.language_servers.len()), + 5 + ); + + let window = + cx.add_window(|window, cx| LspLogView::new(project.clone(), log_store, window, cx)); + let log_view = window.root(cx).unwrap(); + let mut cx = VisualTestContext::from_window(*window, cx); + + log_view.update(&mut cx, |view, cx| { + let visible_servers = view + .menu_items(cx) + .unwrap() + .into_iter() + .map(|item| (item.server_id, item.server_name)) + .collect::>(); + assert_eq!( + visible_servers, + [ + ( + current_ssh_server_id, + LanguageServerName::new_static("current-ssh-server"), + ), + ( + current_server_id, + LanguageServerName::new_static("current-server"), + ), + ] + ); + }); +} + +#[gpui::test] +async fn test_lsp_log_view_labels_registered_supplementary_servers(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/the-root"), json!({ "test.rs": "" })) + .await; + let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await; + let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); + let log_store = cx.new(|cx| LogStore::new(false, cx)); + log_store.update(cx, |store, cx| store.add_project(&project, cx)); + + let server_id = LanguageServerId(100); + let (server, _fake_server) = lsp::FakeLanguageServer::new( + server_id, + lsp::LanguageServerBinary { + path: "path/to/prettier".into(), + arguments: Vec::new(), + env: None, + }, + "prettier".to_string(), + Default::default(), + &mut cx.to_async(), + ); + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.register_supplementary_language_server_for_test( + server_id, + LanguageServerName::new_static("prettier (default)"), + Arc::new(server), + cx, + ); + }); + + let window = + cx.add_window(|window, cx| LspLogView::new(project.clone(), log_store, window, cx)); + let log_view = window.root(cx).unwrap(); + let mut cx = VisualTestContext::from_window(*window, cx); + + log_view.update(&mut cx, |view, cx| { + assert_eq!( + view.menu_items(cx).unwrap(), + &[LogMenuItem { + server_id, + server_name: LanguageServerName::new_static("prettier (default)"), + worktree_root_name: "supplementary".to_string(), + rpc_trace_enabled: false, + selected_entry: LogKind::Logs, + trace_level: lsp::TraceValue::Off, + server_kind: LanguageServerKind::Supplementary { + project: project.downgrade(), + }, + }] + ); + }); + + lsp_store.update(&mut cx, |lsp_store, cx| { + lsp_store.unregister_supplementary_language_server_for_test(server_id, cx); + }); + log_view.update(&mut cx, |view, cx| { + assert!(view.menu_items(cx).unwrap().is_empty()); + }); +} + +#[gpui::test] +async fn test_log_store_does_not_retain_language_servers(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/the-root"), json!({ "test.rs": "" })) + .await; + let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await; + let server_id = LanguageServerId(100); + let server_kind = LanguageServerKind::Local { + project: project.downgrade(), + }; + let server_key = LanguageServerLogKey::new(server_kind.clone(), server_id); + let log_store = cx.new(|cx| LogStore::new(false, cx)); + + let (first_server, _first_fake_server) = lsp::FakeLanguageServer::new( + server_id, + lsp::LanguageServerBinary { + path: "path/to/first-language-server".into(), + arguments: Vec::new(), + env: None, + }, + "first-language-server".to_string(), + Default::default(), + &mut cx.to_async(), + ); + let first_server = Arc::new(first_server); + let first_server_weak = Arc::downgrade(&first_server); + log_store.update(cx, |store, cx| { + store.add_language_server( + server_kind.clone(), + server_id, + Some(LanguageServerName::new_static("first-language-server")), + None, + Some(first_server.clone()), + cx, + ); + }); + + let (replacement_server, _replacement_fake_server) = lsp::FakeLanguageServer::new( + server_id, + lsp::LanguageServerBinary { + path: "path/to/replacement-language-server".into(), + arguments: Vec::new(), + env: None, + }, + "replacement-language-server".to_string(), + Default::default(), + &mut cx.to_async(), + ); + let replacement_server = Arc::new(replacement_server); + let replacement_server_weak = Arc::downgrade(&replacement_server); + log_store.update(cx, |store, cx| { + store.add_language_server( + server_kind, + server_id, + Some(LanguageServerName::new_static( + "replacement-language-server", + )), + None, + Some(replacement_server.clone()), + cx, + ); + }); + + let stored_server = log_store + .read_with(cx, |store, _| { + store + .language_servers + .get(&server_key) + .and_then(|state| state.server()) + }) + .expect("replacement language server should be available"); + assert!(Arc::ptr_eq(&stored_server, &replacement_server)); + drop(stored_server); + + drop(first_server); + assert!(first_server_weak.upgrade().is_none()); + drop(replacement_server); + assert!(replacement_server_weak.upgrade().is_none()); + assert!( + log_store + .read_with(cx, |store, _| store + .language_servers + .get(&server_key) + .and_then(|state| state.server())) + .is_none() + ); +} + +#[gpui::test] +async fn test_log_store_removes_unavailable_copilot_server(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/the-root"), json!({ "test.rs": "" })) + .await; + let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await; + let server_id = LanguageServerId(100); + let server_kind = LanguageServerKind::Supplementary { + project: project.downgrade(), + }; + let server_key = LanguageServerLogKey::new(server_kind, server_id); + let log_store = cx.new(|cx| LogStore::new(false, cx)); + log_store.update(cx, |log_store, cx| log_store.add_project(&project, cx)); + + let (server, _fake_server) = lsp::FakeLanguageServer::new( + server_id, + lsp::LanguageServerBinary { + path: "path/to/copilot-language-server".into(), + arguments: Vec::new(), + env: None, + }, + "copilot".to_string(), + Default::default(), + &mut cx.to_async(), + ); + log_store.update(cx, |log_store, cx| { + log_store.sync_copilot_for_project(&project.downgrade(), Some(Arc::new(server)), cx); + }); + assert!(log_store.read_with(cx, |log_store, _| { + log_store.language_servers.contains_key(&server_key) + })); + + log_store.update(cx, |log_store, cx| { + log_store.sync_copilot_for_project(&project.downgrade(), None, cx); + }); + assert!(!log_store.read_with(cx, |log_store, _| { + log_store.language_servers.contains_key(&server_key) + })); +} + #[gpui::test] async fn test_lsp_log_view(cx: &mut TestAppContext) { zlog::init_test(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 266d398fd5..99bc93bd15 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -333,6 +333,7 @@ pub struct LocalLspStore { >, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, + _binary_status_task: Task<()>, lsp_tree: LanguageServerTree, registered_buffers: HashMap, buffers_opened_in_servers: HashMap>, @@ -415,6 +416,14 @@ impl LocalLspStore { } } + fn update_binary_status(&self, server_name: LanguageServerName, status: BinaryStatus) { + self.languages.update_lsp_binary_status_for_entity( + self.weak.entity_id(), + server_name, + status, + ); + } + fn start_language_server( &mut self, worktree_handle: &Entity, @@ -678,8 +687,7 @@ impl LocalLspStore { }; if update_binary_status { - self.languages - .update_lsp_binary_status(adapter.name(), BinaryStatus::Starting); + self.update_binary_status(adapter.name(), BinaryStatus::Starting); } self.language_servers.insert(server_id, state); @@ -707,7 +715,6 @@ impl LocalLspStore { && let Some(path) = settings.path.as_ref().map(PathBuf::from) { let settings = settings.clone(); - let languages = self.languages.clone(); return cx.background_spawn(async move { if let Some(mut wait_until_worktree_trust) = wait_until_worktree_trust { let already_trusted = *wait_until_worktree_trust.borrow(); @@ -726,8 +733,7 @@ impl LocalLspStore { adapter.name(), ); } - languages - .update_lsp_binary_status(adapter.name(), BinaryStatus::Starting); + delegate.update_status(adapter.name(), BinaryStatus::Starting); } let mut env = delegate.shell_env().await; env.extend(settings.env.unwrap_or_default()); @@ -748,7 +754,6 @@ impl LocalLspStore { #[cfg(any(test, feature = "test-support"))] if !adapter.adapter.is_extension() && self.languages.has_fake_lsp_server(&adapter.name) { let language_server_name = adapter.name.clone(); - let languages = self.languages.clone(); return cx.spawn(async move |_| { if let Some(mut wait_until_worktree_trust) = wait_until_worktree_trust { let already_trusted = *wait_until_worktree_trust.borrow(); @@ -765,7 +770,7 @@ impl LocalLspStore { "Worktree {worktree_abs_path:?} is trusted, starting language server {language_server_name}", ); } - languages.update_lsp_binary_status( + delegate.update_status( language_server_name.clone(), BinaryStatus::Starting, ); @@ -4457,7 +4462,9 @@ impl BufferLspData { #[derive(Debug)] pub enum LspStoreEvent { LanguageServerAdded(LanguageServerId, LanguageServerName, Option), + SupplementaryLanguageServerAdded(LanguageServerId, LanguageServerName), LanguageServerRemoved(LanguageServerId), + SupplementaryLanguageServerRemoved(LanguageServerId), LanguageServerUpdate { language_server_id: LanguageServerId, name: Option, @@ -4680,7 +4687,7 @@ impl LspStore { .detach(); cx.observe_global::(Self::on_settings_changed) .detach(); - subscribe_to_binary_statuses(&languages, cx).detach(); + let binary_status_task = subscribe_to_binary_statuses(&languages, cx); let _maintain_workspace_config = { let (sender, receiver) = watch::channel(); @@ -4717,6 +4724,7 @@ impl LspStore { .unwrap() .shutdown_language_servers_on_quit() }), + _binary_status_task: binary_status_task, lsp_tree: LanguageServerTree::new( manifest_tree, languages.clone(), @@ -4785,7 +4793,6 @@ impl LspStore { .detach(); cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); - subscribe_to_binary_statuses(&languages, cx).detach(); let _maintain_workspace_config = { let (sender, receiver) = watch::channel(); (Self::maintain_workspace_config(receiver, cx), sender) @@ -4811,7 +4818,7 @@ impl LspStore { active_entry: None, _maintain_workspace_config, - _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), + _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), } } @@ -10953,6 +10960,19 @@ impl LspStore { .map(|(key, value)| (*key, value)) } + fn should_forward_untagged_binary_status(&self, server_name: &LanguageServerName) -> bool { + self.as_local().is_some_and(|local| { + local + .language_server_ids + .keys() + .any(|seed| &seed.name == server_name) + || self + .language_server_statuses + .values() + .any(|status| &status.name == server_name) + }) + } + #[cfg(feature = "test-support")] pub fn has_language_server_seed_for_worktree(&self, worktree_id: WorktreeId) -> bool { self.as_local().is_some_and(|local| { @@ -12116,17 +12136,18 @@ impl LspStore { if let Some(name) = name { log::info!("stopping language server {name}"); - self.languages - .update_lsp_binary_status(name.clone(), BinaryStatus::Stopping); + if let Some(local) = self.as_local() { + local.update_binary_status(name.clone(), BinaryStatus::Stopping); + } cx.notify(); return cx.spawn(async move |lsp_store, cx| { Self::shutdown_language_server(server_state, name.clone(), cx).await; lsp_store .update(cx, |lsp_store, cx| { - lsp_store - .languages - .update_lsp_binary_status(name, BinaryStatus::Stopped); + if let Some(local) = lsp_store.as_local() { + local.update_binary_status(name, BinaryStatus::Stopped); + } cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); cx.notify(); }) @@ -12677,9 +12698,7 @@ impl LspStore { simulate_disk_based_diagnostics_completion: None, }, ); - local - .languages - .update_lsp_binary_status(adapter.name(), BinaryStatus::None); + local.update_binary_status(adapter.name(), BinaryStatus::None); if let Some(file_ops_caps) = language_server .capabilities() .workspace @@ -12977,10 +12996,21 @@ impl LspStore { local .supplementary_language_servers .insert(id, (name.clone(), server)); - cx.emit(LspStoreEvent::LanguageServerAdded(id, name, None)); + cx.emit(LspStoreEvent::SupplementaryLanguageServerAdded(id, name)); } } + #[cfg(feature = "test-support")] + pub fn register_supplementary_language_server_for_test( + &mut self, + id: LanguageServerId, + name: LanguageServerName, + server: Arc, + cx: &mut Context, + ) { + self.register_supplementary_language_server(id, name, server, cx); + } + fn unregister_supplementary_language_server( &mut self, id: LanguageServerId, @@ -12988,19 +13018,17 @@ impl LspStore { ) { if let Some(local) = self.as_local_mut() { local.supplementary_language_servers.remove(&id); - cx.emit(LspStoreEvent::LanguageServerRemoved(id)); + cx.emit(LspStoreEvent::SupplementaryLanguageServerRemoved(id)); } } - pub(crate) fn supplementary_language_servers( - &self, - ) -> impl '_ + Iterator { - self.as_local().into_iter().flat_map(|local| { - local - .supplementary_language_servers - .iter() - .map(|(id, (name, _))| (*id, name.clone())) - }) + #[cfg(feature = "test-support")] + pub fn unregister_supplementary_language_server_for_test( + &mut self, + id: LanguageServerId, + cx: &mut Context, + ) { + self.unregister_supplementary_language_server(id, cx); } pub fn language_server_adapter_for_id( @@ -14015,11 +14043,19 @@ fn subscribe_to_binary_statuses( languages: &Arc, cx: &mut Context<'_, LspStore>, ) -> Task<()> { - let mut server_statuses = languages.language_server_binary_statuses(); + let (mut server_statuses, subscription) = languages.language_server_binary_statuses(); cx.spawn(async move |lsp_store, cx| { - while let Some((server_name, binary_status)) = server_statuses.next().await { + let _subscription = subscription; + while let Some((source, server_name, binary_status)) = server_statuses.next().await { if lsp_store - .update(cx, |_, cx| { + .update(cx, |lsp_store, cx| { + if source.is_some_and(|source| source != cx.entity_id()) + || source.is_none() + && !lsp_store.should_forward_untagged_binary_status(&server_name) + { + return; + } + let mut message = None; let binary_status = match binary_status { BinaryStatus::None => proto::ServerBinaryStatus::None, @@ -15208,9 +15244,16 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { Ok(()) } + fn status_source_id(&self) -> gpui::EntityId { + self.lsp_store.entity_id() + } + fn update_status(&self, server_name: LanguageServerName, status: language::BinaryStatus) { - self.language_registry - .update_lsp_binary_status(server_name, status); + self.language_registry.update_lsp_binary_status_for_entity( + self.status_source_id(), + server_name, + status, + ); } fn registered_lsp_adapters(&self) -> Vec> { diff --git a/crates/project/src/lsp_store/log_store.rs b/crates/project/src/lsp_store/log_store.rs index ef798c5cb2..c6da6b5cd9 100644 --- a/crates/project/src/lsp_store/log_store.rs +++ b/crates/project/src/lsp_store/log_store.rs @@ -1,6 +1,6 @@ use std::{ collections::VecDeque, - sync::Arc, + sync::{Arc, Weak}, time::{Duration, Instant}, }; @@ -35,7 +35,7 @@ impl Global for GlobalLogStore {} #[derive(Debug)] pub enum Event { NewServerLogEntry { - id: LanguageServerId, + key: LanguageServerLogKey, kind: LanguageServerLogType, text: String, }, @@ -46,12 +46,13 @@ impl EventEmitter for LogStore {} pub struct LogStore { on_headless_host: bool, projects: HashMap, ProjectState>, - pub language_servers: HashMap, - io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String, Instant)>, + pub language_servers: HashMap, + io_tx: mpsc::UnboundedSender<(LanguageServerLogKey, IoKind, String, Instant)>, } struct ProjectState { _subscriptions: [Subscription; 2], + copilot_server_id: Option, copilot_log_subscription: Option, } @@ -132,7 +133,7 @@ impl Message for RpcMessage { pub struct LanguageServerState { pub name: Option, pub worktree_id: Option, - pub kind: LanguageServerKind, + server: Option>, log_messages: VecDeque, trace_messages: VecDeque, pub rpc_state: Option, @@ -142,12 +143,17 @@ pub struct LanguageServerState { pub toggled_log_kind: Option, } +impl LanguageServerState { + pub fn server(&self) -> Option> { + self.server.as_ref()?.upgrade() + } +} + impl std::fmt::Debug for LanguageServerState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LanguageServerState") .field("name", &self.name) .field("worktree_id", &self.worktree_id) - .field("kind", &self.kind) .field("log_messages", &self.log_messages) .field("trace_messages", &self.trace_messages) .field("rpc_state", &self.rpc_state) @@ -158,12 +164,12 @@ impl std::fmt::Debug for LanguageServerState { } } -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Eq, Hash, Clone)] pub enum LanguageServerKind { Local { project: WeakEntity }, Remote { project: WeakEntity }, LocalSsh { lsp_store: WeakEntity }, - Global, + Supplementary { project: WeakEntity }, } impl std::fmt::Debug for LanguageServerKind { @@ -172,18 +178,61 @@ impl std::fmt::Debug for LanguageServerKind { LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), LanguageServerKind::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"), - LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), + LanguageServerKind::Supplementary { .. } => { + write!(f, "LanguageServerKind::Supplementary") + } } } } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct LanguageServerLogKey { + pub kind: LanguageServerKind, + pub server_id: LanguageServerId, +} + +impl LanguageServerLogKey { + pub fn new(kind: LanguageServerKind, server_id: LanguageServerId) -> Self { + Self { kind, server_id } + } + + pub fn is_for_project( + &self, + project: &WeakEntity, + lsp_store: &WeakEntity, + ) -> bool { + self.kind.is_for_project(project, lsp_store) + } +} + impl LanguageServerKind { pub fn project(&self) -> Option<&WeakEntity> { match self { - Self::Local { project } => Some(project), - Self::Remote { project } => Some(project), + Self::Local { project } + | Self::Remote { project } + | Self::Supplementary { project } => Some(project), Self::LocalSsh { .. } => None, - Self::Global { .. } => None, + } + } + + pub fn is_for_project( + &self, + project: &WeakEntity, + lsp_store: &WeakEntity, + ) -> bool { + match self { + Self::Local { + project: server_project, + } + | Self::Remote { + project: server_project, + } + | Self::Supplementary { + project: server_project, + } => server_project == project, + Self::LocalSsh { + lsp_store: server_lsp_store, + } => server_lsp_store == lsp_store, } } } @@ -400,10 +449,10 @@ impl LogStore { io_tx, }; cx.spawn(async move |log_store, cx| { - while let Some((server_id, io_kind, message, observed_at)) = io_rx.next().await { + while let Some((server_key, io_kind, message, observed_at)) = io_rx.next().await { if let Some(log_store) = log_store.upgrade() { log_store.update(cx, |log_store, cx| { - log_store.on_io(server_id, io_kind, &message, observed_at, cx); + log_store.on_io(&server_key, io_kind, &message, observed_at, cx); }); } } @@ -423,10 +472,11 @@ impl LogStore { cx.observe_release(project, move |this, _, _| { this.projects.remove(&weak_project); this.language_servers - .retain(|_, state| state.kind.project() != Some(&weak_project)); + .retain(|key, _| key.kind.project() != Some(&weak_project)); }), cx.subscribe(project, move |log_store, project, event, cx| { - let server_kind = if project.read(cx).is_local() { + let is_local = project.read(cx).is_local(); + let primary_server_kind = if is_local { LanguageServerKind::Local { project: project.downgrade(), } @@ -435,10 +485,32 @@ impl LogStore { project: project.downgrade(), } }; + let server_kind_for_id = |log_store: &LogStore, server_id| { + // Remote project events carry host-side server IDs, which may + // collide numerically with locally allocated supplementary + // server IDs, so only local projects may resolve an ID to a + // supplementary server. + if is_local { + let supplementary_server_kind = LanguageServerKind::Supplementary { + project: project.downgrade(), + }; + let supplementary_server_key = LanguageServerLogKey::new( + supplementary_server_kind.clone(), + server_id, + ); + if log_store + .language_servers + .contains_key(&supplementary_server_key) + { + return supplementary_server_kind; + } + } + primary_server_kind.clone() + }; match event { crate::Event::LanguageServerAdded(id, name, worktree_id) => { log_store.add_language_server( - server_kind, + primary_server_kind, *id, Some(name.clone()), *worktree_id, @@ -450,6 +522,22 @@ impl LogStore { cx, ); } + crate::Event::SupplementaryLanguageServerAdded(id, name) => { + log_store.add_language_server( + LanguageServerKind::Supplementary { + project: project.downgrade(), + }, + *id, + Some(name.clone()), + None, + project + .read(cx) + .lsp_store() + .read(cx) + .language_server_for_id(*id), + cx, + ); + } crate::Event::LanguageServerBufferRegistered { server_id, buffer_id, @@ -472,7 +560,7 @@ impl LogStore { .map(|status| status.name.clone()) }); log_store.add_language_server( - server_kind, + server_kind_for_id(log_store, *server_id), *server_id, name, worktree_id, @@ -481,9 +569,23 @@ impl LogStore { ); } crate::Event::LanguageServerRemoved(id) => { - log_store.remove_language_server(*id, cx); + let server_key = + LanguageServerLogKey::new(primary_server_kind, *id); + log_store.remove_language_server(&server_key, cx); + } + crate::Event::SupplementaryLanguageServerRemoved(id) => { + let server_key = LanguageServerLogKey::new( + LanguageServerKind::Supplementary { + project: project.downgrade(), + }, + *id, + ); + log_store.remove_language_server(&server_key, cx); } crate::Event::LanguageServerLog(id, typ, message) => { + let server_kind = server_kind_for_id(log_store, *id); + let server_key = + LanguageServerLogKey::new(server_kind.clone(), *id); log_store.add_language_server( server_kind, *id, @@ -494,11 +596,16 @@ impl LogStore { ); match typ { crate::LanguageServerLogType::Log(typ) => { - log_store.add_language_server_log(*id, *typ, message, cx); + log_store.add_language_server_log( + &server_key, + *typ, + message, + cx, + ); } crate::LanguageServerLogType::Trace { verbose_info } => { log_store.add_language_server_trace( - *id, + &server_key, message, verbose_info.clone(), cx, @@ -511,7 +618,7 @@ impl LogStore { MessageKind::Send }; log_store.add_language_server_rpc( - *id, + &server_key, kind, message, RpcTiming::Forwarded(*elapsed), @@ -525,12 +632,17 @@ impl LogStore { enabled, toggled_log_kind, } => { - log_store.toggle_lsp_logs(*server_id, *enabled, *toggled_log_kind); + let server_key = LanguageServerLogKey::new( + server_kind_for_id(log_store, *server_id), + *server_id, + ); + log_store.toggle_lsp_logs(&server_key, *enabled, *toggled_log_kind); } _ => {} } }), ], + copilot_server_id: None, copilot_log_subscription: None, }, ); @@ -538,9 +650,9 @@ impl LogStore { pub fn get_language_server_state( &mut self, - id: LanguageServerId, + key: &LanguageServerLogKey, ) -> Option<&mut LanguageServerState> { - self.language_servers.get_mut(&id) + self.language_servers.get_mut(key) } pub fn add_language_server( @@ -552,21 +664,25 @@ impl LogStore { server: Option>, cx: &mut Context, ) -> Option<&mut LanguageServerState> { - let server_state = self.language_servers.entry(server_id).or_insert_with(|| { - cx.notify(); - LanguageServerState { - name: None, - worktree_id: None, - kind, - rpc_state: None, - log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - trace_level: TraceValue::Off, - log_level: MessageType::LOG, - io_logs_subscription: None, - toggled_log_kind: None, - } - }); + let server_key = LanguageServerLogKey::new(kind, server_id); + let server_state = self + .language_servers + .entry(server_key.clone()) + .or_insert_with(|| { + cx.notify(); + LanguageServerState { + name: None, + worktree_id: None, + server: server.as_ref().map(Arc::downgrade), + rpc_state: None, + log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + trace_level: TraceValue::Off, + log_level: MessageType::LOG, + io_logs_subscription: None, + toggled_log_kind: None, + } + }); if let Some(name) = name { server_state.name = Some(name); @@ -575,15 +691,29 @@ impl LogStore { server_state.worktree_id = Some(worktree_id); } - if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { - let io_tx = self.io_tx.clone(); - let server_id = server.server_id(); - server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { - let observed_at = Instant::now(); - io_tx - .unbounded_send((server_id, io_kind, message.to_string(), observed_at)) - .ok(); - })); + if let Some(server) = server { + let is_new_server = match server_state.server() { + Some(current_server) => !Arc::ptr_eq(¤t_server, &server), + None => true, + }; + if is_new_server { + server_state.server = Some(Arc::downgrade(&server)); + server_state.io_logs_subscription = None; + } + if server_state.io_logs_subscription.is_none() { + let io_tx = self.io_tx.clone(); + server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { + let observed_at = Instant::now(); + io_tx + .unbounded_send(( + server_key.clone(), + io_kind, + message.to_string(), + observed_at, + )) + .ok(); + })); + } } Some(server_state) @@ -591,13 +721,13 @@ impl LogStore { pub fn add_language_server_log( &mut self, - id: LanguageServerId, + key: &LanguageServerLogKey, typ: MessageType, message: &str, cx: &mut Context, ) -> Option<()> { let store_logs = !self.on_headless_host; - let language_server_state = self.get_language_server_state(id)?; + let language_server_state = self.get_language_server_state(key)?; let log_lines = &mut language_server_state.log_messages; let message = message.trim_end().to_string(); @@ -605,7 +735,7 @@ impl LogStore { // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway self.emit_event( Event::NewServerLogEntry { - id, + key: key.clone(), kind: LanguageServerLogType::Log(typ), text: message, }, @@ -618,7 +748,7 @@ impl LogStore { ) { self.emit_event( Event::NewServerLogEntry { - id, + key: key.clone(), kind: LanguageServerLogType::Log(typ), text: new_message, }, @@ -630,20 +760,20 @@ impl LogStore { fn add_language_server_trace( &mut self, - id: LanguageServerId, + key: &LanguageServerLogKey, message: &str, verbose_info: Option, cx: &mut Context, ) -> Option<()> { let store_logs = !self.on_headless_host; - let language_server_state = self.get_language_server_state(id)?; + let language_server_state = self.get_language_server_state(key)?; let log_lines = &mut language_server_state.trace_messages; if !store_logs { // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway self.emit_event( Event::NewServerLogEntry { - id, + key: key.clone(), kind: LanguageServerLogType::Trace { verbose_info }, text: message.trim().to_string(), }, @@ -669,7 +799,7 @@ impl LogStore { } self.emit_event( Event::NewServerLogEntry { - id, + key: key.clone(), kind: LanguageServerLogType::Trace { verbose_info }, text: new_message, }, @@ -696,7 +826,7 @@ impl LogStore { fn add_language_server_rpc( &mut self, - language_server_id: LanguageServerId, + key: &LanguageServerLogKey, kind: MessageKind, message: &str, timing: RpcTiming, @@ -704,7 +834,7 @@ impl LogStore { ) { let store_logs = !self.on_headless_host; let Some(state) = self - .get_language_server_state(language_server_id) + .get_language_server_state(key) .and_then(|state| state.rpc_state.as_mut()) else { return; @@ -737,7 +867,7 @@ impl LogStore { if let Some(header) = header { // Do not send a synthetic message over the wire, it will be derived from the actual RPC message cx.emit(Event::NewServerLogEntry { - id: language_server_id, + key: key.clone(), kind: LanguageServerLogType::Rpc { received, elapsed: None, @@ -748,7 +878,7 @@ impl LogStore { self.emit_event( Event::NewServerLogEntry { - id: language_server_id, + key: key.clone(), kind: LanguageServerLogType::Rpc { received, elapsed }, text: message.to_owned(), }, @@ -756,44 +886,37 @@ impl LogStore { ); } - pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { - self.language_servers.remove(&id); + pub fn remove_language_server(&mut self, key: &LanguageServerLogKey, cx: &mut Context) { + self.language_servers.remove(key); cx.notify(); } - pub fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque> { - Some(&self.language_servers.get(&server_id)?.log_messages) + pub fn server_logs(&self, key: &LanguageServerLogKey) -> Option<&VecDeque> { + Some(&self.language_servers.get(key)?.log_messages) } - pub fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque> { - Some(&self.language_servers.get(&server_id)?.trace_messages) + pub fn server_trace(&self, key: &LanguageServerLogKey) -> Option<&VecDeque> { + Some(&self.language_servers.get(key)?.trace_messages) } - pub fn server_ids_for_project<'a>( + pub fn server_keys_for_project<'a>( &'a self, - lookup_project: &'a WeakEntity, - ) -> impl Iterator + 'a { + project: &'a WeakEntity, + lsp_store: &'a WeakEntity, + ) -> impl Iterator + 'a { self.language_servers - .iter() - .filter_map(move |(id, state)| match &state.kind { - LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => { - if project == lookup_project { - Some(*id) - } else { - None - } - } - LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id), - }) + .keys() + .filter(move |key| key.is_for_project(project, lsp_store)) + .cloned() } pub fn enable_rpc_trace_for_language_server( &mut self, - server_id: LanguageServerId, + key: &LanguageServerLogKey, ) -> Option<&mut LanguageServerRpcState> { let rpc_state = self .language_servers - .get_mut(&server_id)? + .get_mut(key)? .rpc_state .get_or_insert_with(|| LanguageServerRpcState { rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), @@ -805,25 +928,30 @@ impl LogStore { pub fn disable_rpc_trace_for_language_server( &mut self, - server_id: LanguageServerId, + key: &LanguageServerLogKey, ) -> Option<()> { - self.language_servers.get_mut(&server_id)?.rpc_state.take(); + self.language_servers.get_mut(key)?.rpc_state.take(); Some(()) } - pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool { - match server { - LanguageServerSelector::Id(id) => self.language_servers.contains_key(id), - LanguageServerSelector::Name(name) => self - .language_servers - .iter() - .any(|(_, state)| state.name.as_ref() == Some(name)), - } + pub fn has_server_logs( + &self, + server: &LanguageServerSelector, + project: &WeakEntity, + lsp_store: &WeakEntity, + ) -> bool { + self.language_servers.iter().any(|(key, state)| { + key.is_for_project(project, lsp_store) + && match server { + LanguageServerSelector::Id(id) => key.server_id == *id, + LanguageServerSelector::Name(name) => state.name.as_ref() == Some(name), + } + }) } fn on_io( &mut self, - language_server_id: LanguageServerId, + key: &LanguageServerLogKey, io_kind: IoKind, message: &str, observed_at: Instant, @@ -833,7 +961,7 @@ impl LogStore { IoKind::StdOut => true, IoKind::StdIn => false, IoKind::StdErr => { - self.add_language_server_log(language_server_id, MessageType::LOG, message, cx); + self.add_language_server_log(key, MessageType::LOG, message, cx); return Some(()); } }; @@ -844,36 +972,30 @@ impl LogStore { MessageKind::Send }; - self.add_language_server_rpc( - language_server_id, - kind, - message, - RpcTiming::ObservedAt(observed_at), - cx, - ); + self.add_language_server_rpc(key, kind, message, RpcTiming::ObservedAt(observed_at), cx); cx.notify(); Some(()) } fn emit_event(&mut self, e: Event, cx: &mut Context) { match &e { - Event::NewServerLogEntry { id, kind, text } => { - if let Some(state) = self.get_language_server_state(*id) { - let downstream_client = match &state.kind { + Event::NewServerLogEntry { key, kind, text } => { + if let Some(state) = self.get_language_server_state(key) { + let downstream_client = match &key.kind { LanguageServerKind::Remote { project } | LanguageServerKind::Local { project } => project .upgrade() .map(|project| project.read(cx).lsp_store()), LanguageServerKind::LocalSsh { lsp_store } => lsp_store.upgrade(), - LanguageServerKind::Global => None, + LanguageServerKind::Supplementary { .. } => None, } .and_then(|lsp_store| lsp_store.read(cx).downstream_client()); if let Some((client, project_id)) = downstream_client { - if Some(LogKind::from_server_log_type(kind)) == state.toggled_log_kind { + if state.toggled_log_kind == Some(LogKind::from_server_log_type(kind)) { client .send(proto::LanguageServerLog { project_id, - language_server_id: id.to_proto(), + language_server_id: key.server_id.to_proto(), message: text.clone(), log_type: Some(kind.to_proto()), }) @@ -889,31 +1011,88 @@ impl LogStore { pub fn toggle_lsp_logs( &mut self, - server_id: LanguageServerId, + key: &LanguageServerLogKey, enabled: bool, toggled_log_kind: LogKind, ) { - if let Some(server_state) = self.get_language_server_state(server_id) { + if let Some(server_state) = self.get_language_server_state(key) { if enabled { server_state.toggled_log_kind = Some(toggled_log_kind); } else { server_state.toggled_log_kind = None; } } - if LogKind::Rpc == toggled_log_kind { + if toggled_log_kind == LogKind::Rpc { if enabled { - self.enable_rpc_trace_for_language_server(server_id); + self.enable_rpc_trace_for_language_server(key); } else { - self.disable_rpc_trace_for_language_server(server_id); + self.disable_rpc_trace_for_language_server(key); } } } - pub fn copilot_state_for_project( + pub fn sync_copilot_for_project( &mut self, project: &WeakEntity, - ) -> Option<&mut Option> { - self.projects - .get_mut(project) - .map(|project| &mut project.copilot_log_subscription) + server: Option>, + cx: &mut Context, + ) -> Option<()> { + let server_kind = LanguageServerKind::Supplementary { + project: project.clone(), + }; + let current_server_id = server.as_ref().map(|server| server.server_id()); + let current_server_matches = server.as_ref().is_some_and(|server| { + let server_key = LanguageServerLogKey::new(server_kind.clone(), server.server_id()); + self.language_servers + .get(&server_key) + .and_then(|state| state.server()) + .is_some_and(|current_server| Arc::ptr_eq(¤t_server, server)) + }); + let project_state = self.projects.get_mut(project)?; + if project_state.copilot_server_id == current_server_id && current_server_matches { + return Some(()); + } + + project_state.copilot_log_subscription = None; + let previous_server_id = project_state.copilot_server_id.take(); + if previous_server_id != current_server_id + && let Some(previous_server_id) = previous_server_id + { + let previous_server_key = + LanguageServerLogKey::new(server_kind.clone(), previous_server_id); + self.remove_language_server(&previous_server_key, cx); + } + + let Some(server) = server else { + return Some(()); + }; + let server_id = server.server_id(); + let server_key = LanguageServerLogKey::new(server_kind.clone(), server_id); + let weak_log_store = cx.weak_entity(); + let log_subscription = + server.on_notification::(move |params, cx| { + weak_log_store + .update(cx, |log_store, cx| { + log_store.add_language_server_log( + &server_key, + MessageType::LOG, + ¶ms.message, + cx, + ); + }) + .ok(); + }); + self.add_language_server( + server_kind, + server_id, + Some(LanguageServerName::new_static("copilot")), + None, + Some(server), + cx, + ); + + let project_state = self.projects.get_mut(project)?; + project_state.copilot_server_id = Some(server_id); + project_state.copilot_log_subscription = Some(log_subscription); + Some(()) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 186af48708..7e94d87c76 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -336,7 +336,9 @@ pub struct ToastLink { #[derive(Clone, Debug, PartialEq)] pub enum Event { LanguageServerAdded(LanguageServerId, LanguageServerName, Option), + SupplementaryLanguageServerAdded(LanguageServerId, LanguageServerName), LanguageServerRemoved(LanguageServerId), + SupplementaryLanguageServerRemoved(LanguageServerId), LanguageServerLog(LanguageServerId, LanguageServerLogType, String), // [`lsp::notification::DidOpenTextDocument`] was sent to this server using the buffer data. // Zed's buffer-related data is updated accordingly. @@ -3696,9 +3698,15 @@ impl Project { LspStoreEvent::LanguageServerAdded(server_id, name, worktree_id) => cx.emit( Event::LanguageServerAdded(*server_id, name.clone(), *worktree_id), ), + LspStoreEvent::SupplementaryLanguageServerAdded(server_id, name) => cx.emit( + Event::SupplementaryLanguageServerAdded(*server_id, name.clone()), + ), LspStoreEvent::LanguageServerRemoved(server_id) => { cx.emit(Event::LanguageServerRemoved(*server_id)) } + LspStoreEvent::SupplementaryLanguageServerRemoved(server_id) => { + cx.emit(Event::SupplementaryLanguageServerRemoved(*server_id)) + } LspStoreEvent::LanguageServerLog(server_id, log_type, string) => cx.emit( Event::LanguageServerLog(*server_id, log_type.clone(), string.clone()), ), @@ -6260,13 +6268,6 @@ impl Project { Ok(()) } - pub fn supplementary_language_servers<'a>( - &'a self, - cx: &'a App, - ) -> impl 'a + Iterator { - self.lsp_store.read(cx).supplementary_language_servers() - } - pub fn any_language_server_supports_inlay_hints(&self, buffer: &Buffer, cx: &mut App) -> bool { let Some(language) = buffer.language().cloned() else { return false; diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 7f916b3c81..370368e2a0 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -22,7 +22,9 @@ use project::{ debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore}, git_store::GitStore, image_store::ImageId, - lsp_store::log_store::{self, GlobalLogStore, LanguageServerKind, LogKind}, + lsp_store::log_store::{ + self, GlobalLogStore, LanguageServerKind, LanguageServerLogKey, LogKind, + }, project_settings::SettingsObserver, search::SearchQuery, task_store::TaskStore, @@ -408,13 +410,39 @@ impl HeadlessProject { }); } } - LspStoreEvent::LanguageServerRemoved(id) => { + LspStoreEvent::SupplementaryLanguageServerAdded(id, name) => { let log_store = cx .try_global::() .map(|lsp_logs| lsp_logs.0.clone()); if let Some(log_store) = log_store { log_store.update(cx, |log_store, cx| { - log_store.remove_language_server(*id, cx); + log_store.add_language_server( + LanguageServerKind::LocalSsh { + lsp_store: self.lsp_store.downgrade(), + }, + *id, + Some(name.clone()), + None, + lsp_store.read(cx).language_server_for_id(*id), + cx, + ); + }); + } + } + LspStoreEvent::LanguageServerRemoved(id) + | LspStoreEvent::SupplementaryLanguageServerRemoved(id) => { + let log_store = cx + .try_global::() + .map(|lsp_logs| lsp_logs.0.clone()); + if let Some(log_store) = log_store { + let server_key = LanguageServerLogKey::new( + LanguageServerKind::LocalSsh { + lsp_store: self.lsp_store.downgrade(), + }, + *id, + ); + log_store.update(cx, |log_store, cx| { + log_store.remove_language_server(&server_key, cx); }); } self.session @@ -837,11 +865,12 @@ impl HeadlessProject { } async fn handle_toggle_lsp_logs( - _: Entity, + this: Entity, envelope: TypedEnvelope, cx: AsyncApp, ) -> Result<()> { let server_id = LanguageServerId::from_proto(envelope.payload.server_id); + let lsp_store = this.read_with(&cx, |this, _| this.lsp_store.downgrade()); cx.update(|cx| { let log_store = cx .try_global::() @@ -856,8 +885,10 @@ impl HeadlessProject { proto::toggle_lsp_logs::LogType::Trace => LogKind::Trace, proto::toggle_lsp_logs::LogType::Rpc => LogKind::Rpc, }; + let server_key = + LanguageServerLogKey::new(LanguageServerKind::LocalSsh { lsp_store }, server_id); log_store.update(cx, |log_store, _| { - log_store.toggle_lsp_logs(server_id, envelope.payload.enabled, toggled_log_kind); + log_store.toggle_lsp_logs(&server_key, envelope.payload.enabled, toggled_log_kind); }); anyhow::Ok(()) })?; diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index bd5b6d16a3..2aaf99b9b4 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -36,13 +36,14 @@ use language::{ }; use lsp::{ CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT, - LanguageServerName, + LanguageServerId, LanguageServerName, }; use node_runtime::NodeRuntime; use project::{ - ProgressToken, Project, ProjectPath, + LanguageServerLogType, ProgressToken, Project, ProjectPath, agent_server_store::AgentServerCommand, image_store, + lsp_store::log_store::{LanguageServerKind, LanguageServerLogKey, LogStore}, search::{SearchQuery, SearchResult}, }; use remote::{ConnectionState, RemoteClient, RemoteClientEvent}; @@ -4692,6 +4693,77 @@ async fn test_remote_project_creation_notifies_new_entity_observers( assert!(project.read_with(cx, |project, _| project.is_remote())); } +#[gpui::test] +async fn test_log_store_keys_remote_events_by_primary_kind_on_supplementary_id_collision( + cx: &mut TestAppContext, + server_cx: &mut TestAppContext, +) { + let server_fs = Arc::new(FakeFs::new(server_cx.executor())); + server_fs + .insert_tree(path!("/code"), json!({ "project1": { "README.md": "" } })) + .await; + let (project, _headless) = init_test(&server_fs, cx, server_cx).await; + + let log_store = cx.new(|cx| LogStore::new(false, cx)); + log_store.update(cx, |log_store, cx| log_store.add_project(&project, cx)); + + // A supplementary server (e.g. Copilot) allocates its ID from the local + // registry, which may collide numerically with a host-side server ID. + let server_id = LanguageServerId(42); + let supplementary_server_key = LanguageServerLogKey::new( + LanguageServerKind::Supplementary { + project: project.downgrade(), + }, + server_id, + ); + log_store.update(cx, |log_store, cx| { + log_store.add_language_server( + LanguageServerKind::Supplementary { + project: project.downgrade(), + }, + server_id, + Some(LanguageServerName::new_static("copilot")), + None, + None, + cx, + ); + }); + + project.update(cx, |_, cx| { + cx.emit(project::Event::LanguageServerLog( + server_id, + LanguageServerLogType::Log(lsp::MessageType::LOG), + "host server log".to_string(), + )); + }); + cx.run_until_parked(); + + let remote_server_key = LanguageServerLogKey::new( + LanguageServerKind::Remote { + project: project.downgrade(), + }, + server_id, + ); + log_store.read_with(cx, |log_store, _| { + assert_eq!( + log_store.server_logs(&remote_server_key).map(|logs| { + logs.iter() + .map(|log| log.as_ref().to_string()) + .collect::>() + }), + Some(vec!["host server log".to_string()]), + "host server logs should be keyed by the remote server kind" + ); + assert_eq!( + log_store + .server_logs(&supplementary_server_key) + .map(|logs| logs.len()), + Some(0), + "host server logs should not leak into the supplementary server with the same ID" + ); + }); +} + pub async fn init_test( server_fs: &Arc, cx: &mut TestAppContext, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ebe3776df3..3a1704b2a6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -602,12 +602,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut App) { let diagnostic_summary = cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); let active_file_name = cx.new(|_| workspace::active_file_name::ActiveFileName::new()); - let activity_indicator = activity_indicator::ActivityIndicator::new( - workspace, - workspace.project().read(cx).languages().clone(), - window, - cx, - ); + let activity_indicator = activity_indicator::ActivityIndicator::new(workspace, window, cx); let active_buffer_encoding = cx.new(|_| encoding_selector::ActiveBufferEncoding::new(workspace)); let active_buffer_language = @@ -2948,9 +2943,8 @@ mod tests { cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let workspace = multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone()); - let languages = project.read_with(cx, |project, _| project.languages().clone()); let indicator = workspace.update_in(cx, |workspace, window, cx| { - activity_indicator::ActivityIndicator::new(workspace, languages, window, cx) + activity_indicator::ActivityIndicator::new(workspace, window, cx) }); cx.run_until_parked();