Remote LSP logs (#36709)

Ben Kunkle , Kirill Bulatov , and Lukas Wirth created

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: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>

Change summary

Cargo.lock                                                      |   2 
crates/assistant_slash_command/Cargo.toml                       |   2 
crates/assistant_tool/Cargo.toml                                |   2 
crates/breadcrumbs/Cargo.toml                                   |   2 
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/copilot/Cargo.toml                                       |   2 
crates/editor/Cargo.toml                                        |   2 
crates/language_tools/Cargo.toml                                |   3 
crates/language_tools/src/language_tools.rs                     |   4 
crates/language_tools/src/lsp_log.rs                            | 633 ++
crates/language_tools/src/lsp_log_tests.rs                      |   2 
crates/language_tools/src/lsp_tool.rs                           |  87 
crates/project/src/lsp_store.rs                                 |  90 
crates/project/src/project.rs                                   |  21 
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/Cargo.toml                                 |   1 
crates/remote_server/src/headless_project.rs                    |  79 
crates/settings/src/settings.rs                                 |   2 
crates/workspace/Cargo.toml                                     |   3 
crates/workspace/src/pane_group.rs                              |  15 
crates/workspace/src/workspace.rs                               | 237 
29 files changed, 917 insertions(+), 341 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9213,6 +9213,7 @@ dependencies = [
  "language",
  "lsp",
  "project",
+ "proto",
  "release_channel",
  "serde_json",
  "settings",
@@ -13500,6 +13501,7 @@ dependencies = [
  "language",
  "language_extension",
  "language_model",
+ "language_tools",
  "languages",
  "libc",
  "log",

crates/assistant_slash_command/Cargo.toml 🔗

@@ -25,7 +25,7 @@ parking_lot.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 ui.workspace = true
-workspace.workspace = true
+workspace = { path = "../workspace", default-features = false }
 workspace-hack.workspace = true
 
 [dev-dependencies]

crates/assistant_tool/Cargo.toml 🔗

@@ -28,7 +28,7 @@ serde.workspace = true
 serde_json.workspace = true
 text.workspace = true
 util.workspace = true
-workspace.workspace = true
+workspace = { path = "../workspace", default-features = false }
 workspace-hack.workspace = true
 
 [dev-dependencies]

crates/breadcrumbs/Cargo.toml 🔗

@@ -19,7 +19,7 @@ itertools.workspace = true
 settings.workspace = true
 theme.workspace = true
 ui.workspace = true
-workspace.workspace = true
+workspace = { path = "../workspace", default-features = false }
 zed_actions.workspace = true
 workspace-hack.workspace = true
 

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/copilot/Cargo.toml 🔗

@@ -50,7 +50,7 @@ sum_tree.workspace = true
 task.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace.workspace = true
+workspace = { path = "../workspace", default-features = false }
 workspace-hack.workspace = true
 itertools.workspace = true
 

crates/editor/Cargo.toml 🔗

@@ -89,7 +89,7 @@ ui.workspace = true
 url.workspace = true
 util.workspace = true
 uuid.workspace = true
-workspace.workspace = true
+workspace = { path = "../workspace", default-features = false }
 zed_actions.workspace = true
 workspace-hack.workspace = true
 

crates/language_tools/Cargo.toml 🔗

@@ -24,13 +24,14 @@ 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
 tree-sitter.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace.workspace = true
+workspace = { path = "../workspace", default-features = false }
 zed_actions.workspace = true
 workspace-hack.workspace = true
 

crates/language_tools/src/language_tools.rs 🔗

@@ -1,5 +1,5 @@
 mod key_context_view;
-mod lsp_log;
+pub mod lsp_log;
 pub mod lsp_tool;
 mod syntax_tree_view;
 
@@ -14,7 +14,7 @@ use ui::{Context, Window};
 use workspace::{Item, ItemHandle, SplitDirection, Workspace};
 
 pub fn init(cx: &mut App) {
-    lsp_log::init(cx);
+    lsp_log::init(true, cx);
     syntax_tree_view::init(cx);
     key_context_view::init(cx);
 }

crates/language_tools/src/lsp_log.rs 🔗

@@ -9,12 +9,16 @@ use gpui::{
 use itertools::Itertools;
 use language::{LanguageServerId, language_settings::SoftWrap};
 use lsp::{
-    IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType,
-    SetTraceParams, TraceValue, notification::SetTrace,
+    IoKind, LanguageServer, LanguageServerBinary, LanguageServerName, LanguageServerSelector,
+    MessageType, SetTraceParams, TraceValue, notification::SetTrace,
+};
+use project::{
+    LspStore, Project, ProjectItem, WorktreeId, lsp_store::LanguageServerLogType,
+    search::SearchQuery,
 };
-use project::{Project, WorktreeId, 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},
@@ -28,6 +32,7 @@ const RECEIVE_LINE: &str = "\n// Receive:";
 const MAX_STORED_LOG_ENTRIES: usize = 2000;
 
 pub struct LogStore {
+    store_logs: bool,
     projects: HashMap<WeakEntity<Project>, ProjectState>,
     language_servers: HashMap<LanguageServerId, LanguageServerState>,
     copilot_log_subscription: Option<lsp::Subscription>,
@@ -46,6 +51,7 @@ trait Message: AsRef<str> {
     }
 }
 
+#[derive(Debug)]
 pub(super) struct LogMessage {
     message: String,
     typ: MessageType,
@@ -73,8 +79,10 @@ impl Message for LogMessage {
     }
 }
 
+#[derive(Debug)]
 pub(super) struct TraceMessage {
     message: String,
+    is_verbose: bool,
 }
 
 impl AsRef<str> for TraceMessage {
@@ -84,9 +92,18 @@ impl AsRef<str> for TraceMessage {
 }
 
 impl Message for TraceMessage {
-    type Level = ();
+    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)]
 struct RpcMessage {
     message: String,
 }
@@ -101,7 +118,7 @@ impl Message for RpcMessage {
     type Level = ();
 }
 
-pub(super) struct LanguageServerState {
+pub struct LanguageServerState {
     name: Option<LanguageServerName>,
     worktree_id: Option<WorktreeId>,
     kind: LanguageServerKind,
@@ -113,24 +130,35 @@ pub(super) struct LanguageServerState {
     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 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::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"),
             LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
         }
     }
@@ -141,12 +169,14 @@ impl LanguageServerKind {
         match self {
             Self::Local { project } => Some(project),
             Self::Remote { project } => Some(project),
+            Self::LocalSsh { .. } => None,
             Self::Global { .. } => None,
         }
     }
 }
 
-struct LanguageServerRpcState {
+#[derive(Debug)]
+pub struct LanguageServerRpcState {
     rpc_messages: VecDeque<RpcMessage>,
     last_message_kind: Option<MessageKind>,
 }
@@ -167,7 +197,7 @@ pub struct LspLogToolbarItemView {
     _log_view_subscription: Option<Subscription>,
 }
 
-#[derive(Copy, Clone, PartialEq, Eq)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
 enum MessageKind {
     Send,
     Receive,
@@ -183,6 +213,13 @@ pub enum LogKind {
 }
 
 impl LogKind {
+    fn from_server_log_type(log_type: &LanguageServerLogType) -> Self {
+        match log_type {
+            LanguageServerLogType::Log(_) => Self::Logs,
+            LanguageServerLogType::Trace { .. } => Self::Trace,
+            LanguageServerLogType::Rpc { .. } => Self::Rpc,
+        }
+    }
     fn label(&self) -> &'static str {
         match self {
             LogKind::Rpc => RPC_MESSAGES,
@@ -212,59 +249,53 @@ actions!(
     ]
 );
 
-pub(super) struct GlobalLogStore(pub WeakEntity<LogStore>);
+pub struct GlobalLogStore(pub WeakEntity<LogStore>);
 
 impl Global for GlobalLogStore {}
 
-pub fn init(cx: &mut App) {
-    let log_store = cx.new(LogStore::new);
+pub fn init(store_logs: bool, cx: &mut App) {
+    let log_store = cx.new(|cx| LogStore::new(store_logs, cx));
     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);
-            });
-        }
+        log_store.update(cx, |store, cx| {
+            store.add_project(workspace.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),
-                );
-            }
+            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),
+            );
         });
     })
     .detach();
 }
 
 impl LogStore {
-    pub fn new(cx: &mut Context<Self>) -> Self {
+    pub fn new(store_logs: bool, cx: &mut Context<Self>) -> Self {
         let (io_tx, mut io_rx) = mpsc::unbounded();
 
         let copilot_subscription = 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 +305,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),
@@ -287,26 +319,27 @@ impl LogStore {
             })
         });
 
-        let this = Self {
+        let log_store = Self {
             copilot_log_subscription: None,
             _copilot_subscription: copilot_subscription,
             projects: HashMap::default(),
             language_servers: HashMap::default(),
+            store_logs,
             io_tx,
         };
 
-        cx.spawn(async move |this, cx| {
+        cx.spawn(async move |log_store, 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);
+                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);
-        this
+        log_store
     }
 
     pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
@@ -320,20 +353,19 @@ impl LogStore {
                         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 {
+                    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::Local {
+                            LanguageServerKind::Remote {
                                 project: project.downgrade(),
                             }
                         };
-
                         match event {
                             project::Event::LanguageServerAdded(id, name, worktree_id) => {
-                                this.add_language_server(
+                                log_store.add_language_server(
                                     server_kind,
                                     *id,
                                     Some(name.clone()),
@@ -346,20 +378,78 @@ impl LogStore {
                                     cx,
                                 );
                             }
+                            project::Event::LanguageServerBufferRegistered {
+                                server_id,
+                                buffer_id,
+                                name,
+                                ..
+                            } if project.read(cx).is_via_collab() => {
+                                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,
+                                );
+                            }
                             project::Event::LanguageServerRemoved(id) => {
-                                this.remove_language_server(*id, cx);
+                                log_store.remove_language_server(*id, cx);
                             }
                             project::Event::LanguageServerLog(id, typ, message) => {
-                                this.add_language_server(server_kind, *id, None, None, None, cx);
+                                log_store.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);
+                                        log_store.add_language_server_log(*id, *typ, message, cx);
                                     }
-                                    project::LanguageServerLogType::Trace(_) => {
-                                        this.add_language_server_trace(*id, message, cx);
+                                    project::LanguageServerLogType::Trace { verbose_info } => {
+                                        log_store.add_language_server_trace(
+                                            *id,
+                                            message,
+                                            verbose_info.clone(),
+                                            cx,
+                                        );
+                                    }
+                                    project::LanguageServerLogType::Rpc { received } => {
+                                        let kind = if *received {
+                                            MessageKind::Receive
+                                        } else {
+                                            MessageKind::Send
+                                        };
+                                        log_store.add_language_server_rpc(*id, kind, message, cx);
                                     }
                                 }
                             }
+                            project::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);
+                                }
+                            }
                             _ => {}
                         }
                     }),
@@ -375,7 +465,7 @@ impl LogStore {
         self.language_servers.get_mut(&id)
     }
 
-    fn add_language_server(
+    pub fn add_language_server(
         &mut self,
         kind: LanguageServerKind,
         server_id: LanguageServerId,
@@ -426,20 +516,35 @@ impl LogStore {
         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;
-        Self::add_language_server_message(
+        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,
-            id,
-            LogMessage {
-                message: message.trim_end().to_string(),
-                typ,
-            },
+            LogMessage { message, typ },
             language_server_state.log_level,
-            LogKind::Logs,
-            cx,
-        );
+        ) {
+            self.emit_event(
+                Event::NewServerLogEntry {
+                    id,
+                    kind: LanguageServerLogType::Log(typ),
+                    text: new_message,
+                },
+                cx,
+            );
+        }
         Some(())
     }
 
@@ -447,46 +552,127 @@ impl LogStore {
         &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;
-        Self::add_language_server_message(
+        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,
-            id,
             TraceMessage {
                 message: message.trim().to_string(),
+                is_verbose: false,
             },
-            (),
-            LogKind::Trace,
-            cx,
-        );
+            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 add_language_server_message<T: Message>(
+    fn push_new_message<T: Message>(
         log_lines: &mut VecDeque<T>,
-        id: LanguageServerId,
         message: T,
         current_severity: <T as Message>::Level,
-        kind: LogKind,
-        cx: &mut Context<Self>,
-    ) {
+    ) -> Option<String> {
         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);
+
+        let visible_message = visible.then(|| message.as_ref().to_string());
         log_lines.push_back(message);
+        visible_message
+    }
 
-        if visible {
-            cx.emit(Event::NewServerLogEntry { id, kind, text });
-            cx.notify();
+    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,
+        );
     }
 
-    fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
+    pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
         self.language_servers.remove(&id);
         cx.notify();
     }
@@ -516,11 +702,11 @@ impl LogStore {
                         None
                     }
                 }
-                LanguageServerKind::Global => Some(*id),
+                LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id),
             })
     }
 
-    fn enable_rpc_trace_for_language_server(
+    pub fn enable_rpc_trace_for_language_server(
         &mut self,
         server_id: LanguageServerId,
     ) -> Option<&mut LanguageServerRpcState> {
@@ -663,50 +849,45 @@ impl LogStore {
             }
         };
 
-        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(),
-            });
-        }
+        self.add_language_server_rpc(language_server_id, kind, message, cx);
+        cx.notify();
+        Some(())
+    }
 
-        while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
-            rpc_log_lines.pop_front();
+    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();
+                    }
+                }
+            }
         }
 
-        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(),
-        });
-        cx.notify();
-        Some(())
+        cx.emit(e);
     }
 }
 
@@ -751,13 +932,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 +982,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 +997,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 +1020,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 +1035,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 +1072,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 +1186,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 +1214,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 +1259,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 +1324,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 +1652,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 +1673,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,
@@ -1696,12 +1929,6 @@ const SERVER_LOGS: &str = "Server Logs";
 const SERVER_TRACE: &str = "Server Trace";
 const SERVER_INFO: &str = "Server Info";
 
-impl Default for LspLogToolbarItemView {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
 impl LspLogToolbarItemView {
     pub fn new() -> Self {
         Self {

crates/language_tools/src/lsp_log_tests.rs 🔗

@@ -51,7 +51,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

crates/language_tools/src/lsp_tool.rs 🔗

@@ -122,8 +122,7 @@ impl LanguageServerState {
         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 {
+        let Some(lsp_logs) = lsp_logs else {
             return menu;
         };
 
@@ -210,10 +209,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 +241,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 +288,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 +338,15 @@ impl LanguageServerState {
                                 anyhow::Ok(())
                             })
                             .detach();
+                        } else if has_logs {
+                            lsp_logs.update(cx, |lsp_logs, cx| {
+                                lsp_logs.open_server_trace(
+                                    workspace.clone(),
+                                    server_selector.clone(),
+                                    window,
+                                    cx,
+                                );
+                            });
                         } else {
                             cx.propagate();
                         }
@@ -529,26 +529,48 @@ impl LspTool {
             });
 
         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)
             });
 
-        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_tool = Self {
+            server_state,
             popover_menu_handle,
             lsp_menu: None,
             lsp_menu_refresh: Task::ready(()),
             _subscriptions: vec![settings_subscription, lsp_store_subscription],
+        };
+        if !lsp_tool
+            .server_state
+            .read(cx)
+            .language_servers
+            .binary_statuses
+            .is_empty()
+        {
+            lsp_tool.refresh_lsp_menu(true, window, cx);
         }
+
+        lsp_tool
     }
 
     fn on_lsp_store_event(
@@ -708,6 +730,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();

crates/project/src/lsp_store.rs 🔗

@@ -977,7 +977,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 +3484,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 +3567,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 +7486,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"),
@@ -7527,6 +7530,7 @@ impl LspStore {
                         pending_work: Default::default(),
                         has_pending_diagnostic_updates: false,
                         progress_tokens: Default::default(),
+                        worktree: server.worktree_id.map(WorktreeId::from_proto),
                     },
                 )
             })
@@ -8892,6 +8896,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 +10910,7 @@ impl LspStore {
                 pending_work: Default::default(),
                 has_pending_diagnostic_updates: false,
                 progress_tokens: Default::default(),
+                worktree: Some(key.worktree_id),
             },
         );
 
@@ -12190,6 +12196,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 +12713,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/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);
@@ -2971,6 +2977,7 @@ impl Project {
                                 buffer_id,
                                 server_id: *language_server_id,
                                 buffer_abs_path: PathBuf::from(&update.buffer_abs_path),
+                                name: name.clone(),
                             });
                         }
                     }
@@ -4697,6 +4704,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/Cargo.toml 🔗

@@ -43,6 +43,7 @@ gpui_tokio.workspace = true
 http_client.workspace = true
 language.workspace = true
 language_extension.workspace = true
+language_tools.workspace = true
 languages.workspace = true
 log.workspace = true
 lsp.workspace = true

crates/remote_server/src/headless_project.rs 🔗

@@ -1,5 +1,7 @@
 use ::proto::{FromProto, ToProto};
 use anyhow::{Context as _, Result, anyhow};
+use language_tools::lsp_log::{GlobalLogStore, LanguageServerKind};
+use lsp::LanguageServerId;
 
 use extension::ExtensionHostProxy;
 use extension_host::headless_host::HeadlessExtensionStore;
@@ -65,6 +67,7 @@ impl HeadlessProject {
         settings::init(cx);
         language::init(cx);
         project::Project::init_settings(cx);
+        language_tools::lsp_log::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>()
+                    .and_then(|lsp_logs| lsp_logs.0.upgrade());
+                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>()
+                    .and_then(|lsp_logs| lsp_logs.0.upgrade());
+                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>()
+                    .and_then(|lsp_logs| lsp_logs.0.upgrade())
+            })?
+            .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/workspace/Cargo.toml 🔗

@@ -13,6 +13,7 @@ path = "src/workspace.rs"
 doctest = false
 
 [features]
+default = ["call"]
 test-support = [
     "call/test-support",
     "client/test-support",
@@ -29,7 +30,7 @@ test-support = [
 any_vec.workspace = true
 anyhow.workspace = true
 async-recursion.workspace = true
-call.workspace = true
+call = { workspace = true, optional = true }
 client.workspace = true
 clock.workspace = true
 collections.workspace = true

crates/workspace/src/pane_group.rs 🔗

@@ -4,11 +4,14 @@ use crate::{
     workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical},
 };
 use anyhow::Result;
+
+#[cfg(feature = "call")]
 use call::{ActiveCall, ParticipantLocation};
+
 use collections::HashMap;
 use gpui::{
-    Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels,
-    Point, StyleRefinement, WeakEntity, Window, point, size,
+    Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, Pixels, Point,
+    StyleRefinement, WeakEntity, Window, point, size,
 };
 use parking_lot::Mutex;
 use project::Project;
@@ -197,6 +200,7 @@ pub enum Member {
 pub struct PaneRenderContext<'a> {
     pub project: &'a Entity<Project>,
     pub follower_states: &'a HashMap<CollaboratorId, FollowerState>,
+    #[cfg(feature = "call")]
     pub active_call: Option<&'a Entity<ActiveCall>>,
     pub active_pane: &'a Entity<Pane>,
     pub app_state: &'a Arc<AppState>,
@@ -258,6 +262,11 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
         let mut leader_color;
         let status_box;
         match leader_id {
+            #[cfg(not(feature = "call"))]
+            CollaboratorId::PeerId(_) => {
+                return LeaderDecoration::default();
+            }
+            #[cfg(feature = "call")]
             CollaboratorId::PeerId(peer_id) => {
                 let Some(leader) = self.active_call.as_ref().and_then(|call| {
                     let room = call.read(cx).room()?.read(cx);
@@ -315,7 +324,7 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
                             |this, (leader_project_id, leader_user_id)| {
                                 let app_state = self.app_state.clone();
                                 this.cursor_pointer().on_mouse_down(
-                                    MouseButton::Left,
+                                    gpui::MouseButton::Left,
                                     move |_, _, cx| {
                                         crate::join_in_room_project(
                                             leader_project_id,

crates/workspace/src/workspace.rs 🔗

@@ -9,6 +9,7 @@ pub mod pane_group;
 mod path_list;
 mod persistence;
 pub mod searchable;
+#[cfg(feature = "call")]
 pub mod shared_screen;
 mod status_bar;
 pub mod tasks;
@@ -22,11 +23,17 @@ pub use dock::Panel;
 pub use path_list::PathList;
 pub use toast_layer::{ToastAction, ToastLayer, ToastView};
 
-use anyhow::{Context as _, Result, anyhow};
+#[cfg(feature = "call")]
 use call::{ActiveCall, call_settings::CallSettings};
+#[cfg(feature = "call")]
+use client::{Status, proto::ErrorCode};
+#[cfg(feature = "call")]
+use shared_screen::SharedScreen;
+
+use anyhow::{Context as _, Result, anyhow};
 use client::{
-    ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
-    proto::{self, ErrorCode, PanelId, PeerId},
+    ChannelId, Client, ErrorExt, TypedEnvelope, UserStore,
+    proto::{self, PanelId, PeerId},
 };
 use collections::{HashMap, HashSet, hash_map};
 use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
@@ -79,7 +86,6 @@ use schemars::JsonSchema;
 use serde::Deserialize;
 use session::AppSession;
 use settings::{Settings, update_settings_file};
-use shared_screen::SharedScreen;
 use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
@@ -886,6 +892,7 @@ impl Global for GlobalAppState {}
 
 pub struct WorkspaceStore {
     workspaces: HashSet<WindowHandle<Workspace>>,
+    #[cfg(feature = "call")]
     client: Arc<Client>,
     _subscriptions: Vec<client::Subscription>,
 }
@@ -1117,6 +1124,7 @@ pub struct Workspace {
     window_edited: bool,
     last_window_title: Option<String>,
     dirty_items: HashMap<EntityId, Subscription>,
+    #[cfg(feature = "call")]
     active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: Option<WorkspaceId>,
@@ -1158,6 +1166,7 @@ pub struct FollowerState {
 
 struct FollowerView {
     view: Box<dyn FollowableItemHandle>,
+    #[cfg(feature = "call")]
     location: Option<proto::PanelId>,
 }
 
@@ -1357,10 +1366,15 @@ impl Workspace {
 
         let session_id = app_state.session.read(cx).id().to_owned();
 
+        #[cfg(feature = "call")]
         let mut active_call = None;
-        if let Some(call) = ActiveCall::try_global(cx) {
-            let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
-            active_call = Some((call, subscriptions));
+        #[cfg(feature = "call")]
+        {
+            if let Some(call) = ActiveCall::try_global(cx) {
+                let subscriptions =
+                    vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
+                active_call = Some((call, subscriptions));
+            }
         }
 
         let (serializable_items_tx, serializable_items_rx) =
@@ -1446,6 +1460,7 @@ impl Workspace {
             window_edited: false,
             last_window_title: None,
             dirty_items: Default::default(),
+            #[cfg(feature = "call")]
             active_call,
             database_id: workspace_id,
             app_state,
@@ -2250,6 +2265,7 @@ impl Workspace {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<bool>> {
+        #[cfg(feature = "call")]
         let active_call = self.active_call().cloned();
 
         // On Linux and Windows, closing the last window should restore the last workspace.
@@ -2258,51 +2274,58 @@ impl Workspace {
             && cx.windows().len() == 1;
 
         cx.spawn_in(window, async move |this, cx| {
-            let workspace_count = cx.update(|_window, cx| {
-                cx.windows()
-                    .iter()
-                    .filter(|window| window.downcast::<Workspace>().is_some())
-                    .count()
-            })?;
-
-            if let Some(active_call) = active_call
-                && workspace_count == 1
-                && active_call.read_with(cx, |call, _| call.room().is_some())?
+            #[cfg(feature = "call")]
             {
-                if close_intent == CloseIntent::CloseWindow {
-                    let answer = cx.update(|window, cx| {
-                        window.prompt(
-                            PromptLevel::Warning,
-                            "Do you want to leave the current call?",
-                            None,
-                            &["Close window and hang up", "Cancel"],
-                            cx,
-                        )
-                    })?;
+                let workspace_count = cx.update(|_window, cx| {
+                    cx.windows()
+                        .iter()
+                        .filter(|window| window.downcast::<Workspace>().is_some())
+                        .count()
+                })?;
+                if let Some(active_call) = active_call
+                    && workspace_count == 1
+                    && active_call.read_with(cx, |call, _| call.room().is_some())?
+                {
+                    if close_intent == CloseIntent::CloseWindow {
+                        let answer = cx.update(|window, cx| {
+                            window.prompt(
+                                PromptLevel::Warning,
+                                "Do you want to leave the current call?",
+                                None,
+                                &["Close window and hang up", "Cancel"],
+                                cx,
+                            )
+                        })?;
 
-                    if answer.await.log_err() == Some(1) {
-                        return anyhow::Ok(false);
-                    } else {
-                        active_call
-                            .update(cx, |call, cx| call.hang_up(cx))?
-                            .await
-                            .log_err();
+                        if answer.await.log_err() == Some(1) {
+                            return anyhow::Ok(false);
+                        } else {
+                            {
+                                active_call
+                                    .update(cx, |call, cx| call.hang_up(cx))?
+                                    .await
+                                    .log_err();
+                            }
+                        }
                     }
-                }
-                if close_intent == CloseIntent::ReplaceWindow {
-                    _ = active_call.update(cx, |this, cx| {
-                        let workspace = cx
-                            .windows()
-                            .iter()
-                            .filter_map(|window| window.downcast::<Workspace>())
-                            .next()
-                            .unwrap();
-                        let project = workspace.read(cx)?.project.clone();
-                        if project.read(cx).is_shared() {
-                            this.unshare_project(project, cx)?;
+                    if close_intent == CloseIntent::ReplaceWindow {
+                        #[cfg(feature = "call")]
+                        {
+                            _ = active_call.update(cx, |active_call, cx| {
+                                let workspace = cx
+                                    .windows()
+                                    .iter()
+                                    .filter_map(|window| window.downcast::<Workspace>())
+                                    .next()
+                                    .unwrap();
+                                let project = workspace.read(cx)?.project.clone();
+                                if project.read(cx).is_shared() {
+                                    active_call.unshare_project(project, cx)?;
+                                }
+                                anyhow::Ok(())
+                            })?;
                         }
-                        Ok::<_, anyhow::Error>(())
-                    })?;
+                    }
                 }
             }
 
@@ -3486,6 +3509,7 @@ impl Workspace {
         item
     }
 
+    #[cfg(feature = "call")]
     pub fn open_shared_screen(
         &mut self,
         peer_id: PeerId,
@@ -3907,8 +3931,11 @@ impl Workspace {
                 pane.update(cx, |pane, _| {
                     pane.track_alternate_file_items();
                 });
-                if *local {
-                    self.unfollow_in_pane(pane, window, cx);
+                #[cfg(feature = "call")]
+                {
+                    if *local {
+                        self.unfollow_in_pane(pane, window, cx);
+                    }
                 }
                 serialize_workspace = *focus_changed || pane != self.active_pane();
                 if pane == self.active_pane() {
@@ -3973,6 +4000,17 @@ impl Workspace {
         }
     }
 
+    #[cfg(not(feature = "call"))]
+    pub fn unfollow_in_pane(
+        &mut self,
+        _pane: &Entity<Pane>,
+        _window: &mut Window,
+        _cx: &mut Context<Workspace>,
+    ) -> Option<CollaboratorId> {
+        None
+    }
+
+    #[cfg(feature = "call")]
     pub fn unfollow_in_pane(
         &mut self,
         pane: &Entity<Pane>,
@@ -4122,6 +4160,7 @@ impl Workspace {
         cx.notify();
     }
 
+    #[cfg(feature = "call")]
     pub fn start_following(
         &mut self,
         leader_id: impl Into<CollaboratorId>,
@@ -4185,6 +4224,16 @@ impl Workspace {
         }
     }
 
+    #[cfg(not(feature = "call"))]
+    pub fn follow_next_collaborator(
+        &mut self,
+        _: &FollowNextCollaborator,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) {
+    }
+
+    #[cfg(feature = "call")]
     pub fn follow_next_collaborator(
         &mut self,
         _: &FollowNextCollaborator,
@@ -4233,6 +4282,16 @@ impl Workspace {
         }
     }
 
+    #[cfg(not(feature = "call"))]
+    pub fn follow(
+        &mut self,
+        _leader_id: impl Into<CollaboratorId>,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) {
+    }
+
+    #[cfg(feature = "call")]
     pub fn follow(
         &mut self,
         leader_id: impl Into<CollaboratorId>,
@@ -4285,6 +4344,17 @@ impl Workspace {
         }
     }
 
+    #[cfg(not(feature = "call"))]
+    pub fn unfollow(
+        &mut self,
+        _leader_id: impl Into<CollaboratorId>,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) -> Option<()> {
+        None
+    }
+
+    #[cfg(feature = "call")]
     pub fn unfollow(
         &mut self,
         leader_id: impl Into<CollaboratorId>,
@@ -4595,6 +4665,7 @@ impl Workspace {
             anyhow::bail!("no id for view");
         };
         let id = ViewId::from_proto(id)?;
+        #[cfg(feature = "call")]
         let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
 
         let pane = this.update(cx, |this, _cx| {
@@ -4667,6 +4738,7 @@ impl Workspace {
                 id,
                 FollowerView {
                     view: item,
+                    #[cfg(feature = "call")]
                     location: panel_id,
                 },
             );
@@ -4721,6 +4793,7 @@ impl Workspace {
                     view.map(|view| {
                         entry.insert(FollowerView {
                             view,
+                            #[cfg(feature = "call")]
                             location: None,
                         })
                     })
@@ -4911,6 +4984,17 @@ impl Workspace {
         )
     }
 
+    #[cfg(not(feature = "call"))]
+    fn active_item_for_peer(
+        &self,
+        _peer_id: PeerId,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
+        None
+    }
+
+    #[cfg(feature = "call")]
     fn active_item_for_peer(
         &self,
         peer_id: PeerId,
@@ -4952,6 +5036,7 @@ impl Workspace {
         item_to_activate
     }
 
+    #[cfg(feature = "call")]
     fn shared_screen_for_peer(
         &self,
         peer_id: PeerId,
@@ -5002,10 +5087,12 @@ impl Workspace {
         }
     }
 
+    #[cfg(feature = "call")]
     pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
         self.active_call.as_ref().map(|(call, _)| call)
     }
 
+    #[cfg(feature = "call")]
     fn on_active_call_event(
         &mut self,
         _: &Entity<ActiveCall>,
@@ -5918,6 +6005,17 @@ impl Workspace {
     }
 }
 
+#[cfg(not(feature = "call"))]
+fn leader_border_for_pane(
+    _follower_states: &HashMap<CollaboratorId, FollowerState>,
+    _pane: &Entity<Pane>,
+    _: &Window,
+    _cx: &App,
+) -> Option<Div> {
+    None
+}
+
+#[cfg(feature = "call")]
 fn leader_border_for_pane(
     follower_states: &HashMap<CollaboratorId, FollowerState>,
     pane: &Entity<Pane>,
@@ -6384,6 +6482,7 @@ impl Render for Workspace {
                                                                         &PaneRenderContext {
                                                                             follower_states:
                                                                                 &self.follower_states,
+                                                                            #[cfg(feature = "call")]
                                                                             active_call: self.active_call(),
                                                                             active_pane: &self.active_pane,
                                                                             app_state: &self.app_state,
@@ -6448,6 +6547,7 @@ impl Render for Workspace {
                                                                                 &PaneRenderContext {
                                                                                     follower_states:
                                                                                         &self.follower_states,
+                                                                                    #[cfg(feature = "call")]
                                                                                     active_call: self.active_call(),
                                                                                     active_pane: &self.active_pane,
                                                                                     app_state: &self.app_state,
@@ -6510,6 +6610,7 @@ impl Render for Workspace {
                                                                                 &PaneRenderContext {
                                                                                     follower_states:
                                                                                         &self.follower_states,
+                                                                                    #[cfg(feature = "call")]
                                                                                     active_call: self.active_call(),
                                                                                     active_pane: &self.active_pane,
                                                                                     app_state: &self.app_state,
@@ -6558,6 +6659,7 @@ impl Render for Workspace {
                                                                 &PaneRenderContext {
                                                                     follower_states:
                                                                         &self.follower_states,
+                                                                    #[cfg(feature = "call")]
                                                                     active_call: self.active_call(),
                                                                     active_pane: &self.active_pane,
                                                                     app_state: &self.app_state,
@@ -6631,10 +6733,22 @@ impl WorkspaceStore {
                 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
                 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
             ],
+            #[cfg(feature = "call")]
             client,
         }
     }
 
+    #[cfg(not(feature = "call"))]
+    pub fn update_followers(
+        &self,
+        _project_id: Option<u64>,
+        _update: proto::update_followers::Variant,
+        _cx: &App,
+    ) -> Option<()> {
+        None
+    }
+
+    #[cfg(feature = "call")]
     pub fn update_followers(
         &self,
         project_id: Option<u64>,
@@ -6800,6 +6914,7 @@ actions!(
     ]
 );
 
+#[cfg(feature = "call")]
 async fn join_channel_internal(
     channel_id: ChannelId,
     app_state: &Arc<AppState>,
@@ -6947,6 +7062,17 @@ async fn join_channel_internal(
     anyhow::Ok(false)
 }
 
+#[cfg(not(feature = "call"))]
+pub fn join_channel(
+    _channel_id: ChannelId,
+    _app_state: Arc<AppState>,
+    _requesting_window: Option<WindowHandle<Workspace>>,
+    _cx: &mut App,
+) -> Task<Result<()>> {
+    Task::ready(Ok(()))
+}
+
+#[cfg(feature = "call")]
 pub fn join_channel(
     channel_id: ChannelId,
     app_state: Arc<AppState>,
@@ -7454,6 +7580,17 @@ fn serialize_ssh_project(
     })
 }
 
+#[cfg(not(feature = "call"))]
+pub fn join_in_room_project(
+    _project_id: u64,
+    _follow_user_id: u64,
+    _app_state: Arc<AppState>,
+    _cx: &mut App,
+) -> Task<Result<()>> {
+    Task::ready(Ok(()))
+}
+
+#[cfg(feature = "call")]
 pub fn join_in_room_project(
     project_id: u64,
     follow_user_id: u64,