diff --git a/Cargo.lock b/Cargo.lock index 979fc9441c..4684bec47e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "gpui", "language", "project", + "proto", "release_channel", "smallvec", "ui", @@ -9025,6 +9026,7 @@ dependencies = [ "itertools 0.14.0", "language", "lsp", + "picker", "project", "release_channel", "serde_json", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 23a1aead68..0c4de0e053 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -41,7 +41,8 @@ "shift-f11": "debugger::StepOut", "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RateCompletions", - "ctrl-shift-i": "edit_prediction::ToggleMenu" + "ctrl-shift-i": "edit_prediction::ToggleMenu", + "ctrl-alt-l": "lsp_tool::ToggleMenu" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 785103aa92..5bd99963bd 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -47,7 +47,8 @@ "fn-f": "zed::ToggleFullScreen", "ctrl-cmd-f": "zed::ToggleFullScreen", "ctrl-cmd-z": "edit_prediction::RateCompletions", - "ctrl-cmd-i": "edit_prediction::ToggleMenu" + "ctrl-cmd-i": "edit_prediction::ToggleMenu", + "ctrl-cmd-l": "lsp_tool::ToggleMenu" } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 858055fbe6..1b9a19615d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1720,6 +1720,11 @@ // } // } }, + // Common language server settings. + "global_lsp_settings": { + // Whether to show the LSP servers button in the status bar. + "button": true + }, // Jupyter settings "jupyter": { "enabled": true diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 778cf472df..3a80f012f9 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -21,6 +21,7 @@ futures.workspace = true gpui.workspace = true language.workspace = true project.workspace = true +proto.workspace = true smallvec.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 24762cb727..b3287e8222 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -80,10 +80,13 @@ impl ActivityIndicator { let this = cx.new(|cx| { let mut status_events = languages.language_server_binary_statuses(); cx.spawn(async move |this, cx| { - while let Some((name, status)) = status_events.next().await { + 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 }); + this.statuses.push(ServerStatus { + name, + status: LanguageServerStatusUpdate::Binary(binary_status), + }); cx.notify(); })?; } @@ -112,8 +115,76 @@ impl ActivityIndicator { cx.subscribe( &project.read(cx).lsp_store(), - |_, _, event, cx| match event { - LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(), + |activity_indicator, _, event, cx| match event { + LspStoreEvent::LanguageServerUpdate { name, message, .. } => { + if let proto::update_language_server::Variant::StatusUpdate(status_update) = + message + { + let Some(name) = name.clone() else { + return; + }; + let status = match &status_update.status { + Some(proto::status_update::Status::Binary(binary_status)) => { + if let Some(binary_status) = + proto::ServerBinaryStatus::from_i32(*binary_status) + { + let binary_status = match binary_status { + proto::ServerBinaryStatus::None => BinaryStatus::None, + proto::ServerBinaryStatus::CheckingForUpdate => { + BinaryStatus::CheckingForUpdate + } + proto::ServerBinaryStatus::Downloading => { + BinaryStatus::Downloading + } + proto::ServerBinaryStatus::Starting => { + BinaryStatus::Starting + } + proto::ServerBinaryStatus::Stopping => { + BinaryStatus::Stopping + } + proto::ServerBinaryStatus::Stopped => { + BinaryStatus::Stopped + } + proto::ServerBinaryStatus::Failed => { + let Some(error) = status_update.message.clone() + else { + return; + }; + BinaryStatus::Failed { error } + } + }; + LanguageServerStatusUpdate::Binary(binary_status) + } else { + return; + } + } + Some(proto::status_update::Status::Health(health_status)) => { + if let Some(health) = + proto::ServerHealth::from_i32(*health_status) + { + let health = match health { + proto::ServerHealth::Ok => ServerHealth::Ok, + proto::ServerHealth::Warning => ServerHealth::Warning, + proto::ServerHealth::Error => ServerHealth::Error, + }; + LanguageServerStatusUpdate::Health( + health, + status_update.message.clone().map(SharedString::from), + ) + } else { + return; + } + } + None => return, + }; + + activity_indicator.statuses.retain(|s| s.name != name); + activity_indicator + .statuses + .push(ServerStatus { name, status }); + } + cx.notify() + } _ => {} }, ) @@ -228,9 +299,23 @@ impl ActivityIndicator { _: &mut Window, cx: &mut Context, ) { - if let Some(updater) = &self.auto_updater { - updater.update(cx, |updater, cx| updater.dismiss_error(cx)); + let error_dismissed = if let Some(updater) = &self.auto_updater { + updater.update(cx, |updater, cx| updater.dismiss_error(cx)) + } else { + false + }; + if error_dismissed { + return; } + + self.project.update(cx, |project, cx| { + if project.last_formatting_failure(cx).is_some() { + project.reset_last_formatting_failure(cx); + true + } else { + false + } + }); } fn pending_language_server_work<'a>( @@ -399,6 +484,12 @@ impl ActivityIndicator { let mut servers_to_clear_statuses = HashSet::::default(); for status in &self.statuses { match &status.status { + LanguageServerStatusUpdate::Binary( + BinaryStatus::Starting | BinaryStatus::Stopping, + ) => {} + LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => { + servers_to_clear_statuses.insert(status.name.clone()); + } LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => { checking_for_update.push(status.name.clone()); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6b84ca998e..22daab491c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2008,6 +2008,7 @@ async fn join_project( session.connection_id, proto::UpdateLanguageServer { project_id: project_id.to_proto(), + server_name: Some(language_server.name.clone()), language_server_id: language_server.id, variant: Some( proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ffb08e4290..ea30cc6fab 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16164,7 +16164,7 @@ impl Editor { }) } - fn restart_language_server( + pub fn restart_language_server( &mut self, _: &RestartLanguageServer, _: &mut Window, @@ -16175,6 +16175,7 @@ impl Editor { project.update(cx, |project, cx| { project.restart_language_servers_for_buffers( multi_buffer.all_buffers().into_iter().collect(), + HashSet::default(), cx, ); }); @@ -16182,7 +16183,7 @@ impl Editor { } } - fn stop_language_server( + pub fn stop_language_server( &mut self, _: &StopLanguageServer, _: &mut Window, @@ -16193,6 +16194,7 @@ impl Editor { project.update(cx, |project, cx| { project.stop_language_servers_for_buffers( multi_buffer.all_buffers().into_iter().collect(), + HashSet::default(), cx, ); cx.emit(project::Event::RefreshInlayHints); diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index cea3f0dbc3..cfe97f1675 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -4,13 +4,13 @@ use crate::{ GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION, SchemaVersion, }; use async_compression::futures::bufread::GzipEncoder; -use collections::BTreeMap; +use collections::{BTreeMap, HashSet}; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs, RealFs}; use futures::{AsyncReadExt, StreamExt, io::BufReader}; use gpui::{AppContext as _, SemanticVersion, TestAppContext}; use http_client::{FakeHttpClient, Response}; -use language::{BinaryStatus, LanguageMatcher, LanguageRegistry, LanguageServerStatusUpdate}; +use language::{BinaryStatus, LanguageMatcher, LanguageRegistry}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; use parking_lot::Mutex; @@ -720,20 +720,22 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { status_updates.next().await.unwrap(), status_updates.next().await.unwrap(), status_updates.next().await.unwrap(), + status_updates.next().await.unwrap(), ], [ ( LanguageServerName::new_static("gleam"), - LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) + BinaryStatus::Starting ), ( LanguageServerName::new_static("gleam"), - LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) + BinaryStatus::CheckingForUpdate ), ( LanguageServerName::new_static("gleam"), - LanguageServerStatusUpdate::Binary(BinaryStatus::None) - ) + BinaryStatus::Downloading + ), + (LanguageServerName::new_static("gleam"), BinaryStatus::None) ] ); @@ -794,7 +796,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { // Start a new instance of the language server. project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], cx) + project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx) }); cx.executor().run_until_parked(); @@ -816,7 +818,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { cx.executor().run_until_parked(); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], cx) + project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx) }); // The extension re-fetches the latest version of the language server. diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index a4b77eff74..635876dace 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -413,10 +413,6 @@ impl PickerDelegate for BranchListDelegate { cx.emit(DismissEvent); } - fn render_header(&self, _: &mut Window, _cx: &mut Context>) -> Option { - None - } - fn render_match( &self, ix: usize, diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index 322e623e60..b5865e9a85 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -1,6 +1,4 @@ -use gpui::{ - AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, -}; +use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity}; use itertools::Itertools; use picker::{Picker, PickerDelegate}; use project::{Project, git_store::Repository}; @@ -207,15 +205,6 @@ impl PickerDelegate for RepositorySelectorDelegate { .ok(); } - fn render_header( - &self, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - // TODO: Implement header rendering if needed - None - } - fn render_match( &self, ix: usize, diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index c157cd9e73..b2bb684e1b 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -157,6 +157,9 @@ pub enum BinaryStatus { None, CheckingForUpdate, Downloading, + Starting, + Stopping, + Stopped, Failed { error: String }, } @@ -248,7 +251,7 @@ pub struct LanguageQueries { #[derive(Clone, Default)] struct ServerStatusSender { - txs: Arc>>>, + txs: Arc>>>, } pub struct LoadedLanguage { @@ -1085,11 +1088,7 @@ impl LanguageRegistry { self.state.read().all_lsp_adapters.get(name).cloned() } - pub fn update_lsp_status( - &self, - server_name: LanguageServerName, - status: LanguageServerStatusUpdate, - ) { + pub fn update_lsp_binary_status(&self, server_name: LanguageServerName, status: BinaryStatus) { self.lsp_binary_status_tx.send(server_name, status); } @@ -1145,7 +1144,7 @@ impl LanguageRegistry { pub fn language_server_binary_statuses( &self, - ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerStatusUpdate)> { + ) -> mpsc::UnboundedReceiver<(LanguageServerName, BinaryStatus)> { self.lsp_binary_status_tx.subscribe() } @@ -1260,15 +1259,13 @@ impl LanguageRegistryState { } impl ServerStatusSender { - fn subscribe( - &self, - ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerStatusUpdate)> { + fn subscribe(&self) -> mpsc::UnboundedReceiver<(LanguageServerName, BinaryStatus)> { let (tx, rx) = mpsc::unbounded(); self.txs.lock().push(tx); rx } - fn send(&self, name: LanguageServerName, status: LanguageServerStatusUpdate) { + 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()); } diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index a32292daa3..d2eabf0a3e 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -12,8 +12,8 @@ use fs::Fs; use futures::{Future, FutureExt}; use gpui::AsyncApp; use language::{ - BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageServerStatusUpdate, - LanguageToolchainStore, LspAdapter, LspAdapterDelegate, + BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, + LspAdapter, LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName}; use serde::Serialize; @@ -82,10 +82,8 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy { language_server_id: LanguageServerName, status: BinaryStatus, ) { - self.language_registry.update_lsp_status( - language_server_id, - LanguageServerStatusUpdate::Binary(status), - ); + self.language_registry + .update_lsp_binary_status(language_server_id, status); } } diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index cb07b46215..3a0f487f7a 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] anyhow.workspace = true +client.workspace = true collections.workspace = true copilot.workspace = true editor.workspace = true @@ -22,18 +23,19 @@ gpui.workspace = true itertools.workspace = true language.workspace = true lsp.workspace = true +picker.workspace = true project.workspace = true serde_json.workspace = true settings.workspace = true theme.workspace = true tree-sitter.workspace = true ui.workspace = true +util.workspace = true workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true [dev-dependencies] -client = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } release_channel.workspace = true gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs index 6b18103c24..cbf5756875 100644 --- a/crates/language_tools/src/language_tools.rs +++ b/crates/language_tools/src/language_tools.rs @@ -1,17 +1,54 @@ mod key_context_view; mod lsp_log; +pub mod lsp_tool; mod syntax_tree_view; #[cfg(test)] mod lsp_log_tests; -use gpui::App; +use gpui::{App, AppContext, Entity}; pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView}; pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView}; +use ui::{Context, Window}; +use workspace::{Item, ItemHandle, SplitDirection, Workspace}; pub fn init(cx: &mut App) { lsp_log::init(cx); syntax_tree_view::init(cx); key_context_view::init(cx); } + +fn get_or_create_tool( + workspace: &mut Workspace, + destination: SplitDirection, + window: &mut Window, + cx: &mut Context, + new_tool: impl FnOnce(&mut Window, &mut Context) -> T, +) -> Entity +where + T: Item, +{ + if let Some(item) = workspace.item_of_type::(cx) { + return item; + } + + let new_tool = cx.new(|cx| new_tool(window, cx)); + match workspace.find_pane_in_direction(destination, cx) { + Some(right_pane) => { + workspace.add_item( + right_pane, + new_tool.boxed_clone(), + None, + true, + true, + window, + cx, + ); + } + None => { + workspace.split_item(destination, new_tool.boxed_clone(), window, cx); + } + } + new_tool +} diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index bddfbc5c71..de474c1d9f 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -3,14 +3,14 @@ use copilot::Copilot; use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll}; use futures::{StreamExt, channel::mpsc}; use gpui::{ - AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, - ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div, + AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, Global, + IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div, }; use itertools::Itertools; use language::{LanguageServerId, language_settings::SoftWrap}; use lsp::{ - IoKind, LanguageServer, LanguageServerName, MessageType, SetTraceParams, TraceValue, - notification::SetTrace, + IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType, + SetTraceParams, TraceValue, notification::SetTrace, }; use project::{Project, WorktreeId, search::SearchQuery}; use std::{any::TypeId, borrow::Cow, sync::Arc}; @@ -21,6 +21,8 @@ use workspace::{ searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; +use crate::get_or_create_tool; + const SEND_LINE: &str = "\n// Send:"; const RECEIVE_LINE: &str = "\n// Receive:"; const MAX_STORED_LOG_ENTRIES: usize = 2000; @@ -44,7 +46,7 @@ trait Message: AsRef { } } -struct LogMessage { +pub(super) struct LogMessage { message: String, typ: MessageType, } @@ -71,7 +73,7 @@ impl Message for LogMessage { } } -struct TraceMessage { +pub(super) struct TraceMessage { message: String, } @@ -99,7 +101,7 @@ impl Message for RpcMessage { type Level = (); } -struct LanguageServerState { +pub(super) struct LanguageServerState { name: Option, worktree_id: Option, kind: LanguageServerKind, @@ -204,8 +206,13 @@ pub(crate) struct LogMenuItem { actions!(dev, [OpenLanguageServerLogs]); +pub(super) struct GlobalLogStore(pub WeakEntity); + +impl Global for GlobalLogStore {} + pub fn init(cx: &mut App) { let log_store = cx.new(LogStore::new); + cx.set_global(GlobalLogStore(log_store.downgrade())); cx.observe_new(move |workspace: &mut Workspace, _, cx| { let project = workspace.project(); @@ -219,13 +226,14 @@ pub fn init(cx: &mut App) { workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { let project = workspace.project().read(cx); if project.is_local() || project.is_via_ssh() { - workspace.split_item( + let project = workspace.project().clone(); + let log_store = log_store.clone(); + get_or_create_tool( + workspace, SplitDirection::Right, - Box::new(cx.new(|cx| { - LspLogView::new(workspace.project().clone(), log_store.clone(), window, cx) - })), window, cx, + move |window, cx| LspLogView::new(project, log_store, window, cx), ); } }); @@ -354,7 +362,7 @@ impl LogStore { ); } - fn get_language_server_state( + pub(super) fn get_language_server_state( &mut self, id: LanguageServerId, ) -> Option<&mut LanguageServerState> { @@ -480,11 +488,14 @@ impl LogStore { cx.notify(); } - fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque> { + pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque> { Some(&self.language_servers.get(&server_id)?.log_messages) } - fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque> { + pub(super) fn server_trace( + &self, + server_id: LanguageServerId, + ) -> Option<&VecDeque> { Some(&self.language_servers.get(&server_id)?.trace_messages) } @@ -529,6 +540,110 @@ impl LogStore { 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 open_server_log( + &mut self, + workspace: WeakEntity, + server: LanguageServerSelector, + window: &mut Window, + cx: &mut Context, + ) { + cx.spawn_in(window, async move |log_store, cx| { + let Some(log_store) = log_store.upgrade() else { + return; + }; + workspace + .update_in(cx, |workspace, window, cx| { + let project = workspace.project().clone(); + let tool_log_store = log_store.clone(); + let log_view = get_or_create_tool( + workspace, + SplitDirection::Right, + window, + cx, + 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 + } + }, + ) + } + }; + if let Some(server_id) = server_id { + log_view.show_logs_for_server(server_id, window, cx); + } + }); + }) + .ok(); + }) + .detach(); + } + + pub fn open_server_trace( + &mut self, + workspace: WeakEntity, + server: LanguageServerSelector, + window: &mut Window, + cx: &mut Context, + ) { + cx.spawn_in(window, async move |log_store, cx| { + let Some(log_store) = log_store.upgrade() else { + return; + }; + workspace + .update_in(cx, |workspace, window, cx| { + let project = workspace.project().clone(); + let tool_log_store = log_store.clone(); + let log_view = get_or_create_tool( + workspace, + SplitDirection::Right, + window, + cx, + 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 + } + }, + ) + } + }; + if let Some(server_id) = server_id { + log_view.show_rpc_trace_for_server(server_id, window, cx); + } + }); + }) + .ok(); + }) + .detach(); + } + fn on_io( &mut self, language_server_id: LanguageServerId, @@ -856,7 +971,7 @@ impl LspLogView { self.editor_subscriptions = editor_subscriptions; cx.notify(); } - window.focus(&self.focus_handle); + self.editor.read(cx).focus_handle(cx).focus(window); } fn update_log_level( @@ -882,7 +997,7 @@ impl LspLogView { cx.notify(); } - window.focus(&self.focus_handle); + self.editor.read(cx).focus_handle(cx).focus(window); } fn show_trace_for_server( @@ -904,7 +1019,7 @@ impl LspLogView { self.editor_subscriptions = editor_subscriptions; cx.notify(); } - window.focus(&self.focus_handle); + self.editor.read(cx).focus_handle(cx).focus(window); } fn show_rpc_trace_for_server( @@ -947,7 +1062,7 @@ impl LspLogView { cx.notify(); } - window.focus(&self.focus_handle); + self.editor.read(cx).focus_handle(cx).focus(window); } fn toggle_rpc_trace_for_server( @@ -1011,7 +1126,7 @@ impl LspLogView { self.editor = editor; self.editor_subscriptions = editor_subscriptions; cx.notify(); - window.focus(&self.focus_handle); + self.editor.read(cx).focus_handle(cx).focus(window); } } diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs new file mode 100644 index 0000000000..fc1efc7794 --- /dev/null +++ b/crates/language_tools/src/lsp_tool.rs @@ -0,0 +1,917 @@ +use std::{collections::hash_map, path::PathBuf, sync::Arc, time::Duration}; + +use client::proto; +use collections::{HashMap, HashSet}; +use editor::{Editor, EditorEvent}; +use gpui::{Corner, DismissEvent, Entity, Focusable as _, Subscription, Task, WeakEntity, actions}; +use language::{BinaryStatus, BufferId, LocalFile, ServerHealth}; +use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; +use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu}; +use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings}; +use settings::{Settings as _, SettingsStore}; +use ui::{Context, IconButtonShape, Indicator, Tooltip, Window, prelude::*}; + +use workspace::{StatusItemView, Workspace}; + +use crate::lsp_log::GlobalLogStore; + +actions!(lsp_tool, [ToggleMenu]); + +pub struct LspTool { + state: Entity, + lsp_picker: Option>>, + _subscriptions: Vec, +} + +struct PickerState { + workspace: WeakEntity, + lsp_store: WeakEntity, + active_editor: Option, + language_servers: LanguageServers, +} + +#[derive(Debug)] +struct LspPickerDelegate { + state: Entity, + selected_index: usize, + items: Vec, + other_servers_start_index: Option, +} + +struct ActiveEditor { + editor: WeakEntity, + _editor_subscription: Subscription, + editor_buffers: HashSet, +} + +#[derive(Debug, Default, Clone)] +struct LanguageServers { + health_statuses: HashMap, + binary_statuses: HashMap, + servers_per_buffer_abs_path: + HashMap>>, +} + +#[derive(Debug, Clone)] +struct LanguageServerHealthStatus { + name: LanguageServerName, + health: Option<(Option, ServerHealth)>, +} + +#[derive(Debug, Clone)] +struct LanguageServerBinaryStatus { + status: BinaryStatus, + message: Option, +} + +impl LanguageServerHealthStatus { + fn health(&self) -> Option { + self.health.as_ref().map(|(_, health)| *health) + } + + fn message(&self) -> Option { + self.health + .as_ref() + .and_then(|(message, _)| message.clone()) + } +} + +impl LspPickerDelegate { + fn regenerate_items(&mut self, cx: &mut Context>) { + self.state.update(cx, |state, cx| { + let editor_buffers = state + .active_editor + .as_ref() + .map(|active_editor| active_editor.editor_buffers.clone()) + .unwrap_or_default(); + let editor_buffer_paths = editor_buffers + .iter() + .filter_map(|buffer_id| { + let buffer_path = state + .lsp_store + .update(cx, |lsp_store, cx| { + Some( + project::File::from_dyn( + lsp_store + .buffer_store() + .read(cx) + .get(*buffer_id)? + .read(cx) + .file(), + )? + .abs_path(cx), + ) + }) + .ok()??; + Some(buffer_path) + }) + .collect::>(); + + let mut servers_with_health_checks = HashSet::default(); + let mut server_ids_with_health_checks = HashSet::default(); + let mut buffer_servers = + Vec::with_capacity(state.language_servers.health_statuses.len()); + let mut other_servers = + Vec::with_capacity(state.language_servers.health_statuses.len()); + let buffer_server_ids = editor_buffer_paths + .iter() + .filter_map(|buffer_path| { + state + .language_servers + .servers_per_buffer_abs_path + .get(buffer_path) + }) + .flatten() + .fold(HashMap::default(), |mut acc, (server_id, name)| { + match acc.entry(*server_id) { + hash_map::Entry::Occupied(mut o) => { + let old_name: &mut Option<&LanguageServerName> = o.get_mut(); + if old_name.is_none() { + *old_name = name.as_ref(); + } + } + hash_map::Entry::Vacant(v) => { + v.insert(name.as_ref()); + } + } + acc + }); + for (server_id, server_state) in &state.language_servers.health_statuses { + let binary_status = state + .language_servers + .binary_statuses + .get(&server_state.name); + servers_with_health_checks.insert(&server_state.name); + server_ids_with_health_checks.insert(*server_id); + if buffer_server_ids.contains_key(server_id) { + buffer_servers.push(ServerData::WithHealthCheck( + *server_id, + server_state, + binary_status, + )); + } else { + other_servers.push(ServerData::WithHealthCheck( + *server_id, + server_state, + binary_status, + )); + } + } + + for (server_name, status) in state + .language_servers + .binary_statuses + .iter() + .filter(|(name, _)| !servers_with_health_checks.contains(name)) + { + let has_matching_server = state + .language_servers + .servers_per_buffer_abs_path + .iter() + .filter(|(path, _)| editor_buffer_paths.contains(path)) + .flat_map(|(_, server_associations)| server_associations.iter()) + .any(|(_, name)| name.as_ref() == Some(server_name)); + if has_matching_server { + buffer_servers.push(ServerData::WithBinaryStatus(server_name, status)); + } else { + other_servers.push(ServerData::WithBinaryStatus(server_name, status)); + } + } + + buffer_servers.sort_by_key(|data| data.name().clone()); + other_servers.sort_by_key(|data| data.name().clone()); + let mut other_servers_start_index = None; + let mut new_lsp_items = + Vec::with_capacity(buffer_servers.len() + other_servers.len() + 2); + if !buffer_servers.is_empty() { + new_lsp_items.push(LspItem::Header(SharedString::new("Current Buffer"))); + new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item)); + } + if !other_servers.is_empty() { + other_servers_start_index = Some(new_lsp_items.len()); + new_lsp_items.push(LspItem::Header(SharedString::new("Other Active Servers"))); + new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item)); + } + + self.items = new_lsp_items; + self.other_servers_start_index = other_servers_start_index; + }); + } +} + +impl LanguageServers { + fn update_binary_status( + &mut self, + binary_status: BinaryStatus, + message: Option<&str>, + name: LanguageServerName, + ) { + let binary_status_message = message.map(SharedString::new); + if matches!( + binary_status, + BinaryStatus::Stopped | BinaryStatus::Failed { .. } + ) { + self.health_statuses.retain(|_, server| server.name != name); + } + self.binary_statuses.insert( + name, + LanguageServerBinaryStatus { + status: binary_status, + message: binary_status_message, + }, + ); + } + + fn update_server_health( + &mut self, + id: LanguageServerId, + health: ServerHealth, + message: Option<&str>, + name: Option, + ) { + if let Some(state) = self.health_statuses.get_mut(&id) { + state.health = Some((message.map(SharedString::new), health)); + if let Some(name) = name { + state.name = name; + } + } else if let Some(name) = name { + self.health_statuses.insert( + id, + LanguageServerHealthStatus { + health: Some((message.map(SharedString::new), health)), + name, + }, + ); + } + } +} + +#[derive(Debug)] +enum ServerData<'a> { + WithHealthCheck( + LanguageServerId, + &'a LanguageServerHealthStatus, + Option<&'a LanguageServerBinaryStatus>, + ), + WithBinaryStatus(&'a LanguageServerName, &'a LanguageServerBinaryStatus), +} + +#[derive(Debug)] +enum LspItem { + WithHealthCheck( + LanguageServerId, + LanguageServerHealthStatus, + Option, + ), + WithBinaryStatus(LanguageServerName, LanguageServerBinaryStatus), + Header(SharedString), +} + +impl ServerData<'_> { + fn name(&self) -> &LanguageServerName { + match self { + Self::WithHealthCheck(_, state, _) => &state.name, + Self::WithBinaryStatus(name, ..) => name, + } + } + + fn into_lsp_item(self) -> LspItem { + match self { + Self::WithHealthCheck(id, name, status) => { + LspItem::WithHealthCheck(id, name.clone(), status.cloned()) + } + Self::WithBinaryStatus(name, status) => { + LspItem::WithBinaryStatus(name.clone(), status.clone()) + } + } + } +} + +impl PickerDelegate for LspPickerDelegate { + type ListItem = AnyElement; + + fn match_count(&self) -> usize { + self.items.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { + self.selected_index = ix; + cx.notify(); + } + + fn update_matches( + &mut self, + _: String, + _: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + cx.spawn(async move |lsp_picker, cx| { + cx.background_executor() + .timer(Duration::from_millis(30)) + .await; + lsp_picker + .update(cx, |lsp_picker, cx| { + lsp_picker.delegate.regenerate_items(cx); + }) + .ok(); + }) + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + Arc::default() + } + + fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context>) {} + + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + _: bool, + _: &mut Window, + cx: &mut Context>, + ) -> Option { + let is_other_server = self + .other_servers_start_index + .map_or(false, |start| ix >= start); + let server_binary_status; + let server_health; + let server_message; + let server_id; + let server_name; + match self.items.get(ix)? { + LspItem::WithHealthCheck( + language_server_id, + language_server_health_status, + language_server_binary_status, + ) => { + server_binary_status = language_server_binary_status.as_ref(); + server_health = language_server_health_status.health(); + server_message = language_server_health_status.message(); + server_id = Some(*language_server_id); + server_name = language_server_health_status.name.clone(); + } + LspItem::WithBinaryStatus(language_server_name, language_server_binary_status) => { + server_binary_status = Some(language_server_binary_status); + server_health = None; + server_message = language_server_binary_status.message.clone(); + server_id = None; + server_name = language_server_name.clone(); + } + LspItem::Header(header) => { + return Some( + h_flex() + .justify_center() + .child(Label::new(header.clone())) + .into_any_element(), + ); + } + }; + + let workspace = self.state.read(cx).workspace.clone(); + let lsp_logs = cx.global::().0.upgrade()?; + let lsp_store = self.state.read(cx).lsp_store.upgrade()?; + let server_selector = server_id + .map(LanguageServerSelector::Id) + .unwrap_or_else(|| LanguageServerSelector::Name(server_name.clone())); + let can_stop = server_binary_status.is_none_or(|status| { + matches!(status.status, BinaryStatus::None | BinaryStatus::Starting) + }); + // TODO currently, Zed remote does not work well with the LSP logs + // https://github.com/zed-industries/zed/issues/28557 + let has_logs = lsp_store.read(cx).as_local().is_some() + && lsp_logs.read(cx).has_server_logs(&server_selector); + let status_color = server_binary_status + .and_then(|binary_status| match binary_status.status { + BinaryStatus::None => None, + BinaryStatus::CheckingForUpdate + | BinaryStatus::Downloading + | BinaryStatus::Starting => Some(Color::Modified), + BinaryStatus::Stopping => Some(Color::Disabled), + BinaryStatus::Stopped => Some(Color::Disabled), + BinaryStatus::Failed { .. } => Some(Color::Error), + }) + .or_else(|| { + Some(match server_health? { + ServerHealth::Ok => Color::Success, + ServerHealth::Warning => Color::Warning, + ServerHealth::Error => Color::Error, + }) + }) + .unwrap_or(Color::Success); + + Some( + h_flex() + .w_full() + .justify_between() + .gap_2() + .child( + h_flex() + .id("server-status-indicator") + .gap_2() + .child(Indicator::dot().color(status_color)) + .child(Label::new(server_name.0.clone())) + .when_some(server_message.clone(), |div, server_message| { + div.tooltip(move |_, cx| Tooltip::simple(server_message.clone(), cx)) + }), + ) + .child( + h_flex() + .gap_1() + .when(has_logs, |div| { + div.child( + IconButton::new("debug-language-server", IconName::MessageBubbles) + .icon_size(IconSize::XSmall) + .tooltip(|_, cx| Tooltip::simple("Debug Language Server", cx)) + .on_click({ + let workspace = workspace.clone(); + let lsp_logs = lsp_logs.downgrade(); + let server_selector = server_selector.clone(); + move |_, window, cx| { + lsp_logs + .update(cx, |lsp_logs, cx| { + lsp_logs.open_server_trace( + workspace.clone(), + server_selector.clone(), + window, + cx, + ); + }) + .ok(); + } + }), + ) + }) + .when(can_stop, |div| { + div.child( + IconButton::new("stop-server", IconName::Stop) + .icon_size(IconSize::Small) + .tooltip(|_, cx| Tooltip::simple("Stop server", cx)) + .on_click({ + let lsp_store = lsp_store.downgrade(); + let server_selector = server_selector.clone(); + move |_, _, cx| { + lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.stop_language_servers_for_buffers( + Vec::new(), + HashSet::from_iter([ + server_selector.clone() + ]), + cx, + ); + }) + .ok(); + } + }), + ) + }) + .child( + IconButton::new("restart-server", IconName::Rerun) + .icon_size(IconSize::XSmall) + .tooltip(|_, cx| Tooltip::simple("Restart server", cx)) + .on_click({ + let state = self.state.clone(); + let workspace = workspace.clone(); + let lsp_store = lsp_store.downgrade(); + let editor_buffers = state + .read(cx) + .active_editor + .as_ref() + .map(|active_editor| active_editor.editor_buffers.clone()) + .unwrap_or_default(); + let server_selector = server_selector.clone(); + move |_, _, cx| { + if let Some(workspace) = workspace.upgrade() { + let project = workspace.read(cx).project().clone(); + let buffer_store = + project.read(cx).buffer_store().clone(); + let buffers = if is_other_server { + let worktree_store = + project.read(cx).worktree_store(); + state + .read(cx) + .language_servers + .servers_per_buffer_abs_path + .iter() + .filter_map(|(abs_path, servers)| { + if servers.values().any(|server| { + server.as_ref() == Some(&server_name) + }) { + worktree_store + .read(cx) + .find_worktree(abs_path, cx) + } else { + None + } + }) + .filter_map(|(worktree, relative_path)| { + let entry = worktree + .read(cx) + .entry_for_path(&relative_path)?; + project + .read(cx) + .path_for_entry(entry.id, cx) + }) + .filter_map(|project_path| { + buffer_store + .read(cx) + .get_by_path(&project_path) + }) + .collect::>() + } else { + editor_buffers + .iter() + .flat_map(|buffer_id| { + buffer_store.read(cx).get(*buffer_id) + }) + .collect::>() + }; + if !buffers.is_empty() { + lsp_store + .update(cx, |lsp_store, cx| { + lsp_store + .restart_language_servers_for_buffers( + buffers, + HashSet::from_iter([ + server_selector.clone(), + ]), + cx, + ); + }) + .ok(); + } + } + } + }), + ), + ) + .cursor_default() + .into_any_element(), + ) + } + + fn render_editor( + &self, + editor: &Entity, + _: &mut Window, + cx: &mut Context>, + ) -> Div { + div().child(div().track_focus(&editor.focus_handle(cx))) + } + + fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { + if self.items.is_empty() { + Some( + h_flex() + .w_full() + .border_color(cx.theme().colors().border_variant) + .child( + Button::new("stop-all-servers", "Stop all servers") + .disabled(true) + .on_click(move |_, _, _| {}) + .full_width(), + ) + .into_any_element(), + ) + } else { + let lsp_store = self.state.read(cx).lsp_store.clone(); + Some( + h_flex() + .w_full() + .border_color(cx.theme().colors().border_variant) + .child( + Button::new("stop-all-servers", "Stop all servers") + .on_click({ + move |_, _, cx| { + lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.stop_all_language_servers(cx); + }) + .ok(); + } + }) + .full_width(), + ) + .into_any_element(), + ) + } + } + + fn separators_after_indices(&self) -> Vec { + if self.items.is_empty() { + Vec::new() + } else { + vec![self.items.len() - 1] + } + } +} + +// TODO kb keyboard story +impl LspTool { + pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context) -> Self { + let settings_subscription = + cx.observe_global_in::(window, move |lsp_tool, window, cx| { + if ProjectSettings::get_global(cx).global_lsp_settings.button { + if lsp_tool.lsp_picker.is_none() { + lsp_tool.lsp_picker = + Some(Self::new_lsp_picker(lsp_tool.state.clone(), window, cx)); + cx.notify(); + return; + } + } else if lsp_tool.lsp_picker.take().is_some() { + cx.notify(); + } + }); + + let lsp_store = workspace.project().read(cx).lsp_store(); + let lsp_store_subscription = + cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| { + lsp_tool.on_lsp_store_event(e, window, cx) + }); + + let state = cx.new(|_| PickerState { + workspace: workspace.weak_handle(), + lsp_store: lsp_store.downgrade(), + active_editor: None, + language_servers: LanguageServers::default(), + }); + + Self { + state, + lsp_picker: None, + _subscriptions: vec![settings_subscription, lsp_store_subscription], + } + } + + fn on_lsp_store_event( + &mut self, + e: &LspStoreEvent, + window: &mut Window, + cx: &mut Context, + ) { + let Some(lsp_picker) = self.lsp_picker.clone() else { + return; + }; + let mut updated = false; + + match e { + LspStoreEvent::LanguageServerUpdate { + language_server_id, + name, + message: proto::update_language_server::Variant::StatusUpdate(status_update), + } => match &status_update.status { + Some(proto::status_update::Status::Binary(binary_status)) => { + let Some(name) = name.as_ref() else { + return; + }; + if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status) + { + let binary_status = match binary_status { + proto::ServerBinaryStatus::None => BinaryStatus::None, + proto::ServerBinaryStatus::CheckingForUpdate => { + BinaryStatus::CheckingForUpdate + } + proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading, + proto::ServerBinaryStatus::Starting => BinaryStatus::Starting, + proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping, + proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped, + proto::ServerBinaryStatus::Failed => { + let Some(error) = status_update.message.clone() else { + return; + }; + BinaryStatus::Failed { error } + } + }; + self.state.update(cx, |state, _| { + state.language_servers.update_binary_status( + binary_status, + status_update.message.as_deref(), + name.clone(), + ); + }); + updated = true; + }; + } + Some(proto::status_update::Status::Health(health_status)) => { + if let Some(health) = proto::ServerHealth::from_i32(*health_status) { + let health = match health { + proto::ServerHealth::Ok => ServerHealth::Ok, + proto::ServerHealth::Warning => ServerHealth::Warning, + proto::ServerHealth::Error => ServerHealth::Error, + }; + self.state.update(cx, |state, _| { + state.language_servers.update_server_health( + *language_server_id, + health, + status_update.message.as_deref(), + name.clone(), + ); + }); + updated = true; + } + } + None => {} + }, + LspStoreEvent::LanguageServerUpdate { + language_server_id, + name, + message: proto::update_language_server::Variant::RegisteredForBuffer(update), + .. + } => { + self.state.update(cx, |state, _| { + state + .language_servers + .servers_per_buffer_abs_path + .entry(PathBuf::from(&update.buffer_abs_path)) + .or_default() + .insert(*language_server_id, name.clone()); + }); + updated = true; + } + _ => {} + }; + + if updated { + lsp_picker.update(cx, |lsp_picker, cx| { + lsp_picker.refresh(window, cx); + }); + } + } + + fn new_lsp_picker( + state: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Entity> { + cx.new(|cx| { + let mut delegate = LspPickerDelegate { + selected_index: 0, + other_servers_start_index: None, + items: Vec::new(), + state, + }; + delegate.regenerate_items(cx); + Picker::list(delegate, window, cx) + }) + } +} + +impl StatusItemView for LspTool { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn workspace::ItemHandle>, + window: &mut Window, + cx: &mut Context, + ) { + if ProjectSettings::get_global(cx).global_lsp_settings.button { + if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { + if Some(&editor) + != self + .state + .read(cx) + .active_editor + .as_ref() + .and_then(|active_editor| active_editor.editor.upgrade()) + .as_ref() + { + let editor_buffers = + HashSet::from_iter(editor.read(cx).buffer().read(cx).excerpt_buffer_ids()); + let _editor_subscription = cx.subscribe_in( + &editor, + window, + |lsp_tool, _, e: &EditorEvent, window, cx| match e { + EditorEvent::ExcerptsAdded { buffer, .. } => { + lsp_tool.state.update(cx, |state, cx| { + if let Some(active_editor) = state.active_editor.as_mut() { + let buffer_id = buffer.read(cx).remote_id(); + if active_editor.editor_buffers.insert(buffer_id) { + if let Some(picker) = &lsp_tool.lsp_picker { + picker.update(cx, |picker, cx| { + picker.refresh(window, cx) + }); + } + } + } + }); + } + EditorEvent::ExcerptsRemoved { + removed_buffer_ids, .. + } => { + lsp_tool.state.update(cx, |state, cx| { + if let Some(active_editor) = state.active_editor.as_mut() { + let mut removed = false; + for id in removed_buffer_ids { + active_editor.editor_buffers.retain(|buffer_id| { + let retain = buffer_id != id; + removed |= !retain; + retain + }); + } + if removed { + if let Some(picker) = &lsp_tool.lsp_picker { + picker.update(cx, |picker, cx| { + picker.refresh(window, cx) + }); + } + } + } + }); + } + _ => {} + }, + ); + self.state.update(cx, |state, _| { + state.active_editor = Some(ActiveEditor { + editor: editor.downgrade(), + _editor_subscription, + editor_buffers, + }); + }); + + let lsp_picker = Self::new_lsp_picker(self.state.clone(), window, cx); + self.lsp_picker = Some(lsp_picker.clone()); + lsp_picker.update(cx, |lsp_picker, cx| lsp_picker.refresh(window, cx)); + } + } else if self.state.read(cx).active_editor.is_some() { + self.state.update(cx, |state, _| { + state.active_editor = None; + }); + if let Some(lsp_picker) = self.lsp_picker.as_ref() { + lsp_picker.update(cx, |lsp_picker, cx| { + lsp_picker.refresh(window, cx); + }); + }; + } + } else if self.state.read(cx).active_editor.is_some() { + self.state.update(cx, |state, _| { + state.active_editor = None; + }); + if let Some(lsp_picker) = self.lsp_picker.as_ref() { + lsp_picker.update(cx, |lsp_picker, cx| { + lsp_picker.refresh(window, cx); + }); + } + } + } +} + +impl Render for LspTool { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + let Some(lsp_picker) = self.lsp_picker.clone() else { + return div(); + }; + + let mut has_errors = false; + let mut has_warnings = false; + let mut has_other_notifications = false; + let state = self.state.read(cx); + for server in state.language_servers.health_statuses.values() { + if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) { + has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. }); + has_other_notifications |= binary_status.message.is_some(); + } + + if let Some((message, health)) = &server.health { + has_other_notifications |= message.is_some(); + match health { + ServerHealth::Ok => {} + ServerHealth::Warning => has_warnings = true, + ServerHealth::Error => has_errors = true, + } + } + } + + let indicator = if has_errors { + Some(Indicator::dot().color(Color::Error)) + } else if has_warnings { + Some(Indicator::dot().color(Color::Warning)) + } else if has_other_notifications { + Some(Indicator::dot().color(Color::Modified)) + } else { + None + }; + + div().child( + PickerPopoverMenu::new( + lsp_picker.clone(), + IconButton::new("zed-lsp-tool-button", IconName::Bolt) + .when_some(indicator, IconButton::indicator) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .indicator_border_color(Some(cx.theme().colors().status_bar_background)), + move |_, cx| Tooltip::simple("Language servers", cx), + Corner::BottomRight, + cx, + ) + .render(window, cx), + ) + } +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 625a459e20..28ad606132 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -108,6 +108,12 @@ pub struct LanguageServer { root_uri: Url, } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum LanguageServerSelector { + Id(LanguageServerId), + Name(LanguageServerName), +} + /// Identifies a running language server. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index eda4ae641f..c1ebe25538 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -205,6 +205,7 @@ pub trait PickerDelegate: Sized + 'static { window: &mut Window, cx: &mut Context>, ) -> Option; + fn render_header( &self, _window: &mut Window, @@ -212,6 +213,7 @@ pub trait PickerDelegate: Sized + 'static { ) -> Option { None } + fn render_footer( &self, _window: &mut Window, diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index b2c21abcdb..b8101e14f3 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -783,7 +783,7 @@ impl BufferStore { project_path: ProjectPath, cx: &mut Context, ) -> Task>> { - if let Some(buffer) = self.get_by_path(&project_path, cx) { + if let Some(buffer) = self.get_by_path(&project_path) { cx.emit(BufferStoreEvent::BufferOpened { buffer: buffer.clone(), project_path, @@ -946,7 +946,7 @@ impl BufferStore { self.path_to_buffer_id.get(project_path) } - pub fn get_by_path(&self, path: &ProjectPath, _cx: &App) -> Option> { + pub fn get_by_path(&self, path: &ProjectPath) -> Option> { self.path_to_buffer_id.get(path).and_then(|buffer_id| { let buffer = self.get(*buffer_id); buffer diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 5f3e49f7dd..025dca4100 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -275,7 +275,7 @@ impl BreakpointStore { .context("Could not resolve provided abs path")?; let buffer = this .update(&mut cx, |this, cx| { - this.buffer_store().read(cx).get_by_path(&path, cx) + this.buffer_store().read(cx).get_by_path(&path) })? .context("Could not find buffer for a given path")?; let breakpoint = message diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index f7d0de48e2..7002f83ab3 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -3322,7 +3322,7 @@ impl Repository { let Some(project_path) = self.repo_path_to_project_path(path, cx) else { continue; }; - if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) { + if let Some(buffer) = buffer_store.get_by_path(&project_path) { if buffer .read(cx) .file() @@ -3389,7 +3389,7 @@ impl Repository { let Some(project_path) = self.repo_path_to_project_path(path, cx) else { continue; }; - if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) { + if let Some(buffer) = buffer_store.get_by_path(&project_path) { if buffer .read(cx) .file() @@ -3749,7 +3749,7 @@ impl Repository { let buffer_id = git_store .buffer_store .read(cx) - .get_by_path(&project_path?, cx)? + .get_by_path(&project_path?)? .read(cx) .remote_id(); let diff_state = git_store.diffs.get(&buffer_id)?; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index a9c257f3ea..d6f5d7a3cc 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -42,9 +42,8 @@ use itertools::Itertools as _; use language::{ Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, - LanguageRegistry, LanguageServerStatusUpdate, LanguageToolchainStore, LocalFile, LspAdapter, - LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, - Unclipped, + LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, + PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, language_settings::{ FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, @@ -60,9 +59,9 @@ use lsp::{ DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, - LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, - TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, - notification::DidRenameFiles, + LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf, + RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, + WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -256,7 +255,7 @@ impl LocalLspStore { let delegate = delegate as Arc; let key = key.clone(); let adapter = adapter.clone(); - let this = self.weak.clone(); + let lsp_store = self.weak.clone(); let pending_workspace_folders = pending_workspace_folders.clone(); let fs = self.fs.clone(); let pull_diagnostics = ProjectSettings::get_global(cx) @@ -265,7 +264,8 @@ impl LocalLspStore { .enabled; cx.spawn(async move |cx| { let result = async { - let toolchains = this.update(cx, |this, cx| this.toolchain_store(cx))?; + let toolchains = + lsp_store.update(cx, |lsp_store, cx| lsp_store.toolchain_store(cx))?; let language_server = pending_server.await?; let workspace_config = Self::workspace_configuration_for_adapter( @@ -300,7 +300,7 @@ impl LocalLspStore { })??; Self::setup_lsp_messages( - this.clone(), + lsp_store.clone(), fs, &language_server, delegate.clone(), @@ -321,7 +321,7 @@ impl LocalLspStore { })? .await .inspect_err(|_| { - if let Some(lsp_store) = this.upgrade() { + if let Some(lsp_store) = lsp_store.upgrade() { lsp_store .update(cx, |lsp_store, cx| { lsp_store.cleanup_lsp_data(server_id); @@ -343,17 +343,18 @@ impl LocalLspStore { match result { Ok(server) => { - this.update(cx, |this, mut cx| { - this.insert_newly_running_language_server( - adapter, - server.clone(), - server_id, - key, - pending_workspace_folders, - &mut cx, - ); - }) - .ok(); + lsp_store + .update(cx, |lsp_store, mut cx| { + lsp_store.insert_newly_running_language_server( + adapter, + server.clone(), + server_id, + key, + pending_workspace_folders, + &mut cx, + ); + }) + .ok(); stderr_capture.lock().take(); Some(server) } @@ -366,7 +367,9 @@ impl LocalLspStore { error: format!("{err}\n-- stderr--\n{log}"), }, ); - log::error!("Failed to start language server {server_name:?}: {err:#?}"); + let message = + format!("Failed to start language server {server_name:?}: {err:#?}"); + log::error!("{message}"); log::error!("server stderr: {log}"); None } @@ -378,6 +381,9 @@ impl LocalLspStore { pending_workspace_folders, }; + self.languages + .update_lsp_binary_status(adapter.name(), BinaryStatus::Starting); + self.language_servers.insert(server_id, state); self.language_server_ids .entry(key) @@ -1028,20 +1034,14 @@ impl LocalLspStore { clangd_ext::register_notifications(this, language_server, adapter); } - fn shutdown_language_servers( + fn shutdown_language_servers_on_quit( &mut self, - _cx: &mut Context, + _: &mut Context, ) -> impl Future + use<> { let shutdown_futures = self .language_servers .drain() - .map(|(_, server_state)| async { - use LanguageServerState::*; - match server_state { - Running { server, .. } => server.shutdown()?.await, - Starting { startup, .. } => startup.await?.shutdown()?.await, - } - }) + .map(|(_, server_state)| Self::shutdown_server(server_state)) .collect::>(); async move { @@ -1049,6 +1049,24 @@ impl LocalLspStore { } } + async fn shutdown_server(server_state: LanguageServerState) -> anyhow::Result<()> { + match server_state { + LanguageServerState::Running { server, .. } => { + if let Some(shutdown) = server.shutdown() { + shutdown.await; + } + } + LanguageServerState::Starting { startup, .. } => { + if let Some(server) = startup.await { + if let Some(shutdown) = server.shutdown() { + shutdown.await; + } + } + } + } + Ok(()) + } + fn language_servers_for_worktree( &self, worktree_id: WorktreeId, @@ -2318,6 +2336,7 @@ impl LocalLspStore { fn register_buffer_with_language_servers( &mut self, buffer_handle: &Entity, + only_register_servers: HashSet, cx: &mut Context, ) { let buffer = buffer_handle.read(cx); @@ -2383,6 +2402,18 @@ impl LocalLspStore { if reused && server_node.server_id().is_none() { return None; } + if !only_register_servers.is_empty() { + if let Some(server_id) = server_node.server_id() { + if !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) { + return None; + } + } + if let Some(name) = server_node.name() { + if !only_register_servers.contains(&LanguageServerSelector::Name(name)) { + return None; + } + } + } let server_id = server_node.server_id_or_init( |LaunchDisposition { @@ -2390,66 +2421,82 @@ impl LocalLspStore { attach, path, settings, - }| match attach { - language::Attach::InstancePerRoot => { - // todo: handle instance per root proper. - if let Some(server_ids) = self - .language_server_ids - .get(&(worktree_id, server_name.clone())) - { - server_ids.iter().cloned().next().unwrap() - } else { - let language_name = language.name(); - - self.start_language_server( - &worktree, - delegate.clone(), - self.languages - .lsp_adapters(&language_name) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"), - settings, - cx, - ) - } - } - language::Attach::Shared => { - let uri = Url::from_file_path( - worktree.read(cx).abs_path().join(&path.path), - ); - let key = (worktree_id, server_name.clone()); - if !self.language_server_ids.contains_key(&key) { - let language_name = language.name(); - self.start_language_server( - &worktree, - delegate.clone(), - self.languages - .lsp_adapters(&language_name) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"), - settings, - cx, - ); - } - if let Some(server_ids) = self - .language_server_ids - .get(&key) - { - debug_assert_eq!(server_ids.len(), 1); - let server_id = server_ids.iter().cloned().next().unwrap(); - - if let Some(state) = self.language_servers.get(&server_id) { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } - server_id - } else { - unreachable!("Language server ID should be available, as it's registered on demand") - } - } + }| { + let server_id = match attach { + language::Attach::InstancePerRoot => { + // todo: handle instance per root proper. + if let Some(server_ids) = self + .language_server_ids + .get(&(worktree_id, server_name.clone())) + { + server_ids.iter().cloned().next().unwrap() + } else { + let language_name = language.name(); + let adapter = self.languages + .lsp_adapters(&language_name) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"); + let server_id = self.start_language_server( + &worktree, + delegate.clone(), + adapter, + settings, + cx, + ); + server_id + } + } + language::Attach::Shared => { + let uri = Url::from_file_path( + worktree.read(cx).abs_path().join(&path.path), + ); + let key = (worktree_id, server_name.clone()); + if !self.language_server_ids.contains_key(&key) { + let language_name = language.name(); + let adapter = self.languages + .lsp_adapters(&language_name) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"); + self.start_language_server( + &worktree, + delegate.clone(), + adapter, + settings, + cx, + ); + } + if let Some(server_ids) = self + .language_server_ids + .get(&key) + { + debug_assert_eq!(server_ids.len(), 1); + let server_id = server_ids.iter().cloned().next().unwrap(); + if let Some(state) = self.language_servers.get(&server_id) { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; + } + server_id + } else { + unreachable!("Language server ID should be available, as it's registered on demand") + } + } + }; + let lsp_tool = self.weak.clone(); + let server_name = server_node.name(); + let buffer_abs_path = abs_path.to_string_lossy().to_string(); + cx.defer(move |cx| { + lsp_tool.update(cx, |_, cx| cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server_id, + name: server_name, + message: proto::update_language_server::Variant::RegisteredForBuffer(proto::RegisteredForBuffer { + buffer_abs_path, + }) + })).ok(); + }); + server_id }, )?; let server_state = self.language_servers.get(&server_id)?; @@ -2498,6 +2545,16 @@ impl LocalLspStore { vec![snapshot] }); + + cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server.server_id(), + name: None, + message: proto::update_language_server::Variant::RegisteredForBuffer( + proto::RegisteredForBuffer { + buffer_abs_path: abs_path.to_string_lossy().to_string(), + }, + ), + }); } } @@ -3479,7 +3536,7 @@ pub struct LspStore { worktree_store: Entity, toolchain_store: Option>, pub languages: Arc, - pub language_server_statuses: BTreeMap, + language_server_statuses: BTreeMap, active_entry: Option, _maintain_workspace_config: (Task>, watch::Sender<()>), _maintain_buffer_languages: Task<()>, @@ -3503,11 +3560,13 @@ struct BufferLspData { colors: Option>, } +#[derive(Debug)] pub enum LspStoreEvent { LanguageServerAdded(LanguageServerId, LanguageServerName, Option), LanguageServerRemoved(LanguageServerId), LanguageServerUpdate { language_server_id: LanguageServerId, + name: Option, message: proto::update_language_server::Variant, }, LanguageServerLog(LanguageServerId, LanguageServerLogType, String), @@ -3682,6 +3741,7 @@ impl LspStore { } cx.observe_global::(Self::on_settings_changed) .detach(); + subscribe_to_binary_statuses(&languages, cx).detach(); let _maintain_workspace_config = { let (sender, receiver) = watch::channel(); @@ -3714,7 +3774,9 @@ impl LspStore { next_diagnostic_group_id: Default::default(), diagnostics: Default::default(), _subscription: cx.on_app_quit(|this, cx| { - this.as_local_mut().unwrap().shutdown_language_servers(cx) + this.as_local_mut() + .unwrap() + .shutdown_language_servers_on_quit(cx) }), lsp_tree: LanguageServerTree::new(manifest_tree, languages.clone(), cx), registered_buffers: HashMap::default(), @@ -3768,6 +3830,7 @@ 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(fs, receiver, cx), sender) @@ -3819,7 +3882,7 @@ impl LspStore { if let Some(local) = self.as_local_mut() { local.initialize_buffer(buffer, cx); if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers(buffer, cx); + local.register_buffer_with_language_servers(buffer, HashSet::default(), cx); } } } @@ -4047,6 +4110,7 @@ impl LspStore { pub(crate) fn register_buffer_with_language_servers( &mut self, buffer: &Entity, + only_register_servers: HashSet, ignore_refcounts: bool, cx: &mut Context, ) -> OpenLspBufferHandle { @@ -4070,7 +4134,7 @@ impl LspStore { } if ignore_refcounts || *refcount == 1 { - local.register_buffer_with_language_servers(buffer, cx); + local.register_buffer_with_language_servers(buffer, only_register_servers, cx); } if !ignore_refcounts { cx.observe_release(&handle, move |this, buffer, cx| { @@ -4097,6 +4161,26 @@ impl LspStore { .request(proto::RegisterBufferWithLanguageServers { project_id: upstream_project_id, buffer_id, + only_servers: only_register_servers + .into_iter() + .map(|selector| { + let selector = match selector { + LanguageServerSelector::Id(language_server_id) => { + proto::language_server_selector::Selector::ServerId( + language_server_id.to_proto(), + ) + } + LanguageServerSelector::Name(language_server_name) => { + proto::language_server_selector::Selector::Name( + language_server_name.to_string(), + ) + } + }; + proto::LanguageServerSelector { + selector: Some(selector), + } + }) + .collect(), }) .await }) @@ -4182,7 +4266,11 @@ impl LspStore { .registered_buffers .contains_key(&buffer.read(cx).remote_id()) { - local.register_buffer_with_language_servers(&buffer, cx); + local.register_buffer_with_language_servers( + &buffer, + HashSet::default(), + cx, + ); } } } @@ -4267,7 +4355,11 @@ impl LspStore { if let Some(local) = self.as_local_mut() { if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers(buffer_entity, cx); + local.register_buffer_with_language_servers( + buffer_entity, + HashSet::default(), + cx, + ); } } Some(worktree.read(cx).id()) @@ -4488,28 +4580,29 @@ impl LspStore { let buffer_store = self.buffer_store.clone(); if let Some(local) = self.as_local_mut() { let mut adapters = BTreeMap::default(); - let to_stop = local.lsp_tree.clone().update(cx, |lsp_tree, cx| { - let get_adapter = { - let languages = local.languages.clone(); - let environment = local.environment.clone(); - let weak = local.weak.clone(); - let worktree_store = local.worktree_store.clone(); - let http_client = local.http_client.clone(); - let fs = local.fs.clone(); - move |worktree_id, cx: &mut App| { - let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; - Some(LocalLspAdapterDelegate::new( - languages.clone(), - &environment, - weak.clone(), - &worktree, - http_client.clone(), - fs.clone(), - cx, - )) - } - }; + let get_adapter = { + let languages = local.languages.clone(); + let environment = local.environment.clone(); + let weak = local.weak.clone(); + let worktree_store = local.worktree_store.clone(); + let http_client = local.http_client.clone(); + let fs = local.fs.clone(); + move |worktree_id, cx: &mut App| { + let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; + Some(LocalLspAdapterDelegate::new( + languages.clone(), + &environment, + weak.clone(), + &worktree, + http_client.clone(), + fs.clone(), + cx, + )) + } + }; + let mut messages_to_report = Vec::new(); + let to_stop = local.lsp_tree.clone().update(cx, |lsp_tree, cx| { let mut rebase = lsp_tree.rebase(); for buffer_handle in buffer_store.read(cx).buffers().sorted_by_key(|buffer| { Reverse( @@ -4570,9 +4663,10 @@ impl LspStore { continue; }; + let abs_path = file.abs_path(cx); for node in nodes { if !reused { - node.server_id_or_init( + let server_id = node.server_id_or_init( |LaunchDisposition { server_name, attach, @@ -4587,20 +4681,20 @@ impl LspStore { { server_ids.iter().cloned().next().unwrap() } else { - local.start_language_server( + let adapter = local + .languages + .lsp_adapters(&language) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"); + let server_id = local.start_language_server( &worktree, delegate.clone(), - local - .languages - .lsp_adapters(&language) - .into_iter() - .find(|adapter| { - &adapter.name() == server_name - }) - .expect("To find LSP adapter"), + adapter, settings, cx, - ) + ); + server_id } } language::Attach::Shared => { @@ -4610,15 +4704,16 @@ impl LspStore { let key = (worktree_id, server_name.clone()); local.language_server_ids.remove(&key); + let adapter = local + .languages + .lsp_adapters(&language) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"); let server_id = local.start_language_server( &worktree, delegate.clone(), - local - .languages - .lsp_adapters(&language) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"), + adapter, settings, cx, ); @@ -4633,14 +4728,30 @@ impl LspStore { } }, ); + + if let Some(language_server_id) = server_id { + messages_to_report.push(LspStoreEvent::LanguageServerUpdate { + language_server_id, + name: node.name(), + message: + proto::update_language_server::Variant::RegisteredForBuffer( + proto::RegisteredForBuffer { + buffer_abs_path: abs_path.to_string_lossy().to_string(), + }, + ), + }); + } } } } } rebase.finish() }); - for (id, name) in to_stop { - self.stop_local_language_server(id, name, cx).detach(); + for message in messages_to_report { + cx.emit(message); + } + for (id, _) in to_stop { + self.stop_local_language_server(id, cx).detach(); } } } @@ -7122,7 +7233,7 @@ impl LspStore { path: relative_path.into(), }; - if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path, cx) { + if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) { let snapshot = buffer_handle.read(cx).snapshot(); let buffer = buffer_handle.read(cx); let reused_diagnostics = buffer @@ -7801,6 +7912,7 @@ impl LspStore { return upstream_client.send(proto::RegisterBufferWithLanguageServers { project_id: upstream_project_id, buffer_id: buffer_id.to_proto(), + only_servers: envelope.payload.only_servers, }); } @@ -7808,7 +7920,28 @@ impl LspStore { anyhow::bail!("buffer is not open"); }; - let handle = this.register_buffer_with_language_servers(&buffer, false, cx); + let handle = this.register_buffer_with_language_servers( + &buffer, + envelope + .payload + .only_servers + .into_iter() + .filter_map(|selector| { + Some(match selector.selector? { + proto::language_server_selector::Selector::ServerId(server_id) => { + LanguageServerSelector::Id(LanguageServerId::from_proto(server_id)) + } + proto::language_server_selector::Selector::Name(name) => { + LanguageServerSelector::Name(LanguageServerName( + SharedString::from(name), + )) + } + }) + }) + .collect(), + false, + cx, + ); this.buffer_store().update(cx, |buffer_store, _| { buffer_store.register_shared_lsp_handle(peer_id, buffer_id, handle); }); @@ -7980,16 +8113,16 @@ impl LspStore { } async fn handle_update_language_server( - this: Entity, + lsp_store: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { - this.update(&mut cx, |this, cx| { + lsp_store.update(&mut cx, |lsp_store, cx| { let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize); match envelope.payload.variant.context("invalid variant")? { proto::update_language_server::Variant::WorkStart(payload) => { - this.on_lsp_work_start( + lsp_store.on_lsp_work_start( language_server_id, payload.token, LanguageServerProgress { @@ -8003,9 +8136,8 @@ impl LspStore { cx, ); } - proto::update_language_server::Variant::WorkProgress(payload) => { - this.on_lsp_work_progress( + lsp_store.on_lsp_work_progress( language_server_id, payload.token, LanguageServerProgress { @@ -8021,15 +8153,28 @@ impl LspStore { } proto::update_language_server::Variant::WorkEnd(payload) => { - this.on_lsp_work_end(language_server_id, payload.token, cx); + lsp_store.on_lsp_work_end(language_server_id, payload.token, cx); } proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => { - this.disk_based_diagnostics_started(language_server_id, cx); + lsp_store.disk_based_diagnostics_started(language_server_id, cx); } proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => { - this.disk_based_diagnostics_finished(language_server_id, cx) + lsp_store.disk_based_diagnostics_finished(language_server_id, cx) + } + + non_lsp @ proto::update_language_server::Variant::StatusUpdate(_) + | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) => { + cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id, + name: envelope + .payload + .server_name + .map(SharedString::new) + .map(LanguageServerName), + message: non_lsp, + }); } } @@ -8145,6 +8290,9 @@ impl LspStore { cx.emit(LspStoreEvent::DiskBasedDiagnosticsStarted { language_server_id }); cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id, + name: self + .language_server_adapter_for_id(language_server_id) + .map(|adapter| adapter.name()), message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( Default::default(), ), @@ -8165,6 +8313,9 @@ impl LspStore { cx.emit(LspStoreEvent::DiskBasedDiagnosticsFinished { language_server_id }); cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id, + name: self + .language_server_adapter_for_id(language_server_id) + .map(|adapter| adapter.name()), message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( Default::default(), ), @@ -8473,6 +8624,9 @@ impl LspStore { } cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id, + name: self + .language_server_adapter_for_id(language_server_id) + .map(|adapter| adapter.name()), message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { token, title: progress.title, @@ -8521,6 +8675,9 @@ impl LspStore { if did_update { cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id, + name: self + .language_server_adapter_for_id(language_server_id) + .map(|adapter| adapter.name()), message: proto::update_language_server::Variant::WorkProgress( proto::LspWorkProgress { token, @@ -8550,6 +8707,9 @@ impl LspStore { cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id, + name: self + .language_server_adapter_for_id(language_server_id) + .map(|adapter| adapter.name()), message: proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { token }), }) } @@ -8930,22 +9090,73 @@ impl LspStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - this.update(&mut cx, |this, cx| { - let buffers = this.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx); - this.restart_language_servers_for_buffers(buffers, cx); + this.update(&mut cx, |lsp_store, cx| { + let buffers = + lsp_store.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx); + lsp_store.restart_language_servers_for_buffers( + buffers, + envelope + .payload + .only_servers + .into_iter() + .filter_map(|selector| { + Some(match selector.selector? { + proto::language_server_selector::Selector::ServerId(server_id) => { + LanguageServerSelector::Id(LanguageServerId::from_proto(server_id)) + } + proto::language_server_selector::Selector::Name(name) => { + LanguageServerSelector::Name(LanguageServerName( + SharedString::from(name), + )) + } + }) + }) + .collect(), + cx, + ); })?; Ok(proto::Ack {}) } pub async fn handle_stop_language_servers( - this: Entity, + lsp_store: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - this.update(&mut cx, |this, cx| { - let buffers = this.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx); - this.stop_language_servers_for_buffers(buffers, cx); + lsp_store.update(&mut cx, |lsp_store, cx| { + if envelope.payload.all + && envelope.payload.also_servers.is_empty() + && envelope.payload.buffer_ids.is_empty() + { + lsp_store.stop_all_language_servers(cx); + } else { + let buffers = + lsp_store.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx); + lsp_store.stop_language_servers_for_buffers( + buffers, + envelope + .payload + .also_servers + .into_iter() + .filter_map(|selector| { + Some(match selector.selector? { + proto::language_server_selector::Selector::ServerId(server_id) => { + LanguageServerSelector::Id(LanguageServerId::from_proto( + server_id, + )) + } + proto::language_server_selector::Selector::Name(name) => { + LanguageServerSelector::Name(LanguageServerName( + SharedString::from(name), + )) + } + }) + }) + .collect(), + cx, + ); + } })?; Ok(proto::Ack {}) @@ -9269,11 +9480,8 @@ impl LspStore { select! { server = startup.fuse() => server, - _ = timer => { - log::info!( - "timeout waiting for language server {} to finish launching before stopping", - name - ); + () = timer => { + log::info!("timeout waiting for language server {name} to finish launching before stopping"); None }, } @@ -9296,7 +9504,6 @@ impl LspStore { fn stop_local_language_server( &mut self, server_id: LanguageServerId, - name: LanguageServerName, cx: &mut Context, ) -> Task> { let local = match &mut self.mode { @@ -9306,7 +9513,7 @@ impl LspStore { } }; - let mut orphaned_worktrees = vec![]; + let mut orphaned_worktrees = Vec::new(); // Remove this server ID from all entries in the given worktree. local.language_server_ids.retain(|(worktree, _), ids| { if !ids.remove(&server_id) { @@ -9320,8 +9527,6 @@ impl LspStore { true } }); - let _ = self.language_server_statuses.remove(&server_id); - log::info!("stopping language server {name}"); self.buffer_store.update(cx, |buffer_store, cx| { for buffer in buffer_store.buffers() { buffer.update(cx, |buffer, cx| { @@ -9367,19 +9572,85 @@ impl LspStore { }); } local.language_server_watched_paths.remove(&server_id); + let server_state = local.language_servers.remove(&server_id); - cx.notify(); self.cleanup_lsp_data(server_id); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); - cx.spawn(async move |_, cx| { - Self::shutdown_language_server(server_state, name, cx).await; - orphaned_worktrees - }) + let name = self + .language_server_statuses + .remove(&server_id) + .map(|status| LanguageServerName::from(status.name.as_str())) + .or_else(|| { + if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { + Some(adapter.name()) + } else { + None + } + }); + + if let Some(name) = name { + log::info!("stopping language server {name}"); + self.languages + .update_lsp_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); + cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); + cx.notify(); + }) + .ok(); + orphaned_worktrees + }); + } + + if server_state.is_some() { + cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); + } + Task::ready(orphaned_worktrees) + } + + pub fn stop_all_language_servers(&mut self, cx: &mut Context) { + if let Some((client, project_id)) = self.upstream_client() { + let request = client.request(proto::StopLanguageServers { + project_id, + buffer_ids: Vec::new(), + also_servers: Vec::new(), + all: true, + }); + cx.background_spawn(request).detach_and_log_err(cx); + } else { + let Some(local) = self.as_local_mut() else { + return; + }; + let language_servers_to_stop = local + .language_server_ids + .values() + .flatten() + .copied() + .collect(); + local.lsp_tree.update(cx, |this, _| { + this.remove_nodes(&language_servers_to_stop); + }); + let tasks = language_servers_to_stop + .into_iter() + .map(|server| self.stop_local_language_server(server, cx)) + .collect::>(); + cx.background_spawn(async move { + futures::future::join_all(tasks).await; + }) + .detach(); + } } pub fn restart_language_servers_for_buffers( &mut self, buffers: Vec>, + only_restart_servers: HashSet, cx: &mut Context, ) { if let Some((client, project_id)) = self.upstream_client() { @@ -9389,18 +9660,49 @@ impl LspStore { .into_iter() .map(|b| b.read(cx).remote_id().to_proto()) .collect(), + only_servers: only_restart_servers + .into_iter() + .map(|selector| { + let selector = match selector { + LanguageServerSelector::Id(language_server_id) => { + proto::language_server_selector::Selector::ServerId( + language_server_id.to_proto(), + ) + } + LanguageServerSelector::Name(language_server_name) => { + proto::language_server_selector::Selector::Name( + language_server_name.to_string(), + ) + } + }; + proto::LanguageServerSelector { + selector: Some(selector), + } + }) + .collect(), + all: false, }); cx.background_spawn(request).detach_and_log_err(cx); } else { - let stop_task = self.stop_local_language_servers_for_buffers(&buffers, cx); - cx.spawn(async move |this, cx| { + let stop_task = if only_restart_servers.is_empty() { + self.stop_local_language_servers_for_buffers(&buffers, HashSet::default(), cx) + } else { + self.stop_local_language_servers_for_buffers(&[], only_restart_servers.clone(), cx) + }; + cx.spawn(async move |lsp_store, cx| { stop_task.await; - this.update(cx, |this, cx| { - for buffer in buffers { - this.register_buffer_with_language_servers(&buffer, true, cx); - } - }) - .ok() + lsp_store + .update(cx, |lsp_store, cx| { + for buffer in buffers { + lsp_store.register_buffer_with_language_servers( + &buffer, + only_restart_servers.clone(), + true, + cx, + ); + } + }) + .ok() }) .detach(); } @@ -9409,6 +9711,7 @@ impl LspStore { pub fn stop_language_servers_for_buffers( &mut self, buffers: Vec>, + also_restart_servers: HashSet, cx: &mut Context, ) { if let Some((client, project_id)) = self.upstream_client() { @@ -9418,10 +9721,31 @@ impl LspStore { .into_iter() .map(|b| b.read(cx).remote_id().to_proto()) .collect(), + also_servers: also_restart_servers + .into_iter() + .map(|selector| { + let selector = match selector { + LanguageServerSelector::Id(language_server_id) => { + proto::language_server_selector::Selector::ServerId( + language_server_id.to_proto(), + ) + } + LanguageServerSelector::Name(language_server_name) => { + proto::language_server_selector::Selector::Name( + language_server_name.to_string(), + ) + } + }; + proto::LanguageServerSelector { + selector: Some(selector), + } + }) + .collect(), + all: false, }); cx.background_spawn(request).detach_and_log_err(cx); } else { - self.stop_local_language_servers_for_buffers(&buffers, cx) + self.stop_local_language_servers_for_buffers(&buffers, also_restart_servers, cx) .detach(); } } @@ -9429,32 +9753,62 @@ impl LspStore { fn stop_local_language_servers_for_buffers( &mut self, buffers: &[Entity], + also_restart_servers: HashSet, cx: &mut Context, ) -> Task<()> { let Some(local) = self.as_local_mut() else { return Task::ready(()); }; - let language_servers_to_stop = buffers - .iter() - .flat_map(|buffer| { - buffer.update(cx, |buffer, cx| { - local.language_server_ids_for_buffer(buffer, cx) - }) + let mut language_server_names_to_stop = BTreeSet::default(); + let mut language_servers_to_stop = also_restart_servers + .into_iter() + .flat_map(|selector| match selector { + LanguageServerSelector::Id(id) => Some(id), + LanguageServerSelector::Name(name) => { + language_server_names_to_stop.insert(name); + None + } }) .collect::>(); + + let mut covered_worktrees = HashSet::default(); + for buffer in buffers { + buffer.update(cx, |buffer, cx| { + language_servers_to_stop.extend(local.language_server_ids_for_buffer(buffer, cx)); + if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) { + if covered_worktrees.insert(worktree_id) { + language_server_names_to_stop.retain(|name| { + match local.language_server_ids.get(&(worktree_id, name.clone())) { + Some(server_ids) => { + language_servers_to_stop + .extend(server_ids.into_iter().copied()); + false + } + None => true, + } + }); + } + } + }); + } + for name in language_server_names_to_stop { + if let Some(server_ids) = local + .language_server_ids + .iter() + .filter(|((_, server_name), _)| server_name == &name) + .map(|((_, _), server_ids)| server_ids) + .max_by_key(|server_ids| server_ids.len()) + { + language_servers_to_stop.extend(server_ids.into_iter().copied()); + } + } + local.lsp_tree.update(cx, |this, _| { this.remove_nodes(&language_servers_to_stop); }); let tasks = language_servers_to_stop .into_iter() - .map(|server| { - let name = self - .language_server_statuses - .get(&server) - .map(|state| state.name.as_str().into()) - .unwrap_or_else(|| LanguageServerName::from("Unknown")); - self.stop_local_language_server(server, name, cx) - }) + .map(|server| self.stop_local_language_server(server, cx)) .collect::>(); cx.background_spawn(futures::future::join_all(tasks).map(|_| ())) @@ -9472,7 +9826,7 @@ impl LspStore { Some( self.buffer_store() .read(cx) - .get_by_path(&project_path, cx)? + .get_by_path(&project_path)? .read(cx), ) } @@ -9686,6 +10040,9 @@ impl LspStore { simulate_disk_based_diagnostics_completion: None, }, ); + local + .languages + .update_lsp_binary_status(adapter.name(), BinaryStatus::None); if let Some(file_ops_caps) = language_server .capabilities() .workspace @@ -10331,6 +10688,53 @@ impl LspStore { } } +fn subscribe_to_binary_statuses( + languages: &Arc, + cx: &mut Context<'_, LspStore>, +) -> Task<()> { + let mut server_statuses = languages.language_server_binary_statuses(); + cx.spawn(async move |lsp_store, cx| { + while let Some((server_name, binary_status)) = server_statuses.next().await { + if lsp_store + .update(cx, |_, cx| { + let mut message = None; + let binary_status = match binary_status { + BinaryStatus::None => proto::ServerBinaryStatus::None, + BinaryStatus::CheckingForUpdate => { + proto::ServerBinaryStatus::CheckingForUpdate + } + BinaryStatus::Downloading => proto::ServerBinaryStatus::Downloading, + BinaryStatus::Starting => proto::ServerBinaryStatus::Starting, + BinaryStatus::Stopping => proto::ServerBinaryStatus::Stopping, + BinaryStatus::Stopped => proto::ServerBinaryStatus::Stopped, + BinaryStatus::Failed { error } => { + message = Some(error); + proto::ServerBinaryStatus::Failed + } + }; + cx.emit(LspStoreEvent::LanguageServerUpdate { + // Binary updates are about the binary that might not have any language server id at that point. + // Reuse `LanguageServerUpdate` for them and provide a fake id that won't be used on the receiver side. + language_server_id: LanguageServerId(0), + name: Some(server_name), + message: proto::update_language_server::Variant::StatusUpdate( + proto::StatusUpdate { + message, + status: Some(proto::status_update::Status::Binary( + binary_status as i32, + )), + }, + ), + }); + }) + .is_err() + { + break; + } + } + }) +} + fn lsp_workspace_diagnostics_refresh( server: Arc, cx: &mut Context<'_, LspStore>, @@ -11286,7 +11690,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { fn update_status(&self, server_name: LanguageServerName, status: language::BinaryStatus) { self.language_registry - .update_lsp_status(server_name, LanguageServerStatusUpdate::Binary(status)); + .update_lsp_binary_status(server_name, status); } fn registered_lsp_adapters(&self) -> Vec> { diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 78401ac797..d78715d385 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -1,11 +1,11 @@ use ::serde::{Deserialize, Serialize}; use anyhow::Context as _; -use gpui::{App, Entity, SharedString, Task, WeakEntity}; -use language::{LanguageServerStatusUpdate, ServerHealth}; +use gpui::{App, Entity, Task, WeakEntity}; +use language::ServerHealth; use lsp::LanguageServer; use rpc::proto; -use crate::{LspStore, Project, ProjectPath, lsp_store}; +use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; pub const RUST_ANALYZER_NAME: &str = "rust-analyzer"; pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc"; @@ -36,24 +36,45 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: .on_notification::({ let name = name.clone(); move |params, cx| { - let status = params.message; - let log_message = - format!("Language server {name} (id {server_id}) status update: {status:?}"); - match ¶ms.health { - ServerHealth::Ok => log::info!("{log_message}"), - ServerHealth::Warning => log::warn!("{log_message}"), - ServerHealth::Error => log::error!("{log_message}"), - } + let message = params.message; + let log_message = message.as_ref().map(|message| { + format!("Language server {name} (id {server_id}) status update: {message}") + }); + let status = match ¶ms.health { + ServerHealth::Ok => { + if let Some(log_message) = log_message { + log::info!("{log_message}"); + } + proto::ServerHealth::Ok + } + ServerHealth::Warning => { + if let Some(log_message) = log_message { + log::warn!("{log_message}"); + } + proto::ServerHealth::Warning + } + ServerHealth::Error => { + if let Some(log_message) = log_message { + log::error!("{log_message}"); + } + proto::ServerHealth::Error + } + }; lsp_store - .update(cx, |lsp_store, _| { - lsp_store.languages.update_lsp_status( - name.clone(), - LanguageServerStatusUpdate::Health( - params.health, - status.map(SharedString::from), + .update(cx, |_, cx| { + cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server_id, + name: Some(name.clone()), + message: proto::update_language_server::Variant::StatusUpdate( + proto::StatusUpdate { + message, + status: Some(proto::status_update::Status::Health( + status as i32, + )), + }, ), - ); + }); }) .ok(); } diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 1ac990a508..0283f06eec 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -74,6 +74,7 @@ impl LanguageServerTreeNode { pub(crate) fn server_id(&self) -> Option { self.0.upgrade()?.id.get().copied() } + /// Returns a language server ID for this node if it has already been initialized; otherwise runs the provided closure to initialize the language server node in a tree. /// May return None if the node no longer belongs to the server tree it was created in. pub(crate) fn server_id_or_init( @@ -87,6 +88,11 @@ impl LanguageServerTreeNode { .get_or_init(|| init(LaunchDisposition::from(&*this))), ) } + + /// Returns a language server name as the language server adapter would return. + pub fn name(&self) -> Option { + self.0.upgrade().map(|node| node.name.clone()) + } } impl From> for LanguageServerTreeNode { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e8b3814850..cdf6661063 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -81,7 +81,7 @@ use language::{ }; use lsp::{ CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode, - LanguageServerId, LanguageServerName, MessageActionItem, + LanguageServerId, LanguageServerName, LanguageServerSelector, MessageActionItem, }; use lsp_command::*; use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle}; @@ -251,6 +251,7 @@ enum BufferOrderedMessage { LanguageServerUpdate { language_server_id: LanguageServerId, message: proto::update_language_server::Variant, + name: Option, }, Resync, } @@ -1790,7 +1791,7 @@ impl Project { pub fn has_open_buffer(&self, path: impl Into, cx: &App) -> bool { self.buffer_store .read(cx) - .get_by_path(&path.into(), cx) + .get_by_path(&path.into()) .is_some() } @@ -2500,7 +2501,7 @@ impl Project { cx: &mut App, ) -> OpenLspBufferHandle { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.register_buffer_with_language_servers(&buffer, false, cx) + lsp_store.register_buffer_with_language_servers(&buffer, HashSet::default(), false, cx) }) } @@ -2590,7 +2591,7 @@ impl Project { } pub fn get_open_buffer(&self, path: &ProjectPath, cx: &App) -> Option> { - self.buffer_store.read(cx).get_by_path(path, cx) + self.buffer_store.read(cx).get_by_path(path) } fn register_buffer(&mut self, buffer: &Entity, cx: &mut Context) -> Result<()> { @@ -2640,7 +2641,7 @@ impl Project { } async fn send_buffer_ordered_messages( - this: WeakEntity, + project: WeakEntity, rx: UnboundedReceiver, cx: &mut AsyncApp, ) -> Result<()> { @@ -2677,7 +2678,7 @@ impl Project { let mut changes = rx.ready_chunks(MAX_BATCH_SIZE); while let Some(changes) = changes.next().await { - let is_local = this.read_with(cx, |this, _| this.is_local())?; + let is_local = project.read_with(cx, |this, _| this.is_local())?; for change in changes { match change { @@ -2697,7 +2698,7 @@ impl Project { BufferOrderedMessage::Resync => { operations_by_buffer_id.clear(); - if this + if project .update(cx, |this, cx| this.synchronize_remote_buffers(cx))? .await .is_ok() @@ -2709,9 +2710,10 @@ impl Project { BufferOrderedMessage::LanguageServerUpdate { language_server_id, message, + name, } => { flush_operations( - &this, + &project, &mut operations_by_buffer_id, &mut needs_resync_with_host, is_local, @@ -2719,12 +2721,14 @@ impl Project { ) .await?; - this.read_with(cx, |this, _| { - if let Some(project_id) = this.remote_id() { - this.client + project.read_with(cx, |project, _| { + if let Some(project_id) = project.remote_id() { + project + .client .send(proto::UpdateLanguageServer { project_id, - language_server_id: language_server_id.0 as u64, + server_name: name.map(|name| String::from(name.0)), + language_server_id: language_server_id.to_proto(), variant: Some(message), }) .log_err(); @@ -2735,7 +2739,7 @@ impl Project { } flush_operations( - &this, + &project, &mut operations_by_buffer_id, &mut needs_resync_with_host, is_local, @@ -2856,12 +2860,14 @@ impl Project { LspStoreEvent::LanguageServerUpdate { language_server_id, message, + name, } => { if self.is_local() { self.enqueue_buffer_ordered_message( BufferOrderedMessage::LanguageServerUpdate { language_server_id: *language_server_id, message: message.clone(), + name: name.clone(), }, ) .ok(); @@ -3140,20 +3146,22 @@ impl Project { pub fn restart_language_servers_for_buffers( &mut self, buffers: Vec>, + only_restart_servers: HashSet, cx: &mut Context, ) { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.restart_language_servers_for_buffers(buffers, cx) + lsp_store.restart_language_servers_for_buffers(buffers, only_restart_servers, cx) }) } pub fn stop_language_servers_for_buffers( &mut self, buffers: Vec>, + also_restart_servers: HashSet, cx: &mut Context, ) { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.stop_language_servers_for_buffers(buffers, cx) + lsp_store.stop_language_servers_for_buffers(buffers, also_restart_servers, cx) }) } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 19029cdb1d..d2a4e5126c 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -49,6 +49,10 @@ pub struct ProjectSettings { #[serde(default)] pub lsp: HashMap, + /// Common language server settings. + #[serde(default)] + pub global_lsp_settings: GlobalLspSettings, + /// Configuration for Debugger-related features #[serde(default)] pub dap: HashMap, @@ -110,6 +114,16 @@ pub enum ContextServerSettings { }, } +/// Common language server settings. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct GlobalLspSettings { + /// Whether to show the LSP servers button in the status bar. + /// + /// Default: `true` + #[serde(default = "default_true")] + pub button: bool, +} + impl ContextServerSettings { pub fn default_extension() -> Self { Self::Extension { @@ -271,6 +285,14 @@ impl Default for InlineDiagnosticsSettings { } } +impl Default for GlobalLspSettings { + fn default() -> Self { + Self { + button: default_true(), + } + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct CargoDiagnosticsSettings { /// When enabled, Zed disables rust-analyzer's check on save and starts to query diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index cab6ccc0fb..19b88c0695 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -918,6 +918,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { project.update(cx, |project, cx| { project.restart_language_servers_for_buffers( vec![rust_buffer.clone(), json_buffer.clone()], + HashSet::default(), cx, ); }); @@ -1715,12 +1716,16 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC // Restart the server before the diagnostics finish updating. project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer], cx); + project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), cx); }); let mut events = cx.events(&project); // Simulate the newly started server sending more diagnostics. let fake_server = fake_servers.next().await.unwrap(); + assert_eq!( + events.next().await.unwrap(), + Event::LanguageServerRemoved(LanguageServerId(0)) + ); assert_eq!( events.next().await.unwrap(), Event::LanguageServerAdded( @@ -1820,7 +1825,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], cx); + project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx); }); // The diagnostics are cleared. @@ -1875,7 +1880,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T }); cx.executor().run_until_parked(); project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], cx); + project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx); }); let mut fake_server = fake_servers.next().await.unwrap(); diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 71831759e5..0743b94e55 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -534,12 +534,15 @@ message DiagnosticSummary { message UpdateLanguageServer { uint64 project_id = 1; uint64 language_server_id = 2; + optional string server_name = 8; oneof variant { LspWorkStart work_start = 3; LspWorkProgress work_progress = 4; LspWorkEnd work_end = 5; LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6; LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7; + StatusUpdate status_update = 9; + RegisteredForBuffer registered_for_buffer = 10; } } @@ -566,6 +569,34 @@ message LspDiskBasedDiagnosticsUpdating {} message LspDiskBasedDiagnosticsUpdated {} +message StatusUpdate { + optional string message = 1; + oneof status { + ServerBinaryStatus binary = 2; + ServerHealth health = 3; + } +} + +enum ServerHealth { + OK = 0; + WARNING = 1; + ERROR = 2; +} + +enum ServerBinaryStatus { + NONE = 0; + CHECKING_FOR_UPDATE = 1; + DOWNLOADING = 2; + STARTING = 3; + STOPPING = 4; + STOPPED = 5; + FAILED = 6; +} + +message RegisteredForBuffer { + string buffer_abs_path = 1; +} + message LanguageServerLog { uint64 project_id = 1; uint64 language_server_id = 2; @@ -593,6 +624,7 @@ message ApplyCodeActionKindResponse { message RegisterBufferWithLanguageServers { uint64 project_id = 1; uint64 buffer_id = 2; + repeated LanguageServerSelector only_servers = 3; } enum FormatTrigger { @@ -730,14 +762,25 @@ message MultiLspQuery { message AllLanguageServers {} +message LanguageServerSelector { + oneof selector { + uint64 server_id = 1; + string name = 2; + } +} + message RestartLanguageServers { uint64 project_id = 1; repeated uint64 buffer_ids = 2; + repeated LanguageServerSelector only_servers = 3; + bool all = 4; } message StopLanguageServers { uint64 project_id = 1; repeated uint64 buffer_ids = 2; + repeated LanguageServerSelector also_servers = 3; + bool all = 4; } message MultiLspQueryResponse { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index c12d8dd37c..fa5f2617df 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -301,11 +301,13 @@ impl HeadlessProject { match event { LspStoreEvent::LanguageServerUpdate { language_server_id, + name, message, } => { self.session .send(proto::UpdateLanguageServer { project_id: SSH_PROJECT_ID, + server_name: name.as_ref().map(|name| name.to_string()), language_server_id: language_server_id.to_proto(), variant: Some(message.clone()), }) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1e3d648d42..3853229243 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5617,7 +5617,6 @@ impl Workspace { } else if let Some((notification_id, _)) = self.notifications.pop() { dismiss_app_notification(¬ification_id, cx); } else { - cx.emit(Event::ClearActivityIndicator); cx.propagate(); } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 62e29eb7e2..510cdb2b46 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -30,6 +30,7 @@ use gpui::{ px, retain_all, }; use image_viewer::ImageInfo; +use language_tools::lsp_tool::LspTool; use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; use migrator::{migrate_keymap, migrate_settings}; pub use open_listener::*; @@ -295,7 +296,7 @@ pub fn initialize_workspace( let popover_menu_handle = PopoverMenuHandle::default(); - let inline_completion_button = cx.new(|cx| { + let edit_prediction_button = cx.new(|cx| { inline_completion_button::InlineCompletionButton::new( app_state.fs.clone(), app_state.user_store.clone(), @@ -315,7 +316,7 @@ pub fn initialize_workspace( cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); let activity_indicator = activity_indicator::ActivityIndicator::new( workspace, - app_state.languages.clone(), + workspace.project().read(cx).languages().clone(), window, cx, ); @@ -325,13 +326,16 @@ pub fn initialize_workspace( cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx)); let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx)); let image_info = cx.new(|_cx| ImageInfo::new(workspace)); + let lsp_tool = cx.new(|cx| LspTool::new(workspace, window, cx)); + let cursor_position = cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(search_button, window, cx); status_bar.add_left_item(diagnostic_summary, window, cx); + status_bar.add_left_item(lsp_tool, window, cx); status_bar.add_left_item(activity_indicator, window, cx); - status_bar.add_right_item(inline_completion_button, window, cx); + status_bar.add_right_item(edit_prediction_button, window, cx); status_bar.add_right_item(active_buffer_language, window, cx); status_bar.add_right_item(active_toolchain_language, window, cx); status_bar.add_right_item(vim_mode_indicator, window, cx); @@ -4300,6 +4304,7 @@ mod tests { "jj", "journal", "language_selector", + "lsp_tool", "markdown", "menu", "notebook",