Remote LSP logs (#37083)

Kirill Bulatov , Ben Kunkle , and Lukas Wirth created

Take 2: https://github.com/zed-industries/zed/pull/36709 but without the
very bad `cfg`-based approach for storing the RPC logs.

--------------

Enables LSP log tracing in both remote collab and remote ssh
environments.
Server logs and server RPC traces can now be viewed remotely, and the
LSP button is now shown in such projects too.

Closes https://github.com/zed-industries/zed/issues/28557

Co-Authored-By: Kirill <kirill@zed.dev>
Co-Authored-By: Lukas <lukas@zed.dev>

Release Notes:

- Enabled LSP log tracing in both remote collab and remote ssh
environments

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>

Change summary

Cargo.lock                                                      |   1 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql  |   1 
crates/collab/migrations/20250827084812_worktree_in_servers.sql |   2 
crates/collab/src/db/queries/projects.rs                        |   4 
crates/collab/src/db/queries/rooms.rs                           |   2 
crates/collab/src/db/tables/language_server.rs                  |   1 
crates/collab/src/rpc.rs                                        |   4 
crates/language_tools/Cargo.toml                                |   1 
crates/language_tools/src/language_tools.rs                     |  10 
crates/language_tools/src/lsp_button.rs                         | 141 
crates/language_tools/src/lsp_log_view.rs                       | 911 --
crates/language_tools/src/lsp_log_view_tests.rs                 |  14 
crates/project/src/lsp_store.rs                                 | 128 
crates/project/src/lsp_store/log_store.rs                       | 704 ++
crates/project/src/project.rs                                   |  64 
crates/project/src/project_tests.rs                             |   1 
crates/proto/proto/lsp.proto                                    |  44 
crates/proto/proto/zed.proto                                    |   3 
crates/proto/src/proto.rs                                       |   7 
crates/remote_server/src/headless_project.rs                    |  79 
crates/settings/src/settings.rs                                 |   2 
crates/zed/src/zed.rs                                           |  17 
22 files changed, 1,304 insertions(+), 837 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9213,6 +9213,7 @@ dependencies = [
  "language",
  "lsp",
  "project",
+ "proto",
  "release_channel",
  "serde_json",
  "settings",

crates/collab/src/db/queries/projects.rs 🔗

@@ -694,6 +694,7 @@ impl Database {
                 project_id: ActiveValue::set(project_id),
                 id: ActiveValue::set(server.id as i64),
                 name: ActiveValue::set(server.name.clone()),
+                worktree_id: ActiveValue::set(server.worktree_id.map(|id| id as i64)),
                 capabilities: ActiveValue::set(update.capabilities.clone()),
             })
             .on_conflict(
@@ -704,6 +705,7 @@ impl Database {
                 .update_columns([
                     language_server::Column::Name,
                     language_server::Column::Capabilities,
+                    language_server::Column::WorktreeId,
                 ])
                 .to_owned(),
             )
@@ -1065,7 +1067,7 @@ impl Database {
                     server: proto::LanguageServer {
                         id: language_server.id as u64,
                         name: language_server.name,
-                        worktree_id: None,
+                        worktree_id: language_server.worktree_id.map(|id| id as u64),
                     },
                     capabilities: language_server.capabilities,
                 })

crates/collab/src/db/queries/rooms.rs 🔗

@@ -809,7 +809,7 @@ impl Database {
                 server: proto::LanguageServer {
                     id: language_server.id as u64,
                     name: language_server.name,
-                    worktree_id: None,
+                    worktree_id: language_server.worktree_id.map(|id| id as u64),
                 },
                 capabilities: language_server.capabilities,
             })

crates/collab/src/rpc.rs 🔗

@@ -476,7 +476,9 @@ impl Server {
             .add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
             .add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
             .add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
-            .add_message_handler(update_context);
+            .add_message_handler(update_context)
+            .add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>)
+            .add_message_handler(broadcast_project_message_from_host::<proto::LanguageServerLog>);
 
         Arc::new(server)
     }

crates/language_tools/Cargo.toml 🔗

@@ -24,6 +24,7 @@ itertools.workspace = true
 language.workspace = true
 lsp.workspace = true
 project.workspace = true
+proto.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 theme.workspace = true

crates/language_tools/src/language_tools.rs 🔗

@@ -1,20 +1,20 @@
 mod key_context_view;
-mod lsp_log;
-pub mod lsp_tool;
+pub mod lsp_button;
+pub mod lsp_log_view;
 mod syntax_tree_view;
 
 #[cfg(test)]
-mod lsp_log_tests;
+mod lsp_log_view_tests;
 
 use gpui::{App, AppContext, Entity};
 
-pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
+pub use lsp_log_view::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);
+    lsp_log_view::init(true, cx);
     syntax_tree_view::init(cx);
     key_context_view::init(cx);
 }

crates/language_tools/src/lsp_tool.rs → crates/language_tools/src/lsp_button.rs 🔗

@@ -11,7 +11,10 @@ use editor::{Editor, EditorEvent};
 use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
 use language::{BinaryStatus, BufferId, ServerHealth};
 use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
-use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings};
+use project::{
+    LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
+    project_settings::ProjectSettings,
+};
 use settings::{Settings as _, SettingsStore};
 use ui::{
     Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
@@ -20,7 +23,7 @@ use ui::{
 
 use workspace::{StatusItemView, Workspace};
 
-use crate::lsp_log::GlobalLogStore;
+use crate::lsp_log_view;
 
 actions!(
     lsp_tool,
@@ -30,7 +33,7 @@ actions!(
     ]
 );
 
-pub struct LspTool {
+pub struct LspButton {
     server_state: Entity<LanguageServerState>,
     popover_menu_handle: PopoverMenuHandle<ContextMenu>,
     lsp_menu: Option<Entity<ContextMenu>>,
@@ -121,9 +124,8 @@ impl LanguageServerState {
         menu = menu.align_popover_bottom();
         let lsp_logs = cx
             .try_global::<GlobalLogStore>()
-            .and_then(|lsp_logs| lsp_logs.0.upgrade());
-        let lsp_store = self.lsp_store.upgrade();
-        let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else {
+            .map(|lsp_logs| lsp_logs.0.clone());
+        let Some(lsp_logs) = lsp_logs else {
             return menu;
         };
 
@@ -210,10 +212,11 @@ impl LanguageServerState {
             };
 
             let server_selector = server_info.server_selector();
-            // 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 is_remote = self
+                .lsp_store
+                .update(cx, |lsp_store, _| lsp_store.as_remote().is_some())
+                .unwrap_or(false);
+            let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
 
             let status_color = server_info
                 .binary_status
@@ -241,10 +244,10 @@ impl LanguageServerState {
                 .as_ref()
                 .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
                 .cloned();
-            let hover_label = if has_logs {
-                Some("View Logs")
-            } else if message.is_some() {
+            let hover_label = if message.is_some() {
                 Some("View Message")
+            } else if has_logs {
+                Some("View Logs")
             } else {
                 None
             };
@@ -288,16 +291,7 @@ impl LanguageServerState {
                     let server_name = server_info.name.clone();
                     let workspace = self.workspace.clone();
                     move |window, cx| {
-                        if has_logs {
-                            lsp_logs.update(cx, |lsp_logs, cx| {
-                                lsp_logs.open_server_trace(
-                                    workspace.clone(),
-                                    server_selector.clone(),
-                                    window,
-                                    cx,
-                                );
-                            });
-                        } else if let Some(message) = &message {
+                        if let Some(message) = &message {
                             let Some(create_buffer) = workspace
                                 .update(cx, |workspace, cx| {
                                     workspace
@@ -347,6 +341,14 @@ impl LanguageServerState {
                                 anyhow::Ok(())
                             })
                             .detach();
+                        } else if has_logs {
+                            lsp_log_view::open_server_trace(
+                                &lsp_logs,
+                                workspace.clone(),
+                                server_selector.clone(),
+                                window,
+                                cx,
+                            );
                         } else {
                             cx.propagate();
                         }
@@ -510,7 +512,7 @@ impl ServerData<'_> {
     }
 }
 
-impl LspTool {
+impl LspButton {
     pub fn new(
         workspace: &Workspace,
         popover_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -518,37 +520,59 @@ impl LspTool {
         cx: &mut Context<Self>,
     ) -> Self {
         let settings_subscription =
-            cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
+            cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
                 if ProjectSettings::get_global(cx).global_lsp_settings.button {
-                    if lsp_tool.lsp_menu.is_none() {
-                        lsp_tool.refresh_lsp_menu(true, window, cx);
+                    if lsp_button.lsp_menu.is_none() {
+                        lsp_button.refresh_lsp_menu(true, window, cx);
                     }
-                } else if lsp_tool.lsp_menu.take().is_some() {
+                } else if lsp_button.lsp_menu.take().is_some() {
                     cx.notify();
                 }
             });
 
         let lsp_store = workspace.project().read(cx).lsp_store();
+        let mut language_servers = LanguageServers::default();
+        for (_, status) in lsp_store.read(cx).language_server_statuses() {
+            language_servers.binary_statuses.insert(
+                status.name.clone(),
+                LanguageServerBinaryStatus {
+                    status: BinaryStatus::None,
+                    message: None,
+                },
+            );
+        }
+
         let lsp_store_subscription =
-            cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| {
-                lsp_tool.on_lsp_store_event(e, window, cx)
+            cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
+                lsp_button.on_lsp_store_event(e, window, cx)
             });
 
-        let state = cx.new(|_| LanguageServerState {
+        let server_state = cx.new(|_| LanguageServerState {
             workspace: workspace.weak_handle(),
             items: Vec::new(),
             lsp_store: lsp_store.downgrade(),
             active_editor: None,
-            language_servers: LanguageServers::default(),
+            language_servers,
         });
 
-        Self {
-            server_state: state,
+        let mut lsp_button = Self {
+            server_state,
             popover_menu_handle,
             lsp_menu: None,
             lsp_menu_refresh: Task::ready(()),
             _subscriptions: vec![settings_subscription, lsp_store_subscription],
+        };
+        if !lsp_button
+            .server_state
+            .read(cx)
+            .language_servers
+            .binary_statuses
+            .is_empty()
+        {
+            lsp_button.refresh_lsp_menu(true, window, cx);
         }
+
+        lsp_button
     }
 
     fn on_lsp_store_event(
@@ -708,6 +732,25 @@ impl LspTool {
                     }
                 }
             }
+            state
+                .lsp_store
+                .update(cx, |lsp_store, cx| {
+                    for (server_id, status) in lsp_store.language_server_statuses() {
+                        if let Some(worktree) = status.worktree.and_then(|worktree_id| {
+                            lsp_store
+                                .worktree_store()
+                                .read(cx)
+                                .worktree_for_id(worktree_id, cx)
+                        }) {
+                            server_ids_to_worktrees.insert(server_id, worktree.clone());
+                            server_names_to_worktrees
+                                .entry(status.name.clone())
+                                .or_default()
+                                .insert((worktree, server_id));
+                        }
+                    }
+                })
+                .ok();
 
             let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
             let mut servers_without_worktree = Vec::<ServerData>::new();
@@ -852,18 +895,18 @@ impl LspTool {
     ) {
         if create_if_empty || self.lsp_menu.is_some() {
             let state = self.server_state.clone();
-            self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| {
+            self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| {
                 cx.background_executor()
                     .timer(Duration::from_millis(30))
                     .await;
-                lsp_tool
-                    .update_in(cx, |lsp_tool, window, cx| {
-                        lsp_tool.regenerate_items(cx);
+                lsp_button
+                    .update_in(cx, |lsp_button, window, cx| {
+                        lsp_button.regenerate_items(cx);
                         let menu = ContextMenu::build(window, cx, |menu, _, cx| {
                             state.update(cx, |state, cx| state.fill_menu(menu, cx))
                         });
-                        lsp_tool.lsp_menu = Some(menu.clone());
-                        lsp_tool.popover_menu_handle.refresh_menu(
+                        lsp_button.lsp_menu = Some(menu.clone());
+                        lsp_button.popover_menu_handle.refresh_menu(
                             window,
                             cx,
                             Rc::new(move |_, _| Some(menu.clone())),
@@ -876,7 +919,7 @@ impl LspTool {
     }
 }
 
-impl StatusItemView for LspTool {
+impl StatusItemView for LspButton {
     fn set_active_pane_item(
         &mut self,
         active_pane_item: Option<&dyn workspace::ItemHandle>,
@@ -899,9 +942,9 @@ impl StatusItemView for LspTool {
                     let _editor_subscription = cx.subscribe_in(
                         &editor,
                         window,
-                        |lsp_tool, _, e: &EditorEvent, window, cx| match e {
+                        |lsp_button, _, e: &EditorEvent, window, cx| match e {
                             EditorEvent::ExcerptsAdded { buffer, .. } => {
-                                let updated = lsp_tool.server_state.update(cx, |state, cx| {
+                                let updated = lsp_button.server_state.update(cx, |state, cx| {
                                     if let Some(active_editor) = state.active_editor.as_mut() {
                                         let buffer_id = buffer.read(cx).remote_id();
                                         active_editor.editor_buffers.insert(buffer_id)
@@ -910,13 +953,13 @@ impl StatusItemView for LspTool {
                                     }
                                 });
                                 if updated {
-                                    lsp_tool.refresh_lsp_menu(false, window, cx);
+                                    lsp_button.refresh_lsp_menu(false, window, cx);
                                 }
                             }
                             EditorEvent::ExcerptsRemoved {
                                 removed_buffer_ids, ..
                             } => {
-                                let removed = lsp_tool.server_state.update(cx, |state, _| {
+                                let removed = lsp_button.server_state.update(cx, |state, _| {
                                     let mut removed = false;
                                     if let Some(active_editor) = state.active_editor.as_mut() {
                                         for id in removed_buffer_ids {
@@ -930,7 +973,7 @@ impl StatusItemView for LspTool {
                                     removed
                                 });
                                 if removed {
-                                    lsp_tool.refresh_lsp_menu(false, window, cx);
+                                    lsp_button.refresh_lsp_menu(false, window, cx);
                                 }
                             }
                             _ => {}
@@ -960,7 +1003,7 @@ impl StatusItemView for LspTool {
     }
 }
 
-impl Render for LspTool {
+impl Render for LspButton {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
         if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
             return div();
@@ -1005,11 +1048,11 @@ impl Render for LspTool {
             (None, "All Servers Operational")
         };
 
-        let lsp_tool = cx.entity();
+        let lsp_button = cx.entity();
 
         div().child(
             PopoverMenu::new("lsp-tool")
-                .menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone())
+                .menu(move |_, cx| lsp_button.read(cx).lsp_menu.clone())
                 .anchor(Corner::BottomLeft)
                 .with_handle(self.popover_menu_handle.clone())
                 .trigger_with_tooltip(

crates/language_tools/src/lsp_log.rs → crates/language_tools/src/lsp_log_view.rs 🔗

@@ -1,20 +1,24 @@
-use collections::{HashMap, VecDeque};
+use collections::VecDeque;
 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, Global,
-    IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
+    AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
+    ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
 };
 use itertools::Itertools;
 use language::{LanguageServerId, language_settings::SoftWrap};
 use lsp::{
-    IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType,
+    LanguageServer, LanguageServerBinary, LanguageServerName, LanguageServerSelector, MessageType,
     SetTraceParams, TraceValue, notification::SetTrace,
 };
-use project::{Project, WorktreeId, search::SearchQuery};
+use project::{
+    Project,
+    lsp_store::log_store::{self, Event, LanguageServerKind, LogKind, LogStore, Message},
+    search::SearchQuery,
+};
 use std::{any::TypeId, borrow::Cow, sync::Arc};
 use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*};
+use util::ResultExt as _;
 use workspace::{
     SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
     item::{Item, ItemHandle},
@@ -23,132 +27,53 @@ use workspace::{
 
 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;
-
-pub struct LogStore {
-    projects: HashMap<WeakEntity<Project>, ProjectState>,
-    language_servers: HashMap<LanguageServerId, LanguageServerState>,
-    copilot_log_subscription: Option<lsp::Subscription>,
-    _copilot_subscription: Option<gpui::Subscription>,
-    io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
-}
-
-struct ProjectState {
-    _subscriptions: [gpui::Subscription; 2],
-}
-
-trait Message: AsRef<str> {
-    type Level: Copy + std::fmt::Debug;
-    fn should_include(&self, _: Self::Level) -> bool {
-        true
-    }
-}
-
-pub(super) struct LogMessage {
-    message: String,
-    typ: MessageType,
-}
-
-impl AsRef<str> for LogMessage {
-    fn as_ref(&self) -> &str {
-        &self.message
-    }
-}
-
-impl Message for LogMessage {
-    type Level = MessageType;
-
-    fn should_include(&self, level: Self::Level) -> bool {
-        match (self.typ, level) {
-            (MessageType::ERROR, _) => true,
-            (_, MessageType::ERROR) => false,
-            (MessageType::WARNING, _) => true,
-            (_, MessageType::WARNING) => false,
-            (MessageType::INFO, _) => true,
-            (_, MessageType::INFO) => false,
-            _ => true,
-        }
-    }
-}
-
-pub(super) struct TraceMessage {
-    message: String,
-}
-
-impl AsRef<str> for TraceMessage {
-    fn as_ref(&self) -> &str {
-        &self.message
-    }
-}
-
-impl Message for TraceMessage {
-    type Level = ();
-}
-
-struct RpcMessage {
-    message: String,
-}
-
-impl AsRef<str> for RpcMessage {
-    fn as_ref(&self) -> &str {
-        &self.message
-    }
-}
-
-impl Message for RpcMessage {
-    type Level = ();
-}
-
-pub(super) struct LanguageServerState {
-    name: Option<LanguageServerName>,
-    worktree_id: Option<WorktreeId>,
-    kind: LanguageServerKind,
-    log_messages: VecDeque<LogMessage>,
-    trace_messages: VecDeque<TraceMessage>,
-    rpc_state: Option<LanguageServerRpcState>,
-    trace_level: TraceValue,
-    log_level: MessageType,
-    io_logs_subscription: Option<lsp::Subscription>,
-}
-
-#[derive(PartialEq, Clone)]
-pub enum LanguageServerKind {
-    Local { project: WeakEntity<Project> },
-    Remote { project: WeakEntity<Project> },
-    Global,
-}
-
-impl LanguageServerKind {
-    fn is_remote(&self) -> bool {
-        matches!(self, LanguageServerKind::Remote { .. })
-    }
-}
-
-impl std::fmt::Debug for LanguageServerKind {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"),
-            LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"),
-            LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
-        }
-    }
-}
-
-impl LanguageServerKind {
-    fn project(&self) -> Option<&WeakEntity<Project>> {
-        match self {
-            Self::Local { project } => Some(project),
-            Self::Remote { project } => Some(project),
-            Self::Global { .. } => None,
-        }
-    }
-}
-
-struct LanguageServerRpcState {
-    rpc_messages: VecDeque<RpcMessage>,
-    last_message_kind: Option<MessageKind>,
+pub fn open_server_trace(
+    log_store: &Entity<LogStore>,
+    workspace: WeakEntity<Workspace>,
+    server: LanguageServerSelector,
+    window: &mut Window,
+    cx: &mut App,
+) {
+    log_store.update(cx, |_, cx| {
+        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();
+    })
 }
 
 pub struct LspLogView {
@@ -167,32 +92,6 @@ pub struct LspLogToolbarItemView {
     _log_view_subscription: Option<Subscription>,
 }
 
-#[derive(Copy, Clone, PartialEq, Eq)]
-enum MessageKind {
-    Send,
-    Receive,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub enum LogKind {
-    Rpc,
-    Trace,
-    #[default]
-    Logs,
-    ServerInfo,
-}
-
-impl LogKind {
-    fn label(&self) -> &'static str {
-        match self {
-            LogKind::Rpc => RPC_MESSAGES,
-            LogKind::Trace => SERVER_TRACE,
-            LogKind::Logs => SERVER_LOGS,
-            LogKind::ServerInfo => SERVER_INFO,
-        }
-    }
-}
-
 #[derive(Clone, Debug, PartialEq)]
 pub(crate) struct LogMenuItem {
     pub server_id: LanguageServerId,
@@ -212,59 +111,24 @@ actions!(
     ]
 );
 
-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();
-        if project.read(cx).is_local() || project.read(cx).is_via_remote_server() {
-            log_store.update(cx, |store, cx| {
-                store.add_project(project, cx);
-            });
-        }
-
-        let log_store = log_store.clone();
-        workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
-            let project = workspace.project().read(cx);
-            if project.is_local() || project.is_via_remote_server() {
-                let project = workspace.project().clone();
-                let log_store = log_store.clone();
-                get_or_create_tool(
-                    workspace,
-                    SplitDirection::Right,
-                    window,
-                    cx,
-                    move |window, cx| LspLogView::new(project, log_store, window, cx),
-                );
-            }
-        });
-    })
-    .detach();
-}
-
-impl LogStore {
-    pub fn new(cx: &mut Context<Self>) -> Self {
-        let (io_tx, mut io_rx) = mpsc::unbounded();
+pub fn init(store_logs: bool, cx: &mut App) {
+    let log_store = log_store::init(store_logs, cx);
 
-        let copilot_subscription = Copilot::global(cx).map(|copilot| {
+    log_store.update(cx, |_, cx| {
+        Copilot::global(cx).map(|copilot| {
             let copilot = &copilot;
-            cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| {
+            cx.subscribe(copilot, |log_store, copilot, edit_prediction_event, cx| {
                 if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event
                     && let Some(server) = copilot.read(cx).language_server()
                 {
                     let server_id = server.server_id();
-                    let weak_this = cx.weak_entity();
-                    this.copilot_log_subscription =
+                    let weak_lsp_store = cx.weak_entity();
+                    log_store.copilot_log_subscription =
                         Some(server.on_notification::<copilot::request::LogMessage, _>(
                             move |params, cx| {
-                                weak_this
-                                    .update(cx, |this, cx| {
-                                        this.add_language_server_log(
+                                weak_lsp_store
+                                    .update(cx, |lsp_store, cx| {
+                                        lsp_store.add_language_server_log(
                                             server_id,
                                             MessageType::LOG,
                                             &params.message,
@@ -274,8 +138,9 @@ impl LogStore {
                                     .ok();
                             },
                         ));
+
                     let name = LanguageServerName::new_static("copilot");
-                    this.add_language_server(
+                    log_store.add_language_server(
                         LanguageServerKind::Global,
                         server.server_id(),
                         Some(name),
@@ -285,429 +150,29 @@ impl LogStore {
                     );
                 }
             })
-        });
-
-        let this = Self {
-            copilot_log_subscription: None,
-            _copilot_subscription: copilot_subscription,
-            projects: HashMap::default(),
-            language_servers: HashMap::default(),
-            io_tx,
-        };
-
-        cx.spawn(async move |this, cx| {
-            while let Some((server_id, io_kind, message)) = io_rx.next().await {
-                if let Some(this) = this.upgrade() {
-                    this.update(cx, |this, cx| {
-                        this.on_io(server_id, io_kind, &message, cx);
-                    })?;
-                }
-            }
-            anyhow::Ok(())
+            .detach();
         })
-        .detach_and_log_err(cx);
-        this
-    }
-
-    pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
-        let weak_project = project.downgrade();
-        self.projects.insert(
-            project.downgrade(),
-            ProjectState {
-                _subscriptions: [
-                    cx.observe_release(project, move |this, _, _| {
-                        this.projects.remove(&weak_project);
-                        this.language_servers
-                            .retain(|_, state| state.kind.project() != Some(&weak_project));
-                    }),
-                    cx.subscribe(project, |this, project, event, cx| {
-                        let server_kind = if project.read(cx).is_via_remote_server() {
-                            LanguageServerKind::Remote {
-                                project: project.downgrade(),
-                            }
-                        } else {
-                            LanguageServerKind::Local {
-                                project: project.downgrade(),
-                            }
-                        };
+    });
 
-                        match event {
-                            project::Event::LanguageServerAdded(id, name, worktree_id) => {
-                                this.add_language_server(
-                                    server_kind,
-                                    *id,
-                                    Some(name.clone()),
-                                    *worktree_id,
-                                    project
-                                        .read(cx)
-                                        .lsp_store()
-                                        .read(cx)
-                                        .language_server_for_id(*id),
-                                    cx,
-                                );
-                            }
-                            project::Event::LanguageServerRemoved(id) => {
-                                this.remove_language_server(*id, cx);
-                            }
-                            project::Event::LanguageServerLog(id, typ, message) => {
-                                this.add_language_server(server_kind, *id, None, None, None, cx);
-                                match typ {
-                                    project::LanguageServerLogType::Log(typ) => {
-                                        this.add_language_server_log(*id, *typ, message, cx);
-                                    }
-                                    project::LanguageServerLogType::Trace(_) => {
-                                        this.add_language_server_trace(*id, message, cx);
-                                    }
-                                }
-                            }
-                            _ => {}
-                        }
-                    }),
-                ],
-            },
-        );
-    }
-
-    pub(super) fn get_language_server_state(
-        &mut self,
-        id: LanguageServerId,
-    ) -> Option<&mut LanguageServerState> {
-        self.language_servers.get_mut(&id)
-    }
-
-    fn add_language_server(
-        &mut self,
-        kind: LanguageServerKind,
-        server_id: LanguageServerId,
-        name: Option<LanguageServerName>,
-        worktree_id: Option<WorktreeId>,
-        server: Option<Arc<LanguageServer>>,
-        cx: &mut Context<Self>,
-    ) -> Option<&mut LanguageServerState> {
-        let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
-            cx.notify();
-            LanguageServerState {
-                name: None,
-                worktree_id: None,
-                kind,
-                rpc_state: None,
-                log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
-                trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
-                trace_level: TraceValue::Off,
-                log_level: MessageType::LOG,
-                io_logs_subscription: None,
-            }
+    cx.observe_new(move |workspace: &mut Workspace, _, cx| {
+        log_store.update(cx, |store, cx| {
+            store.add_project(workspace.project(), cx);
         });
 
-        if let Some(name) = name {
-            server_state.name = Some(name);
-        }
-        if let Some(worktree_id) = worktree_id {
-            server_state.worktree_id = Some(worktree_id);
-        }
-
-        if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
-            let io_tx = self.io_tx.clone();
-            let server_id = server.server_id();
-            server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
-                io_tx
-                    .unbounded_send((server_id, io_kind, message.to_string()))
-                    .ok();
-            }));
-        }
-
-        Some(server_state)
-    }
-
-    fn add_language_server_log(
-        &mut self,
-        id: LanguageServerId,
-        typ: MessageType,
-        message: &str,
-        cx: &mut Context<Self>,
-    ) -> Option<()> {
-        let language_server_state = self.get_language_server_state(id)?;
-
-        let log_lines = &mut language_server_state.log_messages;
-        Self::add_language_server_message(
-            log_lines,
-            id,
-            LogMessage {
-                message: message.trim_end().to_string(),
-                typ,
-            },
-            language_server_state.log_level,
-            LogKind::Logs,
-            cx,
-        );
-        Some(())
-    }
-
-    fn add_language_server_trace(
-        &mut self,
-        id: LanguageServerId,
-        message: &str,
-        cx: &mut Context<Self>,
-    ) -> Option<()> {
-        let language_server_state = self.get_language_server_state(id)?;
-
-        let log_lines = &mut language_server_state.trace_messages;
-        Self::add_language_server_message(
-            log_lines,
-            id,
-            TraceMessage {
-                message: message.trim().to_string(),
-            },
-            (),
-            LogKind::Trace,
-            cx,
-        );
-        Some(())
-    }
-
-    fn add_language_server_message<T: Message>(
-        log_lines: &mut VecDeque<T>,
-        id: LanguageServerId,
-        message: T,
-        current_severity: <T as Message>::Level,
-        kind: LogKind,
-        cx: &mut Context<Self>,
-    ) {
-        while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
-            log_lines.pop_front();
-        }
-        let text = message.as_ref().to_string();
-        let visible = message.should_include(current_severity);
-        log_lines.push_back(message);
-
-        if visible {
-            cx.emit(Event::NewServerLogEntry { id, kind, text });
-            cx.notify();
-        }
-    }
-
-    fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
-        self.language_servers.remove(&id);
-        cx.notify();
-    }
-
-    pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
-        Some(&self.language_servers.get(&server_id)?.log_messages)
-    }
-
-    pub(super) fn server_trace(
-        &self,
-        server_id: LanguageServerId,
-    ) -> Option<&VecDeque<TraceMessage>> {
-        Some(&self.language_servers.get(&server_id)?.trace_messages)
-    }
-
-    fn server_ids_for_project<'a>(
-        &'a self,
-        lookup_project: &'a WeakEntity<Project>,
-    ) -> impl Iterator<Item = LanguageServerId> + 'a {
-        self.language_servers
-            .iter()
-            .filter_map(move |(id, state)| match &state.kind {
-                LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
-                    if project == lookup_project {
-                        Some(*id)
-                    } else {
-                        None
-                    }
-                }
-                LanguageServerKind::Global => Some(*id),
-            })
-    }
-
-    fn enable_rpc_trace_for_language_server(
-        &mut self,
-        server_id: LanguageServerId,
-    ) -> Option<&mut LanguageServerRpcState> {
-        let rpc_state = self
-            .language_servers
-            .get_mut(&server_id)?
-            .rpc_state
-            .get_or_insert_with(|| LanguageServerRpcState {
-                rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
-                last_message_kind: None,
-            });
-        Some(rpc_state)
-    }
-
-    pub fn disable_rpc_trace_for_language_server(
-        &mut self,
-        server_id: LanguageServerId,
-    ) -> Option<()> {
-        self.language_servers.get_mut(&server_id)?.rpc_state.take();
-        Some(())
-    }
-
-    pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool {
-        match server {
-            LanguageServerSelector::Id(id) => self.language_servers.contains_key(id),
-            LanguageServerSelector::Name(name) => self
-                .language_servers
-                .iter()
-                .any(|(_, state)| state.name.as_ref() == Some(name)),
-        }
-    }
-
-    pub fn 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,
-        io_kind: IoKind,
-        message: &str,
-        cx: &mut Context<Self>,
-    ) -> Option<()> {
-        let is_received = match io_kind {
-            IoKind::StdOut => true,
-            IoKind::StdIn => false,
-            IoKind::StdErr => {
-                self.add_language_server_log(language_server_id, MessageType::LOG, message, cx);
-                return Some(());
-            }
-        };
-
-        let state = self
-            .get_language_server_state(language_server_id)?
-            .rpc_state
-            .as_mut()?;
-        let kind = if is_received {
-            MessageKind::Receive
-        } else {
-            MessageKind::Send
-        };
-
-        let rpc_log_lines = &mut state.rpc_messages;
-        if state.last_message_kind != Some(kind) {
-            while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
-                rpc_log_lines.pop_front();
-            }
-            let line_before_message = match kind {
-                MessageKind::Send => SEND_LINE,
-                MessageKind::Receive => RECEIVE_LINE,
-            };
-            rpc_log_lines.push_back(RpcMessage {
-                message: line_before_message.to_string(),
-            });
-            cx.emit(Event::NewServerLogEntry {
-                id: language_server_id,
-                kind: LogKind::Rpc,
-                text: line_before_message.to_string(),
-            });
-        }
-
-        while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
-            rpc_log_lines.pop_front();
-        }
-
-        let message = message.trim();
-        rpc_log_lines.push_back(RpcMessage {
-            message: message.to_string(),
-        });
-        cx.emit(Event::NewServerLogEntry {
-            id: language_server_id,
-            kind: LogKind::Rpc,
-            text: message.to_string(),
+        let log_store = log_store.clone();
+        workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
+            let log_store = log_store.clone();
+            let project = workspace.project().clone();
+            get_or_create_tool(
+                workspace,
+                SplitDirection::Right,
+                window,
+                cx,
+                move |window, cx| LspLogView::new(project, log_store, window, cx),
+            );
         });
-        cx.notify();
-        Some(())
-    }
+    })
+    .detach();
 }
 
 impl LspLogView {
@@ -751,13 +216,14 @@ impl LspLogView {
 
                 cx.notify();
             });
+
         let events_subscriptions = cx.subscribe_in(
             &log_store,
             window,
             move |log_view, _, e, window, cx| match e {
                 Event::NewServerLogEntry { id, kind, text } => {
                     if log_view.current_server_id == Some(*id)
-                        && *kind == log_view.active_entry_kind
+                        && LogKind::from_server_log_type(kind) == log_view.active_entry_kind
                     {
                         log_view.editor.update(cx, |editor, cx| {
                             editor.set_read_only(false);
@@ -800,7 +266,7 @@ impl LspLogView {
             window.focus(&log_view.editor.focus_handle(cx));
         });
 
-        let mut this = Self {
+        let mut lsp_log_view = Self {
             focus_handle,
             editor,
             editor_subscriptions,
@@ -815,9 +281,9 @@ impl LspLogView {
             ],
         };
         if let Some(server_id) = server_id {
-            this.show_logs_for_server(server_id, window, cx);
+            lsp_log_view.show_logs_for_server(server_id, window, cx);
         }
-        this
+        lsp_log_view
     }
 
     fn editor_for_logs(
@@ -838,7 +304,7 @@ impl LspLogView {
     }
 
     fn editor_for_server_info(
-        server: &LanguageServer,
+        info: ServerInfo,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> (Entity<Editor>, Vec<Subscription>) {
@@ -853,22 +319,21 @@ impl LspLogView {
 * Capabilities: {CAPABILITIES}
 
 * Configuration: {CONFIGURATION}",
-            NAME = server.name(),
-            ID = server.server_id(),
-            BINARY = server.binary(),
-            WORKSPACE_FOLDERS = server
-                .workspace_folders()
-                .into_iter()
-                .filter_map(|path| path
-                    .to_file_path()
-                    .ok()
-                    .map(|path| path.to_string_lossy().into_owned()))
-                .collect::<Vec<_>>()
-                .join(", "),
-            CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
+            NAME = info.name,
+            ID = info.id,
+            BINARY = info.binary.as_ref().map_or_else(
+                || "Unknown".to_string(),
+                |bin| bin.path.as_path().to_string_lossy().to_string()
+            ),
+            WORKSPACE_FOLDERS = info.workspace_folders.join(", "),
+            CAPABILITIES = serde_json::to_string_pretty(&info.capabilities)
                 .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
-            CONFIGURATION = serde_json::to_string_pretty(server.configuration())
-                .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")),
+            CONFIGURATION = info
+                .configuration
+                .map(|configuration| serde_json::to_string_pretty(&configuration))
+                .transpose()
+                .unwrap_or_else(|e| Some(format!("Failed to serialize configuration: {e}")))
+                .unwrap_or_else(|| "Unknown".to_string()),
         );
         let editor = initialize_new_editor(server_info, false, window, cx);
         let editor_subscription = cx.subscribe(
@@ -891,7 +356,9 @@ impl LspLogView {
             .language_servers
             .iter()
             .map(|(server_id, state)| match &state.kind {
-                LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => {
+                LanguageServerKind::Local { .. }
+                | LanguageServerKind::Remote { .. }
+                | LanguageServerKind::LocalSsh { .. } => {
                     let worktree_root_name = state
                         .worktree_id
                         .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
@@ -1003,11 +470,17 @@ impl LspLogView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let trace_level = self
+            .log_store
+            .update(cx, |this, _| {
+                Some(this.get_language_server_state(server_id)?.trace_level)
+            })
+            .unwrap_or(TraceValue::Messages);
         let log_contents = self
             .log_store
             .read(cx)
             .server_trace(server_id)
-            .map(|v| log_contents(v, ()));
+            .map(|v| log_contents(v, trace_level));
         if let Some(log_contents) = log_contents {
             self.current_server_id = Some(server_id);
             self.active_entry_kind = LogKind::Trace;
@@ -1025,6 +498,7 @@ impl LspLogView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        self.toggle_rpc_trace_for_server(server_id, true, window, cx);
         let rpc_log = self.log_store.update(cx, |log_store, _| {
             log_store
                 .enable_rpc_trace_for_language_server(server_id)
@@ -1069,12 +543,33 @@ impl LspLogView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.log_store.update(cx, |log_store, _| {
+        self.log_store.update(cx, |log_store, cx| {
             if enabled {
                 log_store.enable_rpc_trace_for_language_server(server_id);
             } else {
                 log_store.disable_rpc_trace_for_language_server(server_id);
             }
+
+            if let Some(server_state) = log_store.language_servers.get(&server_id) {
+                if let LanguageServerKind::Remote { project } = &server_state.kind {
+                    project
+                        .update(cx, |project, cx| {
+                            if let Some((client, project_id)) =
+                                project.lsp_store().read(cx).upstream_client()
+                            {
+                                client
+                                    .send(proto::ToggleLspLogs {
+                                        project_id,
+                                        log_type: proto::toggle_lsp_logs::LogType::Rpc as i32,
+                                        server_id: server_id.to_proto(),
+                                        enabled,
+                                    })
+                                    .log_err();
+                            }
+                        })
+                        .ok();
+                }
+            };
         });
         if !enabled && Some(server_id) == self.current_server_id {
             self.show_logs_for_server(server_id, window, cx);
@@ -1113,13 +608,38 @@ impl LspLogView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let lsp_store = self.project.read(cx).lsp_store();
-        let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else {
+        let Some(server_info) = self
+            .project
+            .read(cx)
+            .lsp_store()
+            .update(cx, |lsp_store, _| {
+                lsp_store
+                    .language_server_for_id(server_id)
+                    .as_ref()
+                    .map(|language_server| ServerInfo::new(language_server))
+                    .or_else(move || {
+                        let capabilities =
+                            lsp_store.lsp_server_capabilities.get(&server_id)?.clone();
+                        let name = lsp_store
+                            .language_server_statuses
+                            .get(&server_id)
+                            .map(|status| status.name.clone())?;
+                        Some(ServerInfo {
+                            id: server_id,
+                            capabilities,
+                            binary: None,
+                            name,
+                            workspace_folders: Vec::new(),
+                            configuration: None,
+                        })
+                    })
+            })
+        else {
             return;
         };
         self.current_server_id = Some(server_id);
         self.active_entry_kind = LogKind::ServerInfo;
-        let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, window, cx);
+        let (editor, editor_subscriptions) = Self::editor_for_server_info(server_info, window, cx);
         self.editor = editor;
         self.editor_subscriptions = editor_subscriptions;
         cx.notify();
@@ -1416,7 +936,6 @@ impl Render for LspLogToolbarItemView {
 
         let view_selector = current_server.map(|server| {
             let server_id = server.server_id;
-            let is_remote = server.server_kind.is_remote();
             let rpc_trace_enabled = server.rpc_trace_enabled;
             let log_view = log_view.clone();
             PopoverMenu::new("LspViewSelector")
@@ -1438,55 +957,53 @@ impl Render for LspLogToolbarItemView {
                                 view.show_logs_for_server(server_id, window, cx);
                             }),
                         )
-                        .when(!is_remote, |this| {
-                            this.entry(
-                                SERVER_TRACE,
-                                None,
-                                window.handler_for(&log_view, move |view, window, cx| {
-                                    view.show_trace_for_server(server_id, window, cx);
-                                }),
-                            )
-                            .custom_entry(
-                                {
-                                    let log_toolbar_view = log_toolbar_view.clone();
-                                    move |window, _| {
-                                        h_flex()
-                                            .w_full()
-                                            .justify_between()
-                                            .child(Label::new(RPC_MESSAGES))
-                                            .child(
-                                                div().child(
-                                                    Checkbox::new(
-                                                        "LspLogEnableRpcTrace",
-                                                        if rpc_trace_enabled {
+                        .entry(
+                            SERVER_TRACE,
+                            None,
+                            window.handler_for(&log_view, move |view, window, cx| {
+                                view.show_trace_for_server(server_id, window, cx);
+                            }),
+                        )
+                        .custom_entry(
+                            {
+                                let log_toolbar_view = log_toolbar_view.clone();
+                                move |window, _| {
+                                    h_flex()
+                                        .w_full()
+                                        .justify_between()
+                                        .child(Label::new(RPC_MESSAGES))
+                                        .child(
+                                            div().child(
+                                                Checkbox::new(
+                                                    "LspLogEnableRpcTrace",
+                                                    if rpc_trace_enabled {
+                                                        ToggleState::Selected
+                                                    } else {
+                                                        ToggleState::Unselected
+                                                    },
+                                                )
+                                                .on_click(window.listener_for(
+                                                    &log_toolbar_view,
+                                                    move |view, selection, window, cx| {
+                                                        let enabled = matches!(
+                                                            selection,
                                                             ToggleState::Selected
-                                                        } else {
-                                                            ToggleState::Unselected
-                                                        },
-                                                    )
-                                                    .on_click(window.listener_for(
-                                                        &log_toolbar_view,
-                                                        move |view, selection, window, cx| {
-                                                            let enabled = matches!(
-                                                                selection,
-                                                                ToggleState::Selected
-                                                            );
-                                                            view.toggle_rpc_logging_for_server(
-                                                                server_id, enabled, window, cx,
-                                                            );
-                                                            cx.stop_propagation();
-                                                        },
-                                                    )),
-                                                ),
-                                            )
-                                            .into_any_element()
-                                    }
-                                },
-                                window.handler_for(&log_view, move |view, window, cx| {
-                                    view.show_rpc_trace_for_server(server_id, window, cx);
-                                }),
-                            )
-                        })
+                                                        );
+                                                        view.toggle_rpc_logging_for_server(
+                                                            server_id, enabled, window, cx,
+                                                        );
+                                                        cx.stop_propagation();
+                                                    },
+                                                )),
+                                            ),
+                                        )
+                                        .into_any_element()
+                                }
+                            },
+                            window.handler_for(&log_view, move |view, window, cx| {
+                                view.show_rpc_trace_for_server(server_id, window, cx);
+                            }),
+                        )
                         .entry(
                             SERVER_INFO,
                             None,

crates/language_tools/src/lsp_log_tests.rs → crates/language_tools/src/lsp_log_view_tests.rs 🔗

@@ -1,20 +1,22 @@
 use std::sync::Arc;
 
-use crate::lsp_log::LogMenuItem;
+use crate::lsp_log_view::LogMenuItem;
 
 use super::*;
 use futures::StreamExt;
 use gpui::{AppContext as _, SemanticVersion, TestAppContext, VisualTestContext};
 use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
 use lsp::LanguageServerName;
-use lsp_log::LogKind;
-use project::{FakeFs, Project};
+use project::{
+    FakeFs, Project,
+    lsp_store::log_store::{LanguageServerKind, LogKind, LogStore},
+};
 use serde_json::json;
 use settings::SettingsStore;
 use util::path;
 
 #[gpui::test]
-async fn test_lsp_logs(cx: &mut TestAppContext) {
+async fn test_lsp_log_view(cx: &mut TestAppContext) {
     zlog::init_test();
 
     init_test(cx);
@@ -51,7 +53,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
         },
     );
 
-    let log_store = cx.new(LogStore::new);
+    let log_store = cx.new(|cx| LogStore::new(true, cx));
     log_store.update(cx, |store, cx| store.add_project(&project, cx));
 
     let _rust_buffer = project
@@ -94,7 +96,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
                 rpc_trace_enabled: false,
                 selected_entry: LogKind::Logs,
                 trace_level: lsp::TraceValue::Off,
-                server_kind: lsp_log::LanguageServerKind::Local {
+                server_kind: LanguageServerKind::Local {
                     project: project.downgrade()
                 }
             }]

crates/project/src/lsp_store.rs 🔗

@@ -11,18 +11,22 @@
 //! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate.
 pub mod clangd_ext;
 pub mod json_language_server_ext;
+pub mod log_store;
 pub mod lsp_ext_command;
 pub mod rust_analyzer_ext;
 
 use crate::{
     CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource,
     CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, LspPullDiagnostics,
-    ManifestProvidersStore, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics,
-    ResolveState, Symbol,
+    ManifestProvidersStore, Project, ProjectItem, ProjectPath, ProjectTransaction,
+    PulledDiagnostics, ResolveState, Symbol,
     buffer_store::{BufferStore, BufferStoreEvent},
     environment::ProjectEnvironment,
     lsp_command::{self, *},
-    lsp_store,
+    lsp_store::{
+        self,
+        log_store::{GlobalLogStore, LanguageServerKind},
+    },
     manifest_tree::{
         LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestQueryDelegate,
         ManifestTree,
@@ -977,7 +981,9 @@ impl LocalLspStore {
                         this.update(&mut cx, |_, cx| {
                             cx.emit(LspStoreEvent::LanguageServerLog(
                                 server_id,
-                                LanguageServerLogType::Trace(params.verbose),
+                                LanguageServerLogType::Trace {
+                                    verbose_info: params.verbose,
+                                },
                                 params.message,
                             ));
                         })
@@ -3482,13 +3488,13 @@ pub struct LspStore {
     buffer_store: Entity<BufferStore>,
     worktree_store: Entity<WorktreeStore>,
     pub languages: Arc<LanguageRegistry>,
-    language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
+    pub language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
     active_entry: Option<ProjectEntryId>,
     _maintain_workspace_config: (Task<Result<()>>, watch::Sender<()>),
     _maintain_buffer_languages: Task<()>,
     diagnostic_summaries:
         HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
-    pub(super) lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
+    pub lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
     lsp_document_colors: HashMap<BufferId, DocumentColorData>,
     lsp_code_lens: HashMap<BufferId, CodeLensData>,
     running_lsp_requests: HashMap<TypeId, (Global, HashMap<LspRequestId, Task<()>>)>,
@@ -3565,6 +3571,7 @@ pub struct LanguageServerStatus {
     pub pending_work: BTreeMap<String, LanguageServerProgress>,
     pub has_pending_diagnostic_updates: bool,
     progress_tokens: HashSet<String>,
+    pub worktree: Option<WorktreeId>,
 }
 
 #[derive(Clone, Debug)]
@@ -7483,7 +7490,7 @@ impl LspStore {
                         server: Some(proto::LanguageServer {
                             id: server_id.to_proto(),
                             name: status.name.to_string(),
-                            worktree_id: None,
+                            worktree_id: status.worktree.map(|id| id.to_proto()),
                         }),
                         capabilities: serde_json::to_string(&server.capabilities())
                             .expect("serializing server LSP capabilities"),
@@ -7508,9 +7515,15 @@ impl LspStore {
 
     pub(crate) fn set_language_server_statuses_from_proto(
         &mut self,
+        project: WeakEntity<Project>,
         language_servers: Vec<proto::LanguageServer>,
         server_capabilities: Vec<String>,
+        cx: &mut Context<Self>,
     ) {
+        let lsp_logs = cx
+            .try_global::<GlobalLogStore>()
+            .map(|lsp_store| lsp_store.0.clone());
+
         self.language_server_statuses = language_servers
             .into_iter()
             .zip(server_capabilities)
@@ -7520,13 +7533,34 @@ impl LspStore {
                     self.lsp_server_capabilities
                         .insert(server_id, server_capabilities);
                 }
+
+                let name = LanguageServerName::from_proto(server.name);
+                let worktree = server.worktree_id.map(WorktreeId::from_proto);
+
+                if let Some(lsp_logs) = &lsp_logs {
+                    lsp_logs.update(cx, |lsp_logs, cx| {
+                        lsp_logs.add_language_server(
+                            // Only remote clients get their language servers set from proto
+                            LanguageServerKind::Remote {
+                                project: project.clone(),
+                            },
+                            server_id,
+                            Some(name.clone()),
+                            worktree,
+                            None,
+                            cx,
+                        );
+                    });
+                }
+
                 (
                     server_id,
                     LanguageServerStatus {
-                        name: LanguageServerName::from_proto(server.name),
+                        name,
                         pending_work: Default::default(),
                         has_pending_diagnostic_updates: false,
                         progress_tokens: Default::default(),
+                        worktree,
                     },
                 )
             })
@@ -8892,6 +8926,7 @@ impl LspStore {
                     pending_work: Default::default(),
                     has_pending_diagnostic_updates: false,
                     progress_tokens: Default::default(),
+                    worktree: server.worktree_id.map(WorktreeId::from_proto),
                 },
             );
             cx.emit(LspStoreEvent::LanguageServerAdded(
@@ -10905,6 +10940,7 @@ impl LspStore {
                 pending_work: Default::default(),
                 has_pending_diagnostic_updates: false,
                 progress_tokens: Default::default(),
+                worktree: Some(key.worktree_id),
             },
         );
 
@@ -12190,6 +12226,14 @@ impl LspStore {
         let data = self.lsp_code_lens.get_mut(&buffer_id)?;
         Some(data.update.take()?.1)
     }
+
+    pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> {
+        self.downstream_client.clone()
+    }
+
+    pub fn worktree_store(&self) -> Entity<WorktreeStore> {
+        self.worktree_store.clone()
+    }
 }
 
 // Registration with registerOptions as null, should fallback to true.
@@ -12699,45 +12743,69 @@ impl PartialEq for LanguageServerPromptRequest {
 #[derive(Clone, Debug, PartialEq)]
 pub enum LanguageServerLogType {
     Log(MessageType),
-    Trace(Option<String>),
+    Trace { verbose_info: Option<String> },
+    Rpc { received: bool },
 }
 
 impl LanguageServerLogType {
     pub fn to_proto(&self) -> proto::language_server_log::LogType {
         match self {
             Self::Log(log_type) => {
-                let message_type = match *log_type {
-                    MessageType::ERROR => 1,
-                    MessageType::WARNING => 2,
-                    MessageType::INFO => 3,
-                    MessageType::LOG => 4,
+                use proto::log_message::LogLevel;
+                let level = match *log_type {
+                    MessageType::ERROR => LogLevel::Error,
+                    MessageType::WARNING => LogLevel::Warning,
+                    MessageType::INFO => LogLevel::Info,
+                    MessageType::LOG => LogLevel::Log,
                     other => {
-                        log::warn!("Unknown lsp log message type: {:?}", other);
-                        4
+                        log::warn!("Unknown lsp log message type: {other:?}");
+                        LogLevel::Log
                     }
                 };
-                proto::language_server_log::LogType::LogMessageType(message_type)
+                proto::language_server_log::LogType::Log(proto::LogMessage {
+                    level: level as i32,
+                })
             }
-            Self::Trace(message) => {
-                proto::language_server_log::LogType::LogTrace(proto::LspLogTrace {
-                    message: message.clone(),
+            Self::Trace { verbose_info } => {
+                proto::language_server_log::LogType::Trace(proto::TraceMessage {
+                    verbose_info: verbose_info.to_owned(),
                 })
             }
+            Self::Rpc { received } => {
+                let kind = if *received {
+                    proto::rpc_message::Kind::Received
+                } else {
+                    proto::rpc_message::Kind::Sent
+                };
+                let kind = kind as i32;
+                proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind })
+            }
         }
     }
 
     pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self {
+        use proto::log_message::LogLevel;
+        use proto::rpc_message;
         match log_type {
-            proto::language_server_log::LogType::LogMessageType(message_type) => {
-                Self::Log(match message_type {
-                    1 => MessageType::ERROR,
-                    2 => MessageType::WARNING,
-                    3 => MessageType::INFO,
-                    4 => MessageType::LOG,
-                    _ => MessageType::LOG,
-                })
-            }
-            proto::language_server_log::LogType::LogTrace(trace) => Self::Trace(trace.message),
+            proto::language_server_log::LogType::Log(message_type) => Self::Log(
+                match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) {
+                    LogLevel::Error => MessageType::ERROR,
+                    LogLevel::Warning => MessageType::WARNING,
+                    LogLevel::Info => MessageType::INFO,
+                    LogLevel::Log => MessageType::LOG,
+                },
+            ),
+            proto::language_server_log::LogType::Trace(trace_message) => Self::Trace {
+                verbose_info: trace_message.verbose_info,
+            },
+            proto::language_server_log::LogType::Rpc(message) => Self::Rpc {
+                received: match rpc_message::Kind::from_i32(message.kind)
+                    .unwrap_or(rpc_message::Kind::Received)
+                {
+                    rpc_message::Kind::Received => true,
+                    rpc_message::Kind::Sent => false,
+                },
+            },
         }
     }
 }

crates/project/src/lsp_store/log_store.rs 🔗

@@ -0,0 +1,704 @@
+use std::{collections::VecDeque, sync::Arc};
+
+use collections::HashMap;
+use futures::{StreamExt, channel::mpsc};
+use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, Subscription, WeakEntity};
+use lsp::{
+    IoKind, LanguageServer, LanguageServerId, LanguageServerName, LanguageServerSelector,
+    MessageType, TraceValue,
+};
+use rpc::proto;
+use settings::WorktreeId;
+
+use crate::{LanguageServerLogType, LspStore, Project, ProjectItem as _};
+
+const SEND_LINE: &str = "\n// Send:";
+const RECEIVE_LINE: &str = "\n// Receive:";
+const MAX_STORED_LOG_ENTRIES: usize = 2000;
+
+const RPC_MESSAGES: &str = "RPC Messages";
+const SERVER_LOGS: &str = "Server Logs";
+const SERVER_TRACE: &str = "Server Trace";
+const SERVER_INFO: &str = "Server Info";
+
+pub fn init(store_logs: bool, cx: &mut App) -> Entity<LogStore> {
+    let log_store = cx.new(|cx| LogStore::new(store_logs, cx));
+    cx.set_global(GlobalLogStore(log_store.clone()));
+    log_store
+}
+
+pub struct GlobalLogStore(pub Entity<LogStore>);
+
+impl Global for GlobalLogStore {}
+
+#[derive(Debug)]
+pub enum Event {
+    NewServerLogEntry {
+        id: LanguageServerId,
+        kind: LanguageServerLogType,
+        text: String,
+    },
+}
+
+impl EventEmitter<Event> for LogStore {}
+
+pub struct LogStore {
+    store_logs: bool,
+    projects: HashMap<WeakEntity<Project>, ProjectState>,
+    pub copilot_log_subscription: Option<lsp::Subscription>,
+    pub language_servers: HashMap<LanguageServerId, LanguageServerState>,
+    io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
+}
+
+struct ProjectState {
+    _subscriptions: [Subscription; 2],
+}
+
+pub trait Message: AsRef<str> {
+    type Level: Copy + std::fmt::Debug;
+    fn should_include(&self, _: Self::Level) -> bool {
+        true
+    }
+}
+
+#[derive(Debug)]
+pub struct LogMessage {
+    message: String,
+    typ: MessageType,
+}
+
+impl AsRef<str> for LogMessage {
+    fn as_ref(&self) -> &str {
+        &self.message
+    }
+}
+
+impl Message for LogMessage {
+    type Level = MessageType;
+
+    fn should_include(&self, level: Self::Level) -> bool {
+        match (self.typ, level) {
+            (MessageType::ERROR, _) => true,
+            (_, MessageType::ERROR) => false,
+            (MessageType::WARNING, _) => true,
+            (_, MessageType::WARNING) => false,
+            (MessageType::INFO, _) => true,
+            (_, MessageType::INFO) => false,
+            _ => true,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct TraceMessage {
+    message: String,
+    is_verbose: bool,
+}
+
+impl AsRef<str> for TraceMessage {
+    fn as_ref(&self) -> &str {
+        &self.message
+    }
+}
+
+impl Message for TraceMessage {
+    type Level = TraceValue;
+
+    fn should_include(&self, level: Self::Level) -> bool {
+        match level {
+            TraceValue::Off => false,
+            TraceValue::Messages => !self.is_verbose,
+            TraceValue::Verbose => true,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct RpcMessage {
+    message: String,
+}
+
+impl AsRef<str> for RpcMessage {
+    fn as_ref(&self) -> &str {
+        &self.message
+    }
+}
+
+impl Message for RpcMessage {
+    type Level = ();
+}
+
+pub struct LanguageServerState {
+    pub name: Option<LanguageServerName>,
+    pub worktree_id: Option<WorktreeId>,
+    pub kind: LanguageServerKind,
+    log_messages: VecDeque<LogMessage>,
+    trace_messages: VecDeque<TraceMessage>,
+    pub rpc_state: Option<LanguageServerRpcState>,
+    pub trace_level: TraceValue,
+    pub log_level: MessageType,
+    io_logs_subscription: Option<lsp::Subscription>,
+}
+
+impl std::fmt::Debug for LanguageServerState {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("LanguageServerState")
+            .field("name", &self.name)
+            .field("worktree_id", &self.worktree_id)
+            .field("kind", &self.kind)
+            .field("log_messages", &self.log_messages)
+            .field("trace_messages", &self.trace_messages)
+            .field("rpc_state", &self.rpc_state)
+            .field("trace_level", &self.trace_level)
+            .field("log_level", &self.log_level)
+            .finish_non_exhaustive()
+    }
+}
+
+#[derive(PartialEq, Clone)]
+pub enum LanguageServerKind {
+    Local { project: WeakEntity<Project> },
+    Remote { project: WeakEntity<Project> },
+    LocalSsh { lsp_store: WeakEntity<LspStore> },
+    Global,
+}
+
+impl std::fmt::Debug for LanguageServerKind {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"),
+            LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"),
+            LanguageServerKind::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"),
+            LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
+        }
+    }
+}
+
+impl LanguageServerKind {
+    pub fn project(&self) -> Option<&WeakEntity<Project>> {
+        match self {
+            Self::Local { project } => Some(project),
+            Self::Remote { project } => Some(project),
+            Self::LocalSsh { .. } => None,
+            Self::Global { .. } => None,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct LanguageServerRpcState {
+    pub rpc_messages: VecDeque<RpcMessage>,
+    last_message_kind: Option<MessageKind>,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+enum MessageKind {
+    Send,
+    Receive,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub enum LogKind {
+    Rpc,
+    Trace,
+    #[default]
+    Logs,
+    ServerInfo,
+}
+
+impl LogKind {
+    pub fn from_server_log_type(log_type: &LanguageServerLogType) -> Self {
+        match log_type {
+            LanguageServerLogType::Log(_) => Self::Logs,
+            LanguageServerLogType::Trace { .. } => Self::Trace,
+            LanguageServerLogType::Rpc { .. } => Self::Rpc,
+        }
+    }
+
+    pub fn label(&self) -> &'static str {
+        match self {
+            LogKind::Rpc => RPC_MESSAGES,
+            LogKind::Trace => SERVER_TRACE,
+            LogKind::Logs => SERVER_LOGS,
+            LogKind::ServerInfo => SERVER_INFO,
+        }
+    }
+}
+
+impl LogStore {
+    pub fn new(store_logs: bool, cx: &mut Context<Self>) -> Self {
+        let (io_tx, mut io_rx) = mpsc::unbounded();
+
+        let log_store = Self {
+            projects: HashMap::default(),
+            language_servers: HashMap::default(),
+            copilot_log_subscription: None,
+            store_logs,
+            io_tx,
+        };
+        cx.spawn(async move |log_store, cx| {
+            while let Some((server_id, io_kind, message)) = io_rx.next().await {
+                if let Some(log_store) = log_store.upgrade() {
+                    log_store.update(cx, |log_store, cx| {
+                        log_store.on_io(server_id, io_kind, &message, cx);
+                    })?;
+                }
+            }
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+
+        log_store
+    }
+
+    pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
+        let weak_project = project.downgrade();
+        self.projects.insert(
+            project.downgrade(),
+            ProjectState {
+                _subscriptions: [
+                    cx.observe_release(project, move |this, _, _| {
+                        this.projects.remove(&weak_project);
+                        this.language_servers
+                            .retain(|_, state| state.kind.project() != Some(&weak_project));
+                    }),
+                    cx.subscribe(project, move |log_store, project, event, cx| {
+                        let server_kind = if project.read(cx).is_local() {
+                            LanguageServerKind::Local {
+                                project: project.downgrade(),
+                            }
+                        } else {
+                            LanguageServerKind::Remote {
+                                project: project.downgrade(),
+                            }
+                        };
+                        match event {
+                            crate::Event::LanguageServerAdded(id, name, worktree_id) => {
+                                log_store.add_language_server(
+                                    server_kind,
+                                    *id,
+                                    Some(name.clone()),
+                                    *worktree_id,
+                                    project
+                                        .read(cx)
+                                        .lsp_store()
+                                        .read(cx)
+                                        .language_server_for_id(*id),
+                                    cx,
+                                );
+                            }
+                            crate::Event::LanguageServerBufferRegistered {
+                                server_id,
+                                buffer_id,
+                                name,
+                                ..
+                            } => {
+                                let worktree_id = project
+                                    .read(cx)
+                                    .buffer_for_id(*buffer_id, cx)
+                                    .and_then(|buffer| {
+                                        Some(buffer.read(cx).project_path(cx)?.worktree_id)
+                                    });
+                                let name = name.clone().or_else(|| {
+                                    project
+                                        .read(cx)
+                                        .lsp_store()
+                                        .read(cx)
+                                        .language_server_statuses
+                                        .get(server_id)
+                                        .map(|status| status.name.clone())
+                                });
+                                log_store.add_language_server(
+                                    server_kind,
+                                    *server_id,
+                                    name,
+                                    worktree_id,
+                                    None,
+                                    cx,
+                                );
+                            }
+                            crate::Event::LanguageServerRemoved(id) => {
+                                log_store.remove_language_server(*id, cx);
+                            }
+                            crate::Event::LanguageServerLog(id, typ, message) => {
+                                log_store.add_language_server(
+                                    server_kind,
+                                    *id,
+                                    None,
+                                    None,
+                                    None,
+                                    cx,
+                                );
+                                match typ {
+                                    crate::LanguageServerLogType::Log(typ) => {
+                                        log_store.add_language_server_log(*id, *typ, message, cx);
+                                    }
+                                    crate::LanguageServerLogType::Trace { verbose_info } => {
+                                        log_store.add_language_server_trace(
+                                            *id,
+                                            message,
+                                            verbose_info.clone(),
+                                            cx,
+                                        );
+                                    }
+                                    crate::LanguageServerLogType::Rpc { received } => {
+                                        let kind = if *received {
+                                            MessageKind::Receive
+                                        } else {
+                                            MessageKind::Send
+                                        };
+                                        log_store.add_language_server_rpc(*id, kind, message, cx);
+                                    }
+                                }
+                            }
+                            crate::Event::ToggleLspLogs { server_id, enabled } => {
+                                // we do not support any other log toggling yet
+                                if *enabled {
+                                    log_store.enable_rpc_trace_for_language_server(*server_id);
+                                } else {
+                                    log_store.disable_rpc_trace_for_language_server(*server_id);
+                                }
+                            }
+                            _ => {}
+                        }
+                    }),
+                ],
+            },
+        );
+    }
+
+    pub fn get_language_server_state(
+        &mut self,
+        id: LanguageServerId,
+    ) -> Option<&mut LanguageServerState> {
+        self.language_servers.get_mut(&id)
+    }
+
+    pub fn add_language_server(
+        &mut self,
+        kind: LanguageServerKind,
+        server_id: LanguageServerId,
+        name: Option<LanguageServerName>,
+        worktree_id: Option<WorktreeId>,
+        server: Option<Arc<LanguageServer>>,
+        cx: &mut Context<Self>,
+    ) -> Option<&mut LanguageServerState> {
+        let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
+            cx.notify();
+            LanguageServerState {
+                name: None,
+                worktree_id: None,
+                kind,
+                rpc_state: None,
+                log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
+                trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
+                trace_level: TraceValue::Off,
+                log_level: MessageType::LOG,
+                io_logs_subscription: None,
+            }
+        });
+
+        if let Some(name) = name {
+            server_state.name = Some(name);
+        }
+        if let Some(worktree_id) = worktree_id {
+            server_state.worktree_id = Some(worktree_id);
+        }
+
+        if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
+            let io_tx = self.io_tx.clone();
+            let server_id = server.server_id();
+            server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
+                io_tx
+                    .unbounded_send((server_id, io_kind, message.to_string()))
+                    .ok();
+            }));
+        }
+
+        Some(server_state)
+    }
+
+    pub fn add_language_server_log(
+        &mut self,
+        id: LanguageServerId,
+        typ: MessageType,
+        message: &str,
+        cx: &mut Context<Self>,
+    ) -> Option<()> {
+        let store_logs = self.store_logs;
+        let language_server_state = self.get_language_server_state(id)?;
+
+        let log_lines = &mut language_server_state.log_messages;
+        let message = message.trim_end().to_string();
+        if !store_logs {
+            // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway
+            self.emit_event(
+                Event::NewServerLogEntry {
+                    id,
+                    kind: LanguageServerLogType::Log(typ),
+                    text: message,
+                },
+                cx,
+            );
+        } else if let Some(new_message) = Self::push_new_message(
+            log_lines,
+            LogMessage { message, typ },
+            language_server_state.log_level,
+        ) {
+            self.emit_event(
+                Event::NewServerLogEntry {
+                    id,
+                    kind: LanguageServerLogType::Log(typ),
+                    text: new_message,
+                },
+                cx,
+            );
+        }
+        Some(())
+    }
+
+    fn add_language_server_trace(
+        &mut self,
+        id: LanguageServerId,
+        message: &str,
+        verbose_info: Option<String>,
+        cx: &mut Context<Self>,
+    ) -> Option<()> {
+        let store_logs = self.store_logs;
+        let language_server_state = self.get_language_server_state(id)?;
+
+        let log_lines = &mut language_server_state.trace_messages;
+        if !store_logs {
+            // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway
+            self.emit_event(
+                Event::NewServerLogEntry {
+                    id,
+                    kind: LanguageServerLogType::Trace { verbose_info },
+                    text: message.trim().to_string(),
+                },
+                cx,
+            );
+        } else if let Some(new_message) = Self::push_new_message(
+            log_lines,
+            TraceMessage {
+                message: message.trim().to_string(),
+                is_verbose: false,
+            },
+            TraceValue::Messages,
+        ) {
+            if let Some(verbose_message) = verbose_info.as_ref() {
+                Self::push_new_message(
+                    log_lines,
+                    TraceMessage {
+                        message: verbose_message.clone(),
+                        is_verbose: true,
+                    },
+                    TraceValue::Verbose,
+                );
+            }
+            self.emit_event(
+                Event::NewServerLogEntry {
+                    id,
+                    kind: LanguageServerLogType::Trace { verbose_info },
+                    text: new_message,
+                },
+                cx,
+            );
+        }
+        Some(())
+    }
+
+    fn push_new_message<T: Message>(
+        log_lines: &mut VecDeque<T>,
+        message: T,
+        current_severity: <T as Message>::Level,
+    ) -> Option<String> {
+        while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
+            log_lines.pop_front();
+        }
+        let visible = message.should_include(current_severity);
+
+        let visible_message = visible.then(|| message.as_ref().to_string());
+        log_lines.push_back(message);
+        visible_message
+    }
+
+    fn add_language_server_rpc(
+        &mut self,
+        language_server_id: LanguageServerId,
+        kind: MessageKind,
+        message: &str,
+        cx: &mut Context<'_, Self>,
+    ) {
+        let store_logs = self.store_logs;
+        let Some(state) = self
+            .get_language_server_state(language_server_id)
+            .and_then(|state| state.rpc_state.as_mut())
+        else {
+            return;
+        };
+
+        let received = kind == MessageKind::Receive;
+        let rpc_log_lines = &mut state.rpc_messages;
+        if state.last_message_kind != Some(kind) {
+            while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
+                rpc_log_lines.pop_front();
+            }
+            let line_before_message = match kind {
+                MessageKind::Send => SEND_LINE,
+                MessageKind::Receive => RECEIVE_LINE,
+            };
+            if store_logs {
+                rpc_log_lines.push_back(RpcMessage {
+                    message: line_before_message.to_string(),
+                });
+            }
+            // Do not send a synthetic message over the wire, it will be derived from the actual RPC message
+            cx.emit(Event::NewServerLogEntry {
+                id: language_server_id,
+                kind: LanguageServerLogType::Rpc { received },
+                text: line_before_message.to_string(),
+            });
+        }
+
+        while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
+            rpc_log_lines.pop_front();
+        }
+
+        if store_logs {
+            rpc_log_lines.push_back(RpcMessage {
+                message: message.trim().to_owned(),
+            });
+        }
+
+        self.emit_event(
+            Event::NewServerLogEntry {
+                id: language_server_id,
+                kind: LanguageServerLogType::Rpc { received },
+                text: message.to_owned(),
+            },
+            cx,
+        );
+    }
+
+    pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
+        self.language_servers.remove(&id);
+        cx.notify();
+    }
+
+    pub fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
+        Some(&self.language_servers.get(&server_id)?.log_messages)
+    }
+
+    pub fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> {
+        Some(&self.language_servers.get(&server_id)?.trace_messages)
+    }
+
+    pub fn server_ids_for_project<'a>(
+        &'a self,
+        lookup_project: &'a WeakEntity<Project>,
+    ) -> impl Iterator<Item = LanguageServerId> + 'a {
+        self.language_servers
+            .iter()
+            .filter_map(move |(id, state)| match &state.kind {
+                LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
+                    if project == lookup_project {
+                        Some(*id)
+                    } else {
+                        None
+                    }
+                }
+                LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id),
+            })
+    }
+
+    pub fn enable_rpc_trace_for_language_server(
+        &mut self,
+        server_id: LanguageServerId,
+    ) -> Option<&mut LanguageServerRpcState> {
+        let rpc_state = self
+            .language_servers
+            .get_mut(&server_id)?
+            .rpc_state
+            .get_or_insert_with(|| LanguageServerRpcState {
+                rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
+                last_message_kind: None,
+            });
+        Some(rpc_state)
+    }
+
+    pub fn disable_rpc_trace_for_language_server(
+        &mut self,
+        server_id: LanguageServerId,
+    ) -> Option<()> {
+        self.language_servers.get_mut(&server_id)?.rpc_state.take();
+        Some(())
+    }
+
+    pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool {
+        match server {
+            LanguageServerSelector::Id(id) => self.language_servers.contains_key(id),
+            LanguageServerSelector::Name(name) => self
+                .language_servers
+                .iter()
+                .any(|(_, state)| state.name.as_ref() == Some(name)),
+        }
+    }
+
+    fn on_io(
+        &mut self,
+        language_server_id: LanguageServerId,
+        io_kind: IoKind,
+        message: &str,
+        cx: &mut Context<Self>,
+    ) -> Option<()> {
+        let is_received = match io_kind {
+            IoKind::StdOut => true,
+            IoKind::StdIn => false,
+            IoKind::StdErr => {
+                self.add_language_server_log(language_server_id, MessageType::LOG, message, cx);
+                return Some(());
+            }
+        };
+
+        let kind = if is_received {
+            MessageKind::Receive
+        } else {
+            MessageKind::Send
+        };
+
+        self.add_language_server_rpc(language_server_id, kind, message, cx);
+        cx.notify();
+        Some(())
+    }
+
+    fn emit_event(&mut self, e: Event, cx: &mut Context<Self>) {
+        match &e {
+            Event::NewServerLogEntry { id, kind, text } => {
+                if let Some(state) = self.get_language_server_state(*id) {
+                    let downstream_client = match &state.kind {
+                        LanguageServerKind::Remote { project }
+                        | LanguageServerKind::Local { project } => project
+                            .upgrade()
+                            .map(|project| project.read(cx).lsp_store()),
+                        LanguageServerKind::LocalSsh { lsp_store } => lsp_store.upgrade(),
+                        LanguageServerKind::Global => None,
+                    }
+                    .and_then(|lsp_store| lsp_store.read(cx).downstream_client());
+                    if let Some((client, project_id)) = downstream_client {
+                        client
+                            .send(proto::LanguageServerLog {
+                                project_id,
+                                language_server_id: id.to_proto(),
+                                message: text.clone(),
+                                log_type: Some(kind.to_proto()),
+                            })
+                            .ok();
+                    }
+                }
+            }
+        }
+
+        cx.emit(e);
+    }
+}

crates/project/src/project.rs 🔗

@@ -280,6 +280,11 @@ pub enum Event {
         server_id: LanguageServerId,
         buffer_id: BufferId,
         buffer_abs_path: PathBuf,
+        name: Option<LanguageServerName>,
+    },
+    ToggleLspLogs {
+        server_id: LanguageServerId,
+        enabled: bool,
     },
     Toast {
         notification_id: SharedString,
@@ -1001,6 +1006,7 @@ impl Project {
         client.add_entity_request_handler(Self::handle_open_buffer_by_path);
         client.add_entity_request_handler(Self::handle_open_new_buffer);
         client.add_entity_message_handler(Self::handle_create_buffer_for_peer);
+        client.add_entity_message_handler(Self::handle_toggle_lsp_logs);
 
         WorktreeStore::init(&client);
         BufferStore::init(&client);
@@ -1475,7 +1481,7 @@ impl Project {
         })?;
 
         let lsp_store = cx.new(|cx| {
-            let mut lsp_store = LspStore::new_remote(
+            LspStore::new_remote(
                 buffer_store.clone(),
                 worktree_store.clone(),
                 languages.clone(),
@@ -1483,12 +1489,7 @@ impl Project {
                 remote_id,
                 fs.clone(),
                 cx,
-            );
-            lsp_store.set_language_server_statuses_from_proto(
-                response.payload.language_servers,
-                response.payload.language_server_capabilities,
-            );
-            lsp_store
+            )
         })?;
 
         let task_store = cx.new(|cx| {
@@ -1522,7 +1523,7 @@ impl Project {
             )
         })?;
 
-        let this = cx.new(|cx| {
+        let project = cx.new(|cx| {
             let replica_id = response.payload.replica_id as ReplicaId;
 
             let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
@@ -1553,7 +1554,7 @@ impl Project {
 
             cx.subscribe(&dap_store, Self::on_dap_store_event).detach();
 
-            let mut this = Self {
+            let mut project = Self {
                 buffer_ordered_messages_tx: tx,
                 buffer_store: buffer_store.clone(),
                 image_store,
@@ -1596,13 +1597,25 @@ impl Project {
                 toolchain_store: None,
                 agent_location: None,
             };
-            this.set_role(role, cx);
+            project.set_role(role, cx);
             for worktree in worktrees {
-                this.add_worktree(&worktree, cx);
+                project.add_worktree(&worktree, cx);
             }
-            this
+            project
         })?;
 
+        let weak_project = project.downgrade();
+        lsp_store
+            .update(&mut cx, |lsp_store, cx| {
+                lsp_store.set_language_server_statuses_from_proto(
+                    weak_project,
+                    response.payload.language_servers,
+                    response.payload.language_server_capabilities,
+                    cx,
+                );
+            })
+            .ok();
+
         let subscriptions = subscriptions
             .into_iter()
             .map(|s| match s {
@@ -1618,7 +1631,7 @@ impl Project {
                 EntitySubscription::SettingsObserver(subscription) => {
                     subscription.set_entity(&settings_observer, &cx)
                 }
-                EntitySubscription::Project(subscription) => subscription.set_entity(&this, &cx),
+                EntitySubscription::Project(subscription) => subscription.set_entity(&project, &cx),
                 EntitySubscription::LspStore(subscription) => {
                     subscription.set_entity(&lsp_store, &cx)
                 }
@@ -1638,13 +1651,13 @@ impl Project {
             .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))?
             .await?;
 
-        this.update(&mut cx, |this, cx| {
+        project.update(&mut cx, |this, cx| {
             this.set_collaborators_from_proto(response.payload.collaborators, cx)?;
             this.client_subscriptions.extend(subscriptions);
             anyhow::Ok(())
         })??;
 
-        Ok(this)
+        Ok(project)
     }
 
     fn new_search_history() -> SearchHistory {
@@ -2315,10 +2328,14 @@ impl Project {
         self.join_project_response_message_id = message_id;
         self.set_worktrees_from_proto(message.worktrees, cx)?;
         self.set_collaborators_from_proto(message.collaborators, cx)?;
-        self.lsp_store.update(cx, |lsp_store, _| {
+
+        let project = cx.weak_entity();
+        self.lsp_store.update(cx, |lsp_store, cx| {
             lsp_store.set_language_server_statuses_from_proto(
+                project,
                 message.language_servers,
                 message.language_server_capabilities,
+                cx,
             )
         });
         self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync)
@@ -2971,6 +2988,7 @@ impl Project {
                                 buffer_id,
                                 server_id: *language_server_id,
                                 buffer_abs_path: PathBuf::from(&update.buffer_abs_path),
+                                name: name.clone(),
                             });
                         }
                     }
@@ -4697,6 +4715,20 @@ impl Project {
         })?
     }
 
+    async fn handle_toggle_lsp_logs(
+        project: Entity<Self>,
+        envelope: TypedEnvelope<proto::ToggleLspLogs>,
+        mut cx: AsyncApp,
+    ) -> Result<()> {
+        project.update(&mut cx, |_, cx| {
+            cx.emit(Event::ToggleLspLogs {
+                server_id: LanguageServerId::from_proto(envelope.payload.server_id),
+                enabled: envelope.payload.enabled,
+            })
+        })?;
+        Ok(())
+    }
+
     async fn handle_synchronize_buffers(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::SynchronizeBuffers>,

crates/project/src/project_tests.rs 🔗

@@ -1951,6 +1951,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
             server_id: LanguageServerId(1),
             buffer_id,
             buffer_abs_path: PathBuf::from(path!("/dir/a.rs")),
+            name: Some(fake_server.server.name())
         }
     );
     assert_eq!(

crates/proto/proto/lsp.proto 🔗

@@ -610,11 +610,36 @@ message ServerMetadataUpdated {
 message LanguageServerLog {
     uint64 project_id = 1;
     uint64 language_server_id = 2;
+    string message = 3;
     oneof log_type {
-        uint32 log_message_type = 3;
-        LspLogTrace log_trace = 4;
+        LogMessage log = 4;
+        TraceMessage trace = 5;
+        RpcMessage rpc = 6;
+    }
+}
+
+message LogMessage {
+    LogLevel level = 1;
+
+    enum LogLevel {
+        LOG = 0;
+        INFO = 1;
+        WARNING = 2;
+        ERROR = 3;
+    }
+}
+
+message TraceMessage {
+    optional string verbose_info = 1;
+}
+
+message RpcMessage {
+    Kind kind = 1;
+
+    enum Kind {
+        RECEIVED = 0;
+        SENT = 1;
     }
-    string message = 5;
 }
 
 message LspLogTrace {
@@ -932,3 +957,16 @@ message MultiLspQuery {
 message MultiLspQueryResponse {
     repeated LspResponse responses = 1;
 }
+
+message ToggleLspLogs {
+    uint64 project_id = 1;
+    LogType log_type = 2;
+    uint64 server_id = 3;
+    bool enabled = 4;
+
+    enum LogType {
+        LOG = 0;
+        TRACE = 1;
+        RPC = 2;
+    }
+}

crates/proto/proto/zed.proto 🔗

@@ -396,7 +396,8 @@ message Envelope {
         GitCloneResponse git_clone_response = 364;
 
         LspQuery lsp_query = 365;
-        LspQueryResponse lsp_query_response = 366; // current max
+        LspQueryResponse lsp_query_response = 366;
+        ToggleLspLogs toggle_lsp_logs = 367; // current max
     }
 
     reserved 87 to 88;

crates/proto/src/proto.rs 🔗

@@ -312,7 +312,8 @@ messages!(
     (GetDefaultBranch, Background),
     (GetDefaultBranchResponse, Background),
     (GitClone, Background),
-    (GitCloneResponse, Background)
+    (GitCloneResponse, Background),
+    (ToggleLspLogs, Background),
 );
 
 request_messages!(
@@ -481,7 +482,8 @@ request_messages!(
     (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse),
     (PullWorkspaceDiagnostics, Ack),
     (GetDefaultBranch, GetDefaultBranchResponse),
-    (GitClone, GitCloneResponse)
+    (GitClone, GitCloneResponse),
+    (ToggleLspLogs, Ack),
 );
 
 lsp_messages!(
@@ -612,6 +614,7 @@ entity_messages!(
     GitReset,
     GitCheckoutFiles,
     SetIndexText,
+    ToggleLspLogs,
 
     Push,
     Fetch,

crates/remote_server/src/headless_project.rs 🔗

@@ -1,5 +1,6 @@
 use ::proto::{FromProto, ToProto};
 use anyhow::{Context as _, Result, anyhow};
+use lsp::LanguageServerId;
 
 use extension::ExtensionHostProxy;
 use extension_host::headless_host::HeadlessExtensionStore;
@@ -14,6 +15,7 @@ use project::{
     buffer_store::{BufferStore, BufferStoreEvent},
     debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore},
     git_store::GitStore,
+    lsp_store::log_store::{self, GlobalLogStore, LanguageServerKind},
     project_settings::SettingsObserver,
     search::SearchQuery,
     task_store::TaskStore,
@@ -65,6 +67,7 @@ impl HeadlessProject {
         settings::init(cx);
         language::init(cx);
         project::Project::init_settings(cx);
+        log_store::init(false, cx);
     }
 
     pub fn new(
@@ -235,6 +238,7 @@ impl HeadlessProject {
         session.add_entity_request_handler(Self::handle_open_new_buffer);
         session.add_entity_request_handler(Self::handle_find_search_candidates);
         session.add_entity_request_handler(Self::handle_open_server_settings);
+        session.add_entity_message_handler(Self::handle_toggle_lsp_logs);
 
         session.add_entity_request_handler(BufferStore::handle_update_buffer);
         session.add_entity_message_handler(BufferStore::handle_close_buffer);
@@ -298,11 +302,40 @@ impl HeadlessProject {
 
     fn on_lsp_store_event(
         &mut self,
-        _lsp_store: Entity<LspStore>,
+        lsp_store: Entity<LspStore>,
         event: &LspStoreEvent,
         cx: &mut Context<Self>,
     ) {
         match event {
+            LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => {
+                let log_store = cx
+                    .try_global::<GlobalLogStore>()
+                    .map(|lsp_logs| lsp_logs.0.clone());
+                if let Some(log_store) = log_store {
+                    log_store.update(cx, |log_store, cx| {
+                        log_store.add_language_server(
+                            LanguageServerKind::LocalSsh {
+                                lsp_store: self.lsp_store.downgrade(),
+                            },
+                            *id,
+                            Some(name.clone()),
+                            *worktree_id,
+                            lsp_store.read(cx).language_server_for_id(*id),
+                            cx,
+                        );
+                    });
+                }
+            }
+            LspStoreEvent::LanguageServerRemoved(id) => {
+                let log_store = cx
+                    .try_global::<GlobalLogStore>()
+                    .map(|lsp_logs| lsp_logs.0.clone());
+                if let Some(log_store) = log_store {
+                    log_store.update(cx, |log_store, cx| {
+                        log_store.remove_language_server(*id, cx);
+                    });
+                }
+            }
             LspStoreEvent::LanguageServerUpdate {
                 language_server_id,
                 name,
@@ -326,16 +359,6 @@ impl HeadlessProject {
                     })
                     .log_err();
             }
-            LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => {
-                self.session
-                    .send(proto::LanguageServerLog {
-                        project_id: REMOTE_SERVER_PROJECT_ID,
-                        language_server_id: language_server_id.to_proto(),
-                        message: message.clone(),
-                        log_type: Some(log_type.to_proto()),
-                    })
-                    .log_err();
-            }
             LspStoreEvent::LanguageServerPrompt(prompt) => {
                 let request = self.session.request(proto::LanguageServerPromptRequest {
                     project_id: REMOTE_SERVER_PROJECT_ID,
@@ -509,7 +532,31 @@ impl HeadlessProject {
         })
     }
 
-    pub async fn handle_open_server_settings(
+    async fn handle_toggle_lsp_logs(
+        _: Entity<Self>,
+        envelope: TypedEnvelope<proto::ToggleLspLogs>,
+        mut cx: AsyncApp,
+    ) -> Result<()> {
+        let server_id = LanguageServerId::from_proto(envelope.payload.server_id);
+        let lsp_logs = cx
+            .update(|cx| {
+                cx.try_global::<GlobalLogStore>()
+                    .map(|lsp_logs| lsp_logs.0.clone())
+            })?
+            .context("lsp logs store is missing")?;
+
+        lsp_logs.update(&mut cx, |lsp_logs, _| {
+            // we do not support any other log toggling yet
+            if envelope.payload.enabled {
+                lsp_logs.enable_rpc_trace_for_language_server(server_id);
+            } else {
+                lsp_logs.disable_rpc_trace_for_language_server(server_id);
+            }
+        })?;
+        Ok(())
+    }
+
+    async fn handle_open_server_settings(
         this: Entity<Self>,
         _: TypedEnvelope<proto::OpenServerSettings>,
         mut cx: AsyncApp,
@@ -562,7 +609,7 @@ impl HeadlessProject {
         })
     }
 
-    pub async fn handle_find_search_candidates(
+    async fn handle_find_search_candidates(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::FindSearchCandidates>,
         mut cx: AsyncApp,
@@ -594,7 +641,7 @@ impl HeadlessProject {
         Ok(response)
     }
 
-    pub async fn handle_list_remote_directory(
+    async fn handle_list_remote_directory(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::ListRemoteDirectory>,
         cx: AsyncApp,
@@ -626,7 +673,7 @@ impl HeadlessProject {
         })
     }
 
-    pub async fn handle_get_path_metadata(
+    async fn handle_get_path_metadata(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::GetPathMetadata>,
         cx: AsyncApp,
@@ -644,7 +691,7 @@ impl HeadlessProject {
         })
     }
 
-    pub async fn handle_shutdown_remote_server(
+    async fn handle_shutdown_remote_server(
         _this: Entity<Self>,
         _envelope: TypedEnvelope<proto::ShutdownRemoteServer>,
         cx: AsyncApp,

crates/settings/src/settings.rs 🔗

@@ -30,7 +30,7 @@ pub struct ActiveSettingsProfileName(pub String);
 
 impl Global for ActiveSettingsProfileName {}
 
-#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize)]
 pub struct WorktreeId(usize);
 
 impl From<WorktreeId> for usize {

crates/zed/src/zed.rs 🔗

@@ -32,7 +32,8 @@ use gpui::{
 };
 use image_viewer::ImageInfo;
 use language::Capability;
-use language_tools::lsp_tool::{self, LspTool};
+use language_tools::lsp_button::{self, LspButton};
+use language_tools::lsp_log_view::LspLogToolbarItemView;
 use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
 use migrator::{migrate_keymap, migrate_settings};
 use onboarding::DOCS_URL;
@@ -396,12 +397,12 @@ pub fn initialize_workspace(
         let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
         let image_info = cx.new(|_cx| ImageInfo::new(workspace));
 
-        let lsp_tool_menu_handle = PopoverMenuHandle::default();
-        let lsp_tool =
-            cx.new(|cx| LspTool::new(workspace, lsp_tool_menu_handle.clone(), window, cx));
+        let lsp_button_menu_handle = PopoverMenuHandle::default();
+        let lsp_button =
+            cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx));
         workspace.register_action({
-            move |_, _: &lsp_tool::ToggleMenu, window, cx| {
-                lsp_tool_menu_handle.toggle(window, cx);
+            move |_, _: &lsp_button::ToggleMenu, window, cx| {
+                lsp_button_menu_handle.toggle(window, cx);
             }
         });
 
@@ -409,7 +410,7 @@ pub fn initialize_workspace(
             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(lsp_tool, window, cx);
+            status_bar.add_left_item(lsp_button, window, cx);
             status_bar.add_left_item(diagnostic_summary, window, cx);
             status_bar.add_left_item(activity_indicator, window, cx);
             status_bar.add_right_item(edit_prediction_button, window, cx);
@@ -988,7 +989,7 @@ fn initialize_pane(
             toolbar.add_item(diagnostic_editor_controls, window, cx);
             let project_search_bar = cx.new(|_| ProjectSearchBar::new());
             toolbar.add_item(project_search_bar, window, cx);
-            let lsp_log_item = cx.new(|_| language_tools::LspLogToolbarItemView::new());
+            let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new());
             toolbar.add_item(lsp_log_item, window, cx);
             let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
             toolbar.add_item(dap_log_item, window, cx);