Enhance LSP log viewer to show server logs in addition to RPC trace (#2586)

Max Brunsfeld created

In debugging what's going on with the Elixir language server, there was
some interesting content in the server's logs (sent to the app via the
`window/logMessage` LSP endpoint). I decided to invest in making
language server issues easier to debug by exposing these `logMessage`
contents in the app.

Also, improve the UI of the view slightly:

* Select one of the servers by default (instead of "no server selected")
* Make it clearer that the menu is clickable

Change summary

Cargo.lock                          |   2 
crates/lsp/src/lsp.rs               |   9 
crates/lsp_log/Cargo.toml           |   2 
crates/lsp_log/src/lsp_log.rs       | 489 +++++++++++++++++++++---------
crates/lsp_log/src/lsp_log_tests.rs |  97 ++++++
crates/project/src/project.rs       |  60 ++-
crates/project/src/project_tests.rs |   9 
crates/theme/src/theme.rs           |  11 
crates/workspace/src/workspace.rs   |   2 
crates/zed/src/zed.rs               |   5 
styles/src/styleTree/app.ts         |   2 
styles/src/styleTree/lspLogMenu.ts  |  42 ++
12 files changed, 555 insertions(+), 175 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3764,8 +3764,10 @@ name = "lsp_log"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "client",
  "collections",
  "editor",
+ "env_logger 0.9.3",
  "futures 0.3.28",
  "gpui",
  "language",

crates/lsp/src/lsp.rs 🔗

@@ -748,6 +748,15 @@ impl fmt::Display for LanguageServerId {
     }
 }
 
+impl fmt::Debug for LanguageServer {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("LanguageServer")
+            .field("id", &self.server_id.0)
+            .field("name", &self.name)
+            .finish_non_exhaustive()
+    }
+}
+
 impl Drop for Subscription {
     fn drop(&mut self) {
         match self {

crates/lsp_log/Cargo.toml 🔗

@@ -24,7 +24,9 @@ serde.workspace = true
 anyhow.workspace = true
 
 [dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
+env_logger.workspace = true
 unindent.workspace = true

crates/lsp_log/src/lsp_log.rs 🔗

@@ -1,4 +1,7 @@
-use collections::{hash_map, HashMap};
+#[cfg(test)]
+mod lsp_log_tests;
+
+use collections::HashMap;
 use editor::Editor;
 use futures::{channel::mpsc, StreamExt};
 use gpui::{
@@ -12,28 +15,33 @@ use gpui::{
     ViewHandle, WeakModelHandle,
 };
 use language::{Buffer, LanguageServerId, LanguageServerName};
-use project::{Project, WorktreeId};
+use project::{Project, Worktree};
 use std::{borrow::Cow, sync::Arc};
 use theme::{ui, Theme};
 use workspace::{
     item::{Item, ItemHandle},
-    ToolbarItemLocation, ToolbarItemView, Workspace,
+    ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
 };
 
 const SEND_LINE: &str = "// Send:\n";
 const RECEIVE_LINE: &str = "// Receive:\n";
 
 struct LogStore {
-    projects: HashMap<WeakModelHandle<Project>, LogStoreProject>,
+    projects: HashMap<WeakModelHandle<Project>, ProjectState>,
     io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
 }
 
-struct LogStoreProject {
-    servers: HashMap<LanguageServerId, LogStoreLanguageServer>,
-    _subscription: gpui::Subscription,
+struct ProjectState {
+    servers: HashMap<LanguageServerId, LanguageServerState>,
+    _subscriptions: [gpui::Subscription; 2],
+}
+
+struct LanguageServerState {
+    log_buffer: ModelHandle<Buffer>,
+    rpc_state: Option<LanguageServerRpcState>,
 }
 
-struct LogStoreLanguageServer {
+struct LanguageServerRpcState {
     buffer: ModelHandle<Buffer>,
     last_message_kind: Option<MessageKind>,
     _subscription: lsp::Subscription,
@@ -42,6 +50,7 @@ struct LogStoreLanguageServer {
 pub struct LspLogView {
     log_store: ModelHandle<LogStore>,
     current_server_id: Option<LanguageServerId>,
+    is_showing_rpc_trace: bool,
     editor: Option<ViewHandle<Editor>>,
     project: ModelHandle<Project>,
 }
@@ -49,7 +58,6 @@ pub struct LspLogView {
 pub struct LspLogToolbarItemView {
     log_view: Option<ViewHandle<LspLogView>>,
     menu_open: bool,
-    project: ModelHandle<Project>,
 }
 
 #[derive(Copy, Clone, PartialEq, Eq)]
@@ -58,10 +66,36 @@ enum MessageKind {
     Receive,
 }
 
+#[derive(Clone, Debug, PartialEq)]
+struct LogMenuItem {
+    server_id: LanguageServerId,
+    server_name: LanguageServerName,
+    worktree: ModelHandle<Worktree>,
+    rpc_trace_enabled: bool,
+    rpc_trace_selected: bool,
+    logs_selected: bool,
+}
+
 actions!(log, [OpenLanguageServerLogs]);
 
 pub fn init(cx: &mut AppContext) {
-    let log_set = cx.add_model(|cx| LogStore::new(cx));
+    let log_store = cx.add_model(|cx| LogStore::new(cx));
+
+    cx.subscribe_global::<WorkspaceCreated, _>({
+        let log_store = log_store.clone();
+        move |event, cx| {
+            let workspace = &event.0;
+            if let Some(workspace) = workspace.upgrade(cx) {
+                let project = workspace.read(cx).project().clone();
+                if project.read(cx).is_local() {
+                    log_store.update(cx, |store, cx| {
+                        store.add_project(&project, cx);
+                    });
+                }
+            }
+        }
+    })
+    .detach();
 
     cx.add_action(
         move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
@@ -69,7 +103,7 @@ pub fn init(cx: &mut AppContext) {
             if project.is_local() {
                 workspace.add_item(
                     Box::new(cx.add_view(|cx| {
-                        LspLogView::new(workspace.project().clone(), log_set.clone(), cx)
+                        LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
                     })),
                     cx,
                 );
@@ -100,34 +134,113 @@ impl LogStore {
         this
     }
 
-    pub fn has_enabled_logs_for_language_server(
+    pub fn add_project(&mut self, project: &ModelHandle<Project>, cx: &mut ModelContext<Self>) {
+        use project::Event::*;
+
+        let weak_project = project.downgrade();
+        self.projects.insert(
+            weak_project,
+            ProjectState {
+                servers: HashMap::default(),
+                _subscriptions: [
+                    cx.observe_release(&project, move |this, _, _| {
+                        this.projects.remove(&weak_project);
+                    }),
+                    cx.subscribe(project, |this, project, event, cx| match event {
+                        LanguageServerAdded(id) => {
+                            this.add_language_server(&project, *id, cx);
+                        }
+                        LanguageServerRemoved(id) => {
+                            this.remove_language_server(&project, *id, cx);
+                        }
+                        LanguageServerLog(id, message) => {
+                            this.add_language_server_log(&project, *id, message, cx);
+                        }
+                        _ => {}
+                    }),
+                ],
+            },
+        );
+    }
+
+    fn add_language_server(
+        &mut self,
+        project: &ModelHandle<Project>,
+        id: LanguageServerId,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<ModelHandle<Buffer>> {
+        let project_state = self.projects.get_mut(&project.downgrade())?;
+        Some(
+            project_state
+                .servers
+                .entry(id)
+                .or_insert_with(|| {
+                    cx.notify();
+                    LanguageServerState {
+                        rpc_state: None,
+                        log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(),
+                    }
+                })
+                .log_buffer
+                .clone(),
+        )
+    }
+
+    fn add_language_server_log(
+        &mut self,
+        project: &ModelHandle<Project>,
+        id: LanguageServerId,
+        message: &str,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<()> {
+        let buffer = self.add_language_server(&project, id, cx)?;
+        buffer.update(cx, |buffer, cx| {
+            let len = buffer.len();
+            let has_newline = message.ends_with("\n");
+            buffer.edit([(len..len, message)], None, cx);
+            if !has_newline {
+                let len = buffer.len();
+                buffer.edit([(len..len, "\n")], None, cx);
+            }
+        });
+        cx.notify();
+        Some(())
+    }
+
+    fn remove_language_server(
+        &mut self,
+        project: &ModelHandle<Project>,
+        id: LanguageServerId,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<()> {
+        let project_state = self.projects.get_mut(&project.downgrade())?;
+        project_state.servers.remove(&id);
+        cx.notify();
+        Some(())
+    }
+
+    pub fn log_buffer_for_server(
         &self,
         project: &ModelHandle<Project>,
         server_id: LanguageServerId,
-    ) -> bool {
-        self.projects
-            .get(&project.downgrade())
-            .map_or(false, |store| store.servers.contains_key(&server_id))
+    ) -> Option<ModelHandle<Buffer>> {
+        let weak_project = project.downgrade();
+        let project_state = self.projects.get(&weak_project)?;
+        let server_state = project_state.servers.get(&server_id)?;
+        Some(server_state.log_buffer.clone())
     }
 
-    pub fn enable_logs_for_language_server(
+    pub fn enable_rpc_trace_for_language_server(
         &mut self,
         project: &ModelHandle<Project>,
         server_id: LanguageServerId,
         cx: &mut ModelContext<Self>,
     ) -> Option<ModelHandle<Buffer>> {
-        let server = project.read(cx).language_server_for_id(server_id)?;
         let weak_project = project.downgrade();
-        let project_logs = match self.projects.entry(weak_project) {
-            hash_map::Entry::Occupied(entry) => entry.into_mut(),
-            hash_map::Entry::Vacant(entry) => entry.insert(LogStoreProject {
-                servers: HashMap::default(),
-                _subscription: cx.observe_release(&project, move |this, _, _| {
-                    this.projects.remove(&weak_project);
-                }),
-            }),
-        };
-        let server_log_state = project_logs.servers.entry(server_id).or_insert_with(|| {
+        let project_state = self.projects.get_mut(&weak_project)?;
+        let server_state = project_state.servers.get_mut(&server_id)?;
+        let server = project.read(cx).language_server_for_id(server_id)?;
+        let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
             let io_tx = self.io_tx.clone();
             let language = project.read(cx).languages().language_for_name("JSON");
             let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
@@ -142,33 +255,30 @@ impl LogStore {
             })
             .detach();
 
-            let project = project.downgrade();
-            LogStoreLanguageServer {
+            LanguageServerRpcState {
                 buffer,
                 last_message_kind: None,
                 _subscription: server.on_io(move |is_received, json| {
                     io_tx
-                        .unbounded_send((project, server_id, is_received, json.to_string()))
+                        .unbounded_send((weak_project, server_id, is_received, json.to_string()))
                         .ok();
                 }),
             }
         });
-        Some(server_log_state.buffer.clone())
+        Some(rpc_state.buffer.clone())
     }
 
-    pub fn disable_logs_for_language_server(
+    pub fn disable_rpc_trace_for_language_server(
         &mut self,
         project: &ModelHandle<Project>,
         server_id: LanguageServerId,
         _: &mut ModelContext<Self>,
-    ) {
+    ) -> Option<()> {
         let project = project.downgrade();
-        if let Some(store) = self.projects.get_mut(&project) {
-            store.servers.remove(&server_id);
-            if store.servers.is_empty() {
-                self.projects.remove(&project);
-            }
-        }
+        let project_state = self.projects.get_mut(&project)?;
+        let server_state = project_state.servers.get_mut(&server_id)?;
+        server_state.rpc_state.take();
+        Some(())
     }
 
     fn on_io(
@@ -183,7 +293,9 @@ impl LogStore {
             .projects
             .get_mut(&project)?
             .servers
-            .get_mut(&language_server_id)?;
+            .get_mut(&language_server_id)?
+            .rpc_state
+            .as_mut()?;
         state.buffer.update(cx, |buffer, cx| {
             let kind = if is_received {
                 MessageKind::Receive
@@ -209,23 +321,83 @@ impl LogStore {
 impl LspLogView {
     fn new(
         project: ModelHandle<Project>,
-        log_set: ModelHandle<LogStore>,
-        _: &mut ViewContext<Self>,
+        log_store: ModelHandle<LogStore>,
+        cx: &mut ViewContext<Self>,
     ) -> Self {
-        Self {
+        let server_id = log_store
+            .read(cx)
+            .projects
+            .get(&project.downgrade())
+            .and_then(|project| project.servers.keys().copied().next());
+        let mut this = Self {
             project,
-            log_store: log_set,
+            log_store,
             editor: None,
             current_server_id: None,
+            is_showing_rpc_trace: false,
+        };
+        if let Some(server_id) = server_id {
+            this.show_logs_for_server(server_id, cx);
         }
+        this
+    }
+
+    fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
+        let log_store = self.log_store.read(cx);
+        let state = log_store.projects.get(&self.project.downgrade())?;
+        let mut rows = self
+            .project
+            .read(cx)
+            .language_servers()
+            .filter_map(|(server_id, language_server_name, worktree_id)| {
+                let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
+                let state = state.servers.get(&server_id)?;
+                Some(LogMenuItem {
+                    server_id,
+                    server_name: language_server_name,
+                    worktree,
+                    rpc_trace_enabled: state.rpc_state.is_some(),
+                    rpc_trace_selected: self.is_showing_rpc_trace
+                        && self.current_server_id == Some(server_id),
+                    logs_selected: !self.is_showing_rpc_trace
+                        && self.current_server_id == Some(server_id),
+                })
+            })
+            .collect::<Vec<_>>();
+        rows.sort_by_key(|row| row.server_id);
+        rows.dedup_by_key(|row| row.server_id);
+        Some(rows)
     }
 
     fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
+        let buffer = self
+            .log_store
+            .read(cx)
+            .log_buffer_for_server(&self.project, server_id);
+        if let Some(buffer) = buffer {
+            self.current_server_id = Some(server_id);
+            self.is_showing_rpc_trace = false;
+            self.editor = Some(cx.add_view(|cx| {
+                let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx);
+                editor.set_read_only(true);
+                editor.move_to_end(&Default::default(), cx);
+                editor
+            }));
+            cx.notify();
+        }
+    }
+
+    fn show_rpc_trace_for_server(
+        &mut self,
+        server_id: LanguageServerId,
+        cx: &mut ViewContext<Self>,
+    ) {
         let buffer = self.log_store.update(cx, |log_set, cx| {
-            log_set.enable_logs_for_language_server(&self.project, server_id, cx)
+            log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
         });
         if let Some(buffer) = buffer {
             self.current_server_id = Some(server_id);
+            self.is_showing_rpc_trace = true;
             self.editor = Some(cx.add_view(|cx| {
                 let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx);
                 editor.set_read_only(true);
@@ -236,7 +408,7 @@ impl LspLogView {
         }
     }
 
-    fn toggle_logging_for_server(
+    fn toggle_rpc_trace_for_server(
         &mut self,
         server_id: LanguageServerId,
         enabled: bool,
@@ -244,11 +416,15 @@ impl LspLogView {
     ) {
         self.log_store.update(cx, |log_store, cx| {
             if enabled {
-                log_store.enable_logs_for_language_server(&self.project, server_id, cx);
+                log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
             } else {
-                log_store.disable_logs_for_language_server(&self.project, server_id, cx);
+                log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
             }
         });
+        if !enabled && Some(server_id) == self.current_server_id {
+            self.show_logs_for_server(server_id, cx);
+            cx.notify();
+        }
     }
 }
 
@@ -305,28 +481,18 @@ impl View for LspLogToolbarItemView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         let theme = theme::current(cx).clone();
         let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
-        let project = self.project.read(cx);
         let log_view = log_view.read(cx);
-        let log_store = log_view.log_store.read(cx);
 
-        let mut language_servers = project
-            .language_servers()
-            .map(|(id, name, worktree)| {
-                (
-                    id,
-                    name,
-                    worktree,
-                    log_store.has_enabled_logs_for_language_server(&self.project, id),
-                )
-            })
-            .collect::<Vec<_>>();
-        language_servers.sort_by_key(|a| (a.0, a.2));
-        language_servers.dedup_by_key(|a| a.0);
+        let menu_rows = self
+            .log_view
+            .as_ref()
+            .and_then(|view| view.read(cx).menu_items(cx))
+            .unwrap_or_default();
 
         let current_server_id = log_view.current_server_id;
         let current_server = current_server_id.and_then(|current_server_id| {
-            if let Ok(ix) = language_servers.binary_search_by_key(&current_server_id, |e| e.0) {
-                Some(language_servers[ix].clone())
+            if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
+                Some(menu_rows[ix].clone())
             } else {
                 None
             }
@@ -337,7 +503,6 @@ impl View for LspLogToolbarItemView {
         Stack::new()
             .with_child(Self::render_language_server_menu_header(
                 current_server,
-                &self.project,
                 &theme,
                 cx,
             ))
@@ -346,22 +511,20 @@ impl View for LspLogToolbarItemView {
                     Overlay::new(
                         MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
                             Flex::column()
-                                .with_children(language_servers.into_iter().filter_map(
-                                    |(id, name, worktree_id, logging_enabled)| {
-                                        Self::render_language_server_menu_item(
-                                            id,
-                                            name,
-                                            worktree_id,
-                                            logging_enabled,
-                                            Some(id) == current_server_id,
-                                            &self.project,
-                                            &theme,
-                                            cx,
-                                        )
-                                    },
-                                ))
+                                .with_children(menu_rows.into_iter().map(|row| {
+                                    Self::render_language_server_menu_item(
+                                        row.server_id,
+                                        row.server_name,
+                                        row.worktree,
+                                        row.rpc_trace_enabled,
+                                        row.logs_selected,
+                                        row.rpc_trace_selected,
+                                        &theme,
+                                        cx,
+                                    )
+                                }))
                                 .contained()
-                                .with_style(theme.context_menu.container)
+                                .with_style(theme.lsp_log_menu.container)
                                 .constrained()
                                 .with_width(400.)
                                 .with_height(400.)
@@ -388,12 +551,14 @@ impl View for LspLogToolbarItemView {
     }
 }
 
+const RPC_MESSAGES: &str = "RPC Messages";
+const SERVER_LOGS: &str = "Server Logs";
+
 impl LspLogToolbarItemView {
-    pub fn new(project: ModelHandle<Project>) -> Self {
+    pub fn new() -> Self {
         Self {
             menu_open: false,
             log_view: None,
-            project,
         }
     }
 
@@ -410,10 +575,9 @@ impl LspLogToolbarItemView {
     ) {
         if let Some(log_view) = &self.log_view {
             log_view.update(cx, |log_view, cx| {
-                log_view.toggle_logging_for_server(id, enabled, cx);
+                log_view.toggle_rpc_trace_for_server(id, enabled, cx);
                 if !enabled && Some(id) == log_view.current_server_id {
-                    log_view.current_server_id = None;
-                    log_view.editor = None;
+                    log_view.show_logs_for_server(id, cx);
                     cx.notify();
                 }
             });
@@ -423,39 +587,49 @@ impl LspLogToolbarItemView {
 
     fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
         if let Some(log_view) = &self.log_view {
-            log_view.update(cx, |log_view, cx| {
-                log_view.show_logs_for_server(id, cx);
-            });
+            log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
+            self.menu_open = false;
+            cx.notify();
+        }
+    }
+
+    fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
+        if let Some(log_view) = &self.log_view {
+            log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
             self.menu_open = false;
+            cx.notify();
         }
-        cx.notify();
     }
 
     fn render_language_server_menu_header(
-        current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>,
-        project: &ModelHandle<Project>,
+        current_server: Option<LogMenuItem>,
         theme: &Arc<Theme>,
         cx: &mut ViewContext<Self>,
     ) -> impl Element<Self> {
         enum ToggleMenu {}
         MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
-            let project = project.read(cx);
             let label: Cow<str> = current_server
-                .and_then(|(_, server_name, worktree_id, _)| {
-                    let worktree = project.worktree_for_id(worktree_id, cx)?;
-                    let worktree = &worktree.read(cx);
-                    Some(format!("{} - ({})", server_name.0, worktree.root_name()).into())
+                .and_then(|row| {
+                    let worktree = row.worktree.read(cx);
+                    Some(
+                        format!(
+                            "{} ({}) - {}",
+                            row.server_name.0,
+                            worktree.root_name(),
+                            if row.rpc_trace_selected {
+                                RPC_MESSAGES
+                            } else {
+                                SERVER_LOGS
+                            },
+                        )
+                        .into(),
+                    )
                 })
                 .unwrap_or_else(|| "No server selected".into());
-            Label::new(
-                label,
-                theme
-                    .context_menu
-                    .item
-                    .style_for(state, false)
-                    .label
-                    .clone(),
-            )
+            let style = theme.lsp_log_menu.header.style_for(state, false);
+            Label::new(label, style.text.clone())
+                .contained()
+                .with_style(style.container)
         })
         .with_cursor_style(CursorStyle::PointingHand)
         .on_click(MouseButton::Left, move |_, view, cx| {
@@ -466,46 +640,75 @@ impl LspLogToolbarItemView {
     fn render_language_server_menu_item(
         id: LanguageServerId,
         name: LanguageServerName,
-        worktree_id: WorktreeId,
-        logging_enabled: bool,
-        is_selected: bool,
-        project: &ModelHandle<Project>,
+        worktree: ModelHandle<Worktree>,
+        rpc_trace_enabled: bool,
+        logs_selected: bool,
+        rpc_trace_selected: bool,
         theme: &Arc<Theme>,
         cx: &mut ViewContext<Self>,
-    ) -> Option<impl Element<Self>> {
+    ) -> impl Element<Self> {
         enum ActivateLog {}
-        let project = project.read(cx);
-        let worktree = project.worktree_for_id(worktree_id, cx)?;
-        let worktree = &worktree.read(cx);
-        if !worktree.is_visible() {
-            return None;
-        }
-        let label = format!("{} - ({})", name.0, worktree.root_name());
-
-        Some(
-            MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| {
-                let item_style = theme.context_menu.item.style_for(state, is_selected);
-                Flex::row()
-                    .with_child(ui::checkbox_with_label::<Self, _, Self, _>(
-                        Empty::new(),
-                        &theme.welcome.checkbox,
-                        logging_enabled,
-                        id.0,
-                        cx,
-                        move |this, enabled, cx| {
-                            this.toggle_logging_for_server(id, enabled, cx);
-                        },
-                    ))
-                    .with_child(Label::new(label, item_style.label.clone()).aligned().left())
-                    .align_children_center()
-                    .contained()
-                    .with_style(item_style.container)
+        enum ActivateRpcTrace {}
+
+        Flex::column()
+            .with_child({
+                let style = &theme.lsp_log_menu.server;
+                Label::new(
+                    format!("{} ({})", name.0, worktree.read(cx).root_name()),
+                    style.text.clone(),
+                )
+                .contained()
+                .with_style(style.container)
+                .constrained()
+                .with_height(theme.lsp_log_menu.row_height)
             })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, view, cx| {
-                view.show_logs_for_server(id, cx);
-            }),
-        )
+            .with_child(
+                MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, _| {
+                    let style = theme.lsp_log_menu.item.style_for(state, logs_selected);
+                    Label::new(SERVER_LOGS, style.text.clone())
+                        .contained()
+                        .with_style(style.container)
+                        .constrained()
+                        .with_height(theme.lsp_log_menu.row_height)
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, view, cx| {
+                    view.show_logs_for_server(id, cx);
+                }),
+            )
+            .with_child(
+                MouseEventHandler::<ActivateRpcTrace, _>::new(id.0, cx, move |state, cx| {
+                    let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_selected);
+                    Flex::row()
+                        .with_child(
+                            Label::new(RPC_MESSAGES, style.text.clone())
+                                .constrained()
+                                .with_height(theme.lsp_log_menu.row_height),
+                        )
+                        .with_child(
+                            ui::checkbox_with_label::<Self, _, Self, _>(
+                                Empty::new(),
+                                &theme.welcome.checkbox,
+                                rpc_trace_enabled,
+                                id.0,
+                                cx,
+                                move |this, enabled, cx| {
+                                    this.toggle_logging_for_server(id, enabled, cx);
+                                },
+                            )
+                            .flex_float(),
+                        )
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.container)
+                        .constrained()
+                        .with_height(theme.lsp_log_menu.row_height)
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, view, cx| {
+                    view.show_rpc_trace_for_server(id, cx);
+                }),
+            )
     }
 }
 

crates/lsp_log/src/lsp_log_tests.rs 🔗

@@ -0,0 +1,97 @@
+use super::*;
+use gpui::{serde_json::json, TestAppContext};
+use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig};
+use project::FakeFs;
+use settings::SettingsStore;
+
+#[gpui::test]
+async fn test_lsp_logs(cx: &mut TestAppContext) {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+
+    init_test(cx);
+
+    let mut rust_language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_rust_servers = rust_language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "the-rust-language-server",
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            "test.rs": "",
+            "package.json": "",
+        }),
+    )
+    .await;
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+    project.update(cx, |project, _| {
+        project.languages().add(Arc::new(rust_language));
+    });
+
+    let log_store = cx.add_model(|cx| LogStore::new(cx));
+    log_store.update(cx, |store, cx| store.add_project(&project, cx));
+
+    let _rust_buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/the-root/test.rs", cx)
+        })
+        .await
+        .unwrap();
+
+    let mut language_server = fake_rust_servers.next().await.unwrap();
+    language_server
+        .receive_notification::<lsp::notification::DidOpenTextDocument>()
+        .await;
+
+    let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx));
+
+    language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
+        message: "hello from the server".into(),
+        typ: lsp::MessageType::INFO,
+    });
+    cx.foreground().run_until_parked();
+
+    log_view.read_with(cx, |view, cx| {
+        assert_eq!(
+            view.menu_items(cx).unwrap(),
+            &[LogMenuItem {
+                server_id: language_server.server.server_id(),
+                server_name: LanguageServerName("the-rust-language-server".into()),
+                worktree: project.read(cx).worktrees(cx).next().unwrap(),
+                rpc_trace_enabled: false,
+                rpc_trace_selected: false,
+                logs_selected: true,
+            }]
+        );
+        assert_eq!(
+            view.editor.as_ref().unwrap().read(cx).text(cx),
+            "hello from the server\n"
+        );
+    });
+}
+
+fn init_test(cx: &mut gpui::TestAppContext) {
+    cx.foreground().forbid_parking();
+
+    cx.update(|cx| {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+        language::init(cx);
+        client::init_settings(cx);
+        Project::init_settings(cx);
+        editor::init_settings(cx);
+    });
+}

crates/project/src/project.rs 🔗

@@ -245,10 +245,11 @@ pub struct Collaborator {
     pub replica_id: ReplicaId,
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Event {
     LanguageServerAdded(LanguageServerId),
     LanguageServerRemoved(LanguageServerId),
+    LanguageServerLog(LanguageServerId, String),
     ActiveEntryChanged(Option<ProjectEntryId>),
     WorktreeAdded,
     WorktreeRemoved(WorktreeId),
@@ -2454,18 +2455,23 @@ impl Project {
         LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
             let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
             let language_server = pending_server.task.await.log_err()?;
-            let language_server = language_server
-                .initialize(initialization_options)
-                .await
-                .log_err()?;
-            let this = this.upgrade(&cx)?;
+
+            language_server
+                .on_notification::<lsp::notification::LogMessage, _>({
+                    move |params, mut cx| {
+                        if let Some(this) = this.upgrade(&cx) {
+                            this.update(&mut cx, |_, cx| {
+                                cx.emit(Event::LanguageServerLog(server_id, params.message))
+                            });
+                        }
+                    }
+                })
+                .detach();
 
             language_server
                 .on_notification::<lsp::notification::PublishDiagnostics, _>({
-                    let this = this.downgrade();
                     let adapter = adapter.clone();
                     move |mut params, cx| {
-                        let this = this;
                         let adapter = adapter.clone();
                         cx.spawn(|mut cx| async move {
                             adapter.process_diagnostics(&mut params).await;
@@ -2517,8 +2523,7 @@ impl Project {
             // avoid stalling any language server like `gopls` which waits for a response
             // to these requests when initializing.
             language_server
-                .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
-                    let this = this.downgrade();
+                .on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
                     move |params, mut cx| async move {
                         if let Some(this) = this.upgrade(&cx) {
                             this.update(&mut cx, |this, _| {
@@ -2532,12 +2537,11 @@ impl Project {
                             });
                         }
                         Ok(())
-                    }
-                })
+                    },
+                )
                 .detach();
             language_server
-                .on_request::<lsp::request::RegisterCapability, _, _>({
-                    let this = this.downgrade();
+                .on_request::<lsp::request::RegisterCapability, _, _>(
                     move |params, mut cx| async move {
                         let this = this
                             .upgrade(&cx)
@@ -2555,24 +2559,15 @@ impl Project {
                             }
                         }
                         Ok(())
-                    }
-                })
+                    },
+                )
                 .detach();
 
             language_server
                 .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
-                    let this = this.downgrade();
                     let adapter = adapter.clone();
-                    let language_server = language_server.clone();
                     move |params, cx| {
-                        Self::on_lsp_workspace_edit(
-                            this,
-                            params,
-                            server_id,
-                            adapter.clone(),
-                            language_server.clone(),
-                            cx,
-                        )
+                        Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
                     }
                 })
                 .detach();
@@ -2582,7 +2577,6 @@ impl Project {
 
             language_server
                 .on_notification::<lsp::notification::Progress, _>({
-                    let this = this.downgrade();
                     move |params, mut cx| {
                         if let Some(this) = this.upgrade(&cx) {
                             this.update(&mut cx, |this, cx| {
@@ -2598,6 +2592,10 @@ impl Project {
                 })
                 .detach();
 
+            let language_server = language_server
+                .initialize(initialization_options)
+                .await
+                .log_err()?;
             language_server
                 .notify::<lsp::notification::DidChangeConfiguration>(
                     lsp::DidChangeConfigurationParams {
@@ -2606,6 +2604,7 @@ impl Project {
                 )
                 .ok();
 
+            let this = this.upgrade(&cx)?;
             this.update(&mut cx, |this, cx| {
                 // If the language server for this key doesn't match the server id, don't store the
                 // server. Which will cause it to be dropped, killing the process
@@ -2640,6 +2639,8 @@ impl Project {
                     },
                 );
 
+                cx.emit(Event::LanguageServerAdded(server_id));
+
                 if let Some(project_id) = this.remote_id() {
                     this.client
                         .send(proto::StartLanguageServer {
@@ -2765,6 +2766,7 @@ impl Project {
             cx.notify();
 
             let server_state = self.language_servers.remove(&server_id);
+            cx.emit(Event::LanguageServerRemoved(server_id));
             cx.spawn_weak(|this, mut cx| async move {
                 let mut root_path = None;
 
@@ -3109,12 +3111,14 @@ impl Project {
         params: lsp::ApplyWorkspaceEditParams,
         server_id: LanguageServerId,
         adapter: Arc<CachedLspAdapter>,
-        language_server: Arc<LanguageServer>,
         mut cx: AsyncAppContext,
     ) -> Result<lsp::ApplyWorkspaceEditResponse> {
         let this = this
             .upgrade(&cx)
             .ok_or_else(|| anyhow!("project project closed"))?;
+        let language_server = this
+            .read_with(&cx, |this, _| this.language_server_for_id(server_id))
+            .ok_or_else(|| anyhow!("language server not found"))?;
         let transaction = Self::deserialize_workspace_edit(
             this.clone(),
             params.edit,

crates/project/src/project_tests.rs 🔗

@@ -826,6 +826,11 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     let mut events = subscribe(&project, cx);
 
     let fake_server = fake_servers.next().await.unwrap();
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::LanguageServerAdded(LanguageServerId(0)),
+    );
+
     fake_server
         .start_progress(format!("{}/0", progress_token))
         .await;
@@ -953,6 +958,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
 
     // Simulate the newly started server sending more diagnostics.
     let fake_server = fake_servers.next().await.unwrap();
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::LanguageServerAdded(LanguageServerId(1))
+    );
     fake_server.start_progress(progress_token).await;
     assert_eq!(
         events.next().await.unwrap(),

crates/theme/src/theme.rs 🔗

@@ -44,6 +44,7 @@ pub struct Theme {
     pub context_menu: ContextMenu,
     pub contacts_popover: ContactsPopover,
     pub contact_list: ContactList,
+    pub lsp_log_menu: LspLogMenu,
     pub copilot: Copilot,
     pub contact_finder: ContactFinder,
     pub project_panel: ProjectPanel,
@@ -244,6 +245,16 @@ pub struct ContactFinder {
     pub disabled_contact_button: IconButton,
 }
 
+#[derive(Deserialize, Default)]
+pub struct LspLogMenu {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    pub header: Interactive<ContainedText>,
+    pub server: ContainedText,
+    pub item: Interactive<ContainedText>,
+    pub row_height: f32,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct TabBar {
     #[serde(flatten)]

crates/workspace/src/workspace.rs 🔗

@@ -3500,7 +3500,7 @@ impl std::fmt::Debug for OpenPaths {
     }
 }
 
-pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
+pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
 
 pub fn activate_workspace_for_project(
     cx: &mut AsyncAppContext,

crates/zed/src/zed.rs 🔗

@@ -311,9 +311,8 @@ pub fn initialize_workspace(
                                 toolbar.add_item(submit_feedback_button, cx);
                                 let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
                                 toolbar.add_item(feedback_info_text, cx);
-                                let lsp_log_item = cx.add_view(|_| {
-                                    lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
-                                });
+                                let lsp_log_item =
+                                    cx.add_view(|_| lsp_log::LspLogToolbarItemView::new());
                                 toolbar.add_item(lsp_log_item, cx);
                             })
                         });

styles/src/styleTree/app.ts 🔗

@@ -17,6 +17,7 @@ import projectSharedNotification from "./projectSharedNotification"
 import tooltip from "./tooltip"
 import terminal from "./terminal"
 import contactList from "./contactList"
+import lspLogMenu from "./lspLogMenu"
 import incomingCallNotification from "./incomingCallNotification"
 import { ColorScheme } from "../theme/colorScheme"
 import feedback from "./feedback"
@@ -45,6 +46,7 @@ export default function app(colorScheme: ColorScheme): Object {
         contactsPopover: contactsPopover(colorScheme),
         contactFinder: contactFinder(colorScheme),
         contactList: contactList(colorScheme),
+        lspLogMenu: lspLogMenu(colorScheme),
         search: search(colorScheme),
         sharedScreen: sharedScreen(colorScheme),
         updateNotification: updateNotification(colorScheme),

styles/src/styleTree/lspLogMenu.ts 🔗

@@ -0,0 +1,42 @@
+import { ColorScheme } from "../theme/colorScheme"
+import { background, border, text } from "./components"
+
+export default function contactsPanel(colorScheme: ColorScheme) {
+    let layer = colorScheme.middle
+
+    return {
+        rowHeight: 30,
+        background: background(layer),
+        border: border(layer),
+        shadow: colorScheme.popoverShadow,
+        header: {
+            ...text(layer, "sans", { size: "sm" }),
+            padding: { left: 8, right: 8, top: 2, bottom: 2 },
+            cornerRadius: 6,
+            background: background(layer, "on"),
+            border: border(layer, "on", { overlay: true }),
+            hover: {
+                background: background(layer, "hovered"),
+                ...text(layer, "sans", "hovered", { size: "sm" }),
+            }
+        },
+        server: {
+            ...text(layer, "sans", { size: "sm" }),
+            padding: { left: 8, right: 8, top: 8, bottom: 8 },
+        },
+        item: {
+            ...text(layer, "sans", { size: "sm" }),
+            padding: { left: 18, right: 18, top: 2, bottom: 2 },
+            hover: {
+                background: background(layer, "hovered"),
+                ...text(layer, "sans", "hovered", { size: "sm" }),
+            },
+            active: {
+                background: background(layer, "active"),
+            },
+            activeHover: {
+                background: background(layer, "active"),
+            },
+        },
+    }
+}