lsp_log: Add server capabilities view (#19448)

Vitaly Slobodin created

Hello, this PR adds a new view to the LSP servers menu for
displaying an LSP server capabilities.

When I work on LSP stuff, quite often I need to check what capabilities
an LSP server has. Currently there is no built-in way for checking that
in Zed, and I have to use [`LSP
DevTools`](https://lsp-devtools.readthedocs.io) project. LSP DevTools
works OK but it works as a proxy between the client and the server, so
setting it up is not that easy in Zed. Zed already has many goodies for
LSP like tracing and RPC messages, so I thought that a simple view with
server capabilities could be useful too. Thanks!

## Some screenshots:

### Ruby LSP

![CleanShot 2024-10-19 at 07 44
38@2x](https://github.com/user-attachments/assets/22c97b49-c539-4e39-a5f1-1c926347abca)


### New menu entry:

![CleanShot 2024-10-19 at 07 45
08@2x](https://github.com/user-attachments/assets/d3903d6e-c09a-40e2-b042-1abde490987d)


Release Notes:

- N/A

Change summary

crates/language_tools/src/lsp_log.rs | 81 +++++++++++++++++++++++++++++
1 file changed, 79 insertions(+), 2 deletions(-)

Detailed changes

crates/language_tools/src/lsp_log.rs 🔗

@@ -9,7 +9,8 @@ use gpui::{
 };
 use language::{LanguageServerId, LanguageServerName};
 use lsp::{
-    notification::SetTrace, IoKind, LanguageServer, MessageType, SetTraceParams, TraceValue,
+    notification::SetTrace, IoKind, LanguageServer, MessageType, ServerCapabilities,
+    SetTraceParams, TraceValue,
 };
 use project::{search::SearchQuery, Project, WorktreeId};
 use std::{borrow::Cow, sync::Arc};
@@ -107,6 +108,7 @@ struct LanguageServerState {
     rpc_state: Option<LanguageServerRpcState>,
     trace_level: TraceValue,
     log_level: MessageType,
+    capabilities: ServerCapabilities,
     io_logs_subscription: Option<lsp::Subscription>,
 }
 
@@ -176,6 +178,7 @@ pub enum LogKind {
     Trace,
     #[default]
     Logs,
+    Capabilities,
 }
 
 impl LogKind {
@@ -184,6 +187,7 @@ impl LogKind {
             LogKind::Rpc => RPC_MESSAGES,
             LogKind::Trace => SERVER_TRACE,
             LogKind::Logs => SERVER_LOGS,
+            LogKind::Capabilities => SERVER_CAPABILITIES,
         }
     }
 }
@@ -374,6 +378,7 @@ impl LogStore {
                 trace_level: TraceValue::Off,
                 log_level: MessageType::LOG,
                 io_logs_subscription: None,
+                capabilities: ServerCapabilities::default(),
             }
         });
 
@@ -384,7 +389,10 @@ impl LogStore {
             server_state.worktree_id = Some(worktree_id);
         }
 
-        if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
+        if let Some(server) = server
+            .clone()
+            .filter(|_| server_state.io_logs_subscription.is_none())
+        {
             let io_tx = self.io_tx.clone();
             let server_id = server.server_id();
             server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
@@ -393,6 +401,11 @@ impl LogStore {
                     .ok();
             }));
         }
+
+        if let Some(server) = server {
+            server_state.capabilities = server.capabilities();
+        }
+
         Some(server_state)
     }
 
@@ -477,6 +490,10 @@ impl LogStore {
         Some(&self.language_servers.get(&server_id)?.trace_messages)
     }
 
+    fn server_capabilities(&self, server_id: LanguageServerId) -> Option<&ServerCapabilities> {
+        Some(&self.language_servers.get(&server_id)?.capabilities)
+    }
+
     fn server_ids_for_project<'a>(
         &'a self,
         lookup_project: &'a WeakModel<Project>,
@@ -602,6 +619,9 @@ impl LspLogView {
                             LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx),
                             LogKind::Trace => this.show_trace_for_server(server_id, cx),
                             LogKind::Logs => this.show_logs_for_server(server_id, cx),
+                            LogKind::Capabilities => {
+                                this.show_capabilities_for_server(server_id, cx)
+                            }
                         }
                     } else {
                         this.current_server_id = None;
@@ -618,6 +638,7 @@ impl LspLogView {
                     LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx),
                     LogKind::Trace => this.show_trace_for_server(server_id, cx),
                     LogKind::Logs => this.show_logs_for_server(server_id, cx),
+                    LogKind::Capabilities => this.show_capabilities_for_server(server_id, cx),
                 }
             }
 
@@ -695,6 +716,33 @@ impl LspLogView {
         (editor, vec![editor_subscription, search_subscription])
     }
 
+    fn editor_for_capabilities(
+        capabilities: ServerCapabilities,
+        cx: &mut ViewContext<Self>,
+    ) -> (View<Editor>, Vec<Subscription>) {
+        let editor = cx.new_view(|cx| {
+            let mut editor = Editor::multi_line(cx);
+            editor.set_text(serde_json::to_string_pretty(&capabilities).unwrap(), cx);
+            editor.move_to_end(&MoveToEnd, cx);
+            editor.set_read_only(true);
+            editor.set_show_inline_completions(Some(false), cx);
+            editor
+        });
+        let editor_subscription = cx.subscribe(
+            &editor,
+            |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| {
+                cx.emit(event.clone())
+            },
+        );
+        let search_subscription = cx.subscribe(
+            &editor,
+            |_, _, event: &SearchEvent, cx: &mut ViewContext<'_, LspLogView>| {
+                cx.emit(event.clone())
+            },
+        );
+        (editor, vec![editor_subscription, search_subscription])
+    }
+
     pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
         let log_store = self.log_store.read(cx);
 
@@ -881,6 +929,7 @@ impl LspLogView {
             cx.notify();
         }
     }
+
     fn update_trace_level(
         &self,
         server_id: LanguageServerId,
@@ -899,6 +948,25 @@ impl LspLogView {
                 .ok();
         }
     }
+
+    fn show_capabilities_for_server(
+        &mut self,
+        server_id: LanguageServerId,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let capabilities = self.log_store.read(cx).server_capabilities(server_id);
+
+        if let Some(capabilities) = capabilities {
+            self.current_server_id = Some(server_id);
+            self.active_entry_kind = LogKind::Capabilities;
+            let (editor, editor_subscriptions) =
+                Self::editor_for_capabilities(capabilities.clone(), cx);
+            self.editor = editor;
+            self.editor_subscriptions = editor_subscriptions;
+            cx.notify();
+        }
+        cx.focus(&self.focus_handle);
+    }
 }
 
 fn log_filter<T: Message>(line: &T, cmp: <T as Message>::Level) -> Option<&str> {
@@ -967,6 +1035,7 @@ impl Item for LspLogView {
                     LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, cx),
                     LogKind::Trace => new_view.show_trace_for_server(server_id, cx),
                     LogKind::Logs => new_view.show_logs_for_server(server_id, cx),
+                    LogKind::Capabilities => new_view.show_capabilities_for_server(server_id, cx),
                 }
             }
             new_view
@@ -1168,6 +1237,13 @@ impl Render for LspLogToolbarItemView {
                                     view.show_rpc_trace_for_server(row.server_id, cx);
                                 }),
                             );
+                            menu = menu.entry(
+                                SERVER_CAPABILITIES,
+                                None,
+                                cx.handler_for(&log_view, move |view, cx| {
+                                    view.show_capabilities_for_server(row.server_id, cx);
+                                }),
+                            );
                             if server_selected && row.selected_entry == LogKind::Rpc {
                                 let selected_ix = menu.select_last();
                                 debug_assert_eq!(
@@ -1317,6 +1393,7 @@ impl Render for LspLogToolbarItemView {
 const RPC_MESSAGES: &str = "RPC Messages";
 const SERVER_LOGS: &str = "Server Logs";
 const SERVER_TRACE: &str = "Server Trace";
+const SERVER_CAPABILITIES: &str = "Server Capabilities";
 
 impl Default for LspLogToolbarItemView {
     fn default() -> Self {