Add language server control tool into the status bar (#32490)

Kirill Bulatov and Nate Butler created

Release Notes:

- Added the language server control tool into the status bar

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>

Change summary

Cargo.lock                                             |   2 
assets/keymaps/default-linux.json                      |   3 
assets/keymaps/default-macos.json                      |   3 
assets/settings/default.json                           |   5 
crates/activity_indicator/Cargo.toml                   |   1 
crates/activity_indicator/src/activity_indicator.rs    | 103 +
crates/collab/src/rpc.rs                               |   1 
crates/editor/src/editor.rs                            |   6 
crates/extension_host/src/extension_store_test.rs      |  18 
crates/git_ui/src/branch_picker.rs                     |   4 
crates/git_ui/src/repository_selector.rs               |  13 
crates/language/src/language_registry.rs               |  19 
crates/language_extension/src/extension_lsp_adapter.rs |  10 
crates/language_tools/Cargo.toml                       |   4 
crates/language_tools/src/language_tools.rs            |  39 
crates/language_tools/src/lsp_log.rs                   | 153 +
crates/language_tools/src/lsp_tool.rs                  | 917 ++++++++++++
crates/lsp/src/lsp.rs                                  |   6 
crates/picker/src/picker.rs                            |   2 
crates/project/src/buffer_store.rs                     |   4 
crates/project/src/debugger/breakpoint_store.rs        |   2 
crates/project/src/git_store.rs                        |   6 
crates/project/src/lsp_store.rs                        | 609 +++++--
crates/project/src/lsp_store/rust_analyzer_ext.rs      |  57 
crates/project/src/manifest_tree/server_tree.rs        |   6 
crates/project/src/project.rs                          |  38 
crates/project/src/project_settings.rs                 |  22 
crates/project/src/project_tests.rs                    |  11 
crates/proto/proto/lsp.proto                           |  43 
crates/remote_server/src/headless_project.rs           |   2 
crates/workspace/src/workspace.rs                      |   1 
crates/zed/src/zed.rs                                  |  11 
32 files changed, 1,834 insertions(+), 287 deletions(-)

Detailed changes

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",

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"
     }
   },
   {

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"
     }
   },
   {

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

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

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<Self>,
     ) {
-        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::<LanguageServerName>::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());
                 }

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(

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);

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.

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<Picker<Self>>) -> Option<AnyElement> {
-        None
-    }
-
     fn render_match(
         &self,
         ix: usize,

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<Picker<Self>>,
-    ) -> Option<AnyElement> {
-        // TODO: Implement header rendering if needed
-        None
-    }
-
     fn render_match(
         &self,
         ix: usize,

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<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerStatusUpdate)>>>>,
+    txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, BinaryStatus)>>>>,
 }
 
 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());
     }

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);
     }
 }
 

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"] }

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<T>(
+    workspace: &mut Workspace,
+    destination: SplitDirection,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+    new_tool: impl FnOnce(&mut Window, &mut Context<T>) -> T,
+) -> Entity<T>
+where
+    T: Item,
+{
+    if let Some(item) = workspace.item_of_type::<T>(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
+}

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<str> {
     }
 }
 
-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<LanguageServerName>,
     worktree_id: Option<WorktreeId>,
     kind: LanguageServerKind,
@@ -204,8 +206,13 @@ pub(crate) struct LogMenuItem {
 
 actions!(dev, [OpenLanguageServerLogs]);
 
+pub(super) struct GlobalLogStore(pub WeakEntity<LogStore>);
+
+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<LogMessage>> {
+    pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
         Some(&self.language_servers.get(&server_id)?.log_messages)
     }
 
-    fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> {
+    pub(super) fn server_trace(
+        &self,
+        server_id: LanguageServerId,
+    ) -> Option<&VecDeque<TraceMessage>> {
         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<Workspace>,
+        server: LanguageServerSelector,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        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<Workspace>,
+        server: LanguageServerSelector,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        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);
     }
 }
 

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<PickerState>,
+    lsp_picker: Option<Entity<Picker<LspPickerDelegate>>>,
+    _subscriptions: Vec<Subscription>,
+}
+
+struct PickerState {
+    workspace: WeakEntity<Workspace>,
+    lsp_store: WeakEntity<LspStore>,
+    active_editor: Option<ActiveEditor>,
+    language_servers: LanguageServers,
+}
+
+#[derive(Debug)]
+struct LspPickerDelegate {
+    state: Entity<PickerState>,
+    selected_index: usize,
+    items: Vec<LspItem>,
+    other_servers_start_index: Option<usize>,
+}
+
+struct ActiveEditor {
+    editor: WeakEntity<Editor>,
+    _editor_subscription: Subscription,
+    editor_buffers: HashSet<BufferId>,
+}
+
+#[derive(Debug, Default, Clone)]
+struct LanguageServers {
+    health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
+    binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
+    servers_per_buffer_abs_path:
+        HashMap<PathBuf, HashMap<LanguageServerId, Option<LanguageServerName>>>,
+}
+
+#[derive(Debug, Clone)]
+struct LanguageServerHealthStatus {
+    name: LanguageServerName,
+    health: Option<(Option<SharedString>, ServerHealth)>,
+}
+
+#[derive(Debug, Clone)]
+struct LanguageServerBinaryStatus {
+    status: BinaryStatus,
+    message: Option<SharedString>,
+}
+
+impl LanguageServerHealthStatus {
+    fn health(&self) -> Option<ServerHealth> {
+        self.health.as_ref().map(|(_, health)| *health)
+    }
+
+    fn message(&self) -> Option<SharedString> {
+        self.health
+            .as_ref()
+            .and_then(|(message, _)| message.clone())
+    }
+}
+
+impl LspPickerDelegate {
+    fn regenerate_items(&mut self, cx: &mut Context<Picker<Self>>) {
+        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::<Vec<_>>();
+
+            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<LanguageServerName>,
+    ) {
+        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<LanguageServerBinaryStatus>,
+    ),
+    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<Picker<Self>>) {
+        self.selected_index = ix;
+        cx.notify();
+    }
+
+    fn update_matches(
+        &mut self,
+        _: String,
+        _: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> 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<str> {
+        Arc::default()
+    }
+
+    fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context<Picker<Self>>) {}
+
+    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+        cx.emit(DismissEvent);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        _: bool,
+        _: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        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::<GlobalLogStore>().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::<Vec<_>>()
+                                            } else {
+                                                editor_buffers
+                                                    .iter()
+                                                    .flat_map(|buffer_id| {
+                                                        buffer_store.read(cx).get(*buffer_id)
+                                                    })
+                                                    .collect::<Vec<_>>()
+                                            };
+                                            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<Editor>,
+        _: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Div {
+        div().child(div().track_focus(&editor.focus_handle(cx)))
+    }
+
+    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
+        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<usize> {
+        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>) -> Self {
+        let settings_subscription =
+            cx.observe_global_in::<SettingsStore>(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<Self>,
+    ) {
+        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<PickerState>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Entity<Picker<LspPickerDelegate>> {
+        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<Self>,
+    ) {
+        if ProjectSettings::get_global(cx).global_lsp_settings.button {
+            if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
+                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<Self>) -> 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),
+        )
+    }
+}

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)]

crates/picker/src/picker.rs 🔗

@@ -205,6 +205,7 @@ pub trait PickerDelegate: Sized + 'static {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem>;
+
     fn render_header(
         &self,
         _window: &mut Window,
@@ -212,6 +213,7 @@ pub trait PickerDelegate: Sized + 'static {
     ) -> Option<AnyElement> {
         None
     }
+
     fn render_footer(
         &self,
         _window: &mut Window,

crates/project/src/buffer_store.rs 🔗

@@ -783,7 +783,7 @@ impl BufferStore {
         project_path: ProjectPath,
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Buffer>>> {
-        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<Entity<Buffer>> {
+    pub fn get_by_path(&self, path: &ProjectPath) -> Option<Entity<Buffer>> {
         self.path_to_buffer_id.get(path).and_then(|buffer_id| {
             let buffer = self.get(*buffer_id);
             buffer

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

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)?;

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<dyn LspAdapterDelegate>;
             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<LspStore>,
+        _: &mut Context<LspStore>,
     ) -> impl Future<Output = ()> + 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::<Vec<_>>();
 
         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<Buffer>,
+        only_register_servers: HashSet<LanguageServerSelector>,
         cx: &mut Context<LspStore>,
     ) {
         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<WorktreeStore>,
     toolchain_store: Option<Entity<ToolchainStore>>,
     pub languages: Arc<LanguageRegistry>,
-    pub language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
+    language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
     active_entry: Option<ProjectEntryId>,
     _maintain_workspace_config: (Task<Result<()>>, watch::Sender<()>),
     _maintain_buffer_languages: Task<()>,
@@ -3503,11 +3560,13 @@ struct BufferLspData {
     colors: Option<Vec<DocumentColor>>,
 }
 
+#[derive(Debug)]
 pub enum LspStoreEvent {
     LanguageServerAdded(LanguageServerId, LanguageServerName, Option<WorktreeId>),
     LanguageServerRemoved(LanguageServerId),
     LanguageServerUpdate {
         language_server_id: LanguageServerId,
+        name: Option<LanguageServerName>,
         message: proto::update_language_server::Variant,
     },
     LanguageServerLog(LanguageServerId, LanguageServerLogType, String),
@@ -3682,6 +3741,7 @@ impl LspStore {
         }
         cx.observe_global::<SettingsStore>(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<Buffer>,
+        only_register_servers: HashSet<LanguageServerSelector>,
         ignore_refcounts: bool,
         cx: &mut Context<Self>,
     ) -> 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<Self>,
+        lsp_store: Entity<Self>,
         envelope: TypedEnvelope<proto::UpdateLanguageServer>,
         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<proto::RestartLanguageServers>,
         mut cx: AsyncApp,
     ) -> Result<proto::Ack> {
-        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<Self>,
+        lsp_store: Entity<Self>,
         envelope: TypedEnvelope<proto::StopLanguageServers>,
         mut cx: AsyncApp,
     ) -> Result<proto::Ack> {
-        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<Self>,
     ) -> Task<Vec<WorktreeId>> {
         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<Self>) {
+        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::<Vec<_>>();
+            cx.background_spawn(async move {
+                futures::future::join_all(tasks).await;
+            })
+            .detach();
+        }
     }
 
     pub fn restart_language_servers_for_buffers(
         &mut self,
         buffers: Vec<Entity<Buffer>>,
+        only_restart_servers: HashSet<LanguageServerSelector>,
         cx: &mut Context<Self>,
     ) {
         if let Some((client, project_id)) = self.upstream_client() {

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<LspStore>, language_server:
         .on_notification::<ServerStatus, _>({
             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 &params.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 &params.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();
             }

crates/project/src/manifest_tree/server_tree.rs 🔗

@@ -74,6 +74,7 @@ impl LanguageServerTreeNode {
     pub(crate) fn server_id(&self) -> Option<LanguageServerId> {
         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<LanguageServerName> {
+        self.0.upgrade().map(|node| node.name.clone())
+    }
 }
 
 impl From<Weak<InnerTreeNode>> for LanguageServerTreeNode {

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<LanguageServerName>,
     },
     Resync,
 }
@@ -1790,7 +1791,7 @@ impl Project {
     pub fn has_open_buffer(&self, path: impl Into<ProjectPath>, 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<Entity<Buffer>> {
-        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<Buffer>, cx: &mut Context<Self>) -> Result<()> {
@@ -2640,7 +2641,7 @@ impl Project {
     }
 
     async fn send_buffer_ordered_messages(
-        this: WeakEntity<Self>,
+        project: WeakEntity<Self>,
         rx: UnboundedReceiver<BufferOrderedMessage>,
         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<Entity<Buffer>>,
+        only_restart_servers: HashSet<LanguageServerSelector>,
         cx: &mut Context<Self>,
     ) {
         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<Entity<Buffer>>,
+        also_restart_servers: HashSet<LanguageServerSelector>,
         cx: &mut Context<Self>,
     ) {
         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)
         })
     }
 

crates/project/src/project_settings.rs 🔗

@@ -49,6 +49,10 @@ pub struct ProjectSettings {
     #[serde(default)]
     pub lsp: HashMap<LanguageServerName, LspSettings>,
 
+    /// Common language server settings.
+    #[serde(default)]
+    pub global_lsp_settings: GlobalLspSettings,
+
     /// Configuration for Debugger-related features
     #[serde(default)]
     pub dap: HashMap<DebugAdapterName, DapSettings>,
@@ -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

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();

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 {

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()),
                     })

crates/workspace/src/workspace.rs 🔗

@@ -5617,7 +5617,6 @@ impl Workspace {
         } else if let Some((notification_id, _)) = self.notifications.pop() {
             dismiss_app_notification(&notification_id, cx);
         } else {
-            cx.emit(Event::ClearActivityIndicator);
             cx.propagate();
         }
     }

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",