language_tools: Split LSP log view selector into two (#21742)

Piotr Osiewicz created

This should make it easier to interact with LSP log view when there are
multiple language servers. I often find the current UI clunky when I
have over 5 servers running (which isn't uncommon with multiple projects
open)


https://github.com/user-attachments/assets/2ecaf17f-4b40-4c8f-aa6f-03b437a3d979


Closes #ISSUE

Release Notes:

- N/A

Change summary

crates/language_tools/src/lsp_log.rs | 388 ++++++++++++++++-------------
1 file changed, 215 insertions(+), 173 deletions(-)

Detailed changes

crates/language_tools/src/lsp_log.rs 🔗

@@ -1145,19 +1145,28 @@ impl Render for LspLogToolbarItemView {
                 None
             }
         });
-
+        let available_language_servers: Vec<_> = menu_rows
+            .iter()
+            .map(|row| {
+                (
+                    row.server_id,
+                    row.server_name.clone(),
+                    row.worktree_root_name.clone(),
+                    row.selected_entry,
+                )
+            })
+            .collect();
         let log_toolbar_view = cx.view().clone();
         let lsp_menu = PopoverMenu::new("LspLogView")
             .anchor(AnchorCorner::TopLeft)
             .trigger(Button::new(
                 "language_server_menu_header",
                 current_server
+                    .as_ref()
                     .map(|row| {
                         Cow::Owned(format!(
-                            "{} ({}) - {}",
-                            row.server_name.0,
-                            row.worktree_root_name,
-                            row.selected_entry.label()
+                            "{} ({})",
+                            row.server_name.0, row.worktree_root_name,
                         ))
                     })
                     .unwrap_or_else(|| "No server selected".into()),
@@ -1165,36 +1174,71 @@ impl Render for LspLogToolbarItemView {
             .menu({
                 let log_view = log_view.clone();
                 move |cx| {
-                    let menu_rows = menu_rows.clone();
                     let log_view = log_view.clone();
-                    let log_toolbar_view = log_toolbar_view.clone();
-                    ContextMenu::build(cx, move |mut menu, cx| {
-                        for (ix, row) in menu_rows.into_iter().enumerate() {
-                            let server_selected = Some(row.server_id) == current_server_id;
-                            menu = menu
-                                .header(format!(
-                                    "{} ({})",
-                                    row.server_name.0, row.worktree_root_name
-                                ))
-                                .entry(
-                                    SERVER_LOGS,
-                                    None,
-                                    cx.handler_for(&log_view, move |view, cx| {
-                                        view.show_logs_for_server(row.server_id, cx);
-                                    }),
-                                );
-                            // We do not support tracing for remote language servers right now
-                            if row.server_kind.is_remote() {
-                                continue;
-                            }
+                    ContextMenu::build(cx, |mut menu, cx| {
+                        for (server_id, name, worktree_root, active_entry_kind) in
+                            available_language_servers.iter()
+                        {
+                            let label = format!("{} ({})", name, worktree_root);
+                            let server_id = *server_id;
+                            let active_entry_kind = *active_entry_kind;
                             menu = menu.entry(
-                                SERVER_TRACE,
+                                label,
                                 None,
                                 cx.handler_for(&log_view, move |view, cx| {
-                                    view.show_trace_for_server(row.server_id, cx);
+                                    view.current_server_id = Some(server_id);
+                                    view.active_entry_kind = active_entry_kind;
+                                    match view.active_entry_kind {
+                                        LogKind::Rpc => {
+                                            view.toggle_rpc_trace_for_server(server_id, true, cx);
+                                            view.show_rpc_trace_for_server(server_id, cx);
+                                        }
+                                        LogKind::Trace => view.show_trace_for_server(server_id, cx),
+                                        LogKind::Logs => view.show_logs_for_server(server_id, cx),
+                                        LogKind::Capabilities => {
+                                            view.show_capabilities_for_server(server_id, cx)
+                                        }
+                                    }
+                                    cx.notify();
                                 }),
                             );
-                            menu = menu.custom_entry(
+                        }
+                        menu
+                    })
+                    .into()
+                }
+            });
+        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")
+                .anchor(AnchorCorner::TopLeft)
+                .trigger(Button::new(
+                    "language_server_menu_header",
+                    server.selected_entry.label(),
+                ))
+                .menu(move |cx| {
+                    let log_toolbar_view = log_toolbar_view.clone();
+                    let log_view = log_view.clone();
+                    Some(ContextMenu::build(cx, move |this, cx| {
+                        this.entry(
+                            SERVER_LOGS,
+                            None,
+                            cx.handler_for(&log_view, move |view, cx| {
+                                view.show_logs_for_server(server_id, cx);
+                            }),
+                        )
+                        .when(!is_remote, |this| {
+                            this.entry(
+                                SERVER_TRACE,
+                                None,
+                                cx.handler_for(&log_view, move |view, cx| {
+                                    view.show_trace_for_server(server_id, cx);
+                                }),
+                            )
+                            .custom_entry(
                                 {
                                     let log_toolbar_view = log_toolbar_view.clone();
                                     move |cx| {
@@ -1205,8 +1249,8 @@ impl Render for LspLogToolbarItemView {
                                             .child(
                                                 div().child(
                                                     Checkbox::new(
-                                                        ix,
-                                                        if row.rpc_trace_enabled {
+                                                        "LspLogEnableRpcTrace",
+                                                        if rpc_trace_enabled {
                                                             Selection::Selected
                                                         } else {
                                                             Selection::Unselected
@@ -1220,9 +1264,7 @@ impl Render for LspLogToolbarItemView {
                                                                 Selection::Selected
                                                             );
                                                             view.toggle_rpc_logging_for_server(
-                                                                row.server_id,
-                                                                enabled,
-                                                                cx,
+                                                                server_id, enabled, cx,
                                                             );
                                                             cx.stop_propagation();
                                                         },
@@ -1233,167 +1275,167 @@ impl Render for LspLogToolbarItemView {
                                     }
                                 },
                                 cx.handler_for(&log_view, move |view, cx| {
-                                    view.show_rpc_trace_for_server(row.server_id, cx);
-                                }),
-                            );
-                            if server_selected && row.selected_entry == LogKind::Rpc {
-                                let selected_ix = menu.select_last();
-                                // Each language server has:
-                                // 1. A title.
-                                // 2. Server logs.
-                                // 3. Server trace.
-                                // 4. RPC messages.
-                                // 5. Server capabilities
-                                // Thus, if nth server's RPC is selected, the index of selected entry should match this formula
-                                let _expected_index = ix * 5 + 3;
-                                debug_assert_eq!(
-                                    Some(_expected_index),
-                                    selected_ix,
-                                    "Could not scroll to a just added LSP menu item"
-                                );
-                            }
-                            menu = menu.entry(
-                                SERVER_CAPABILITIES,
-                                None,
-                                cx.handler_for(&log_view, move |view, cx| {
-                                    view.show_capabilities_for_server(row.server_id, cx);
+                                    view.show_rpc_trace_for_server(server_id, cx);
                                 }),
-                            );
-                        }
-                        menu
-                    })
-                    .into()
-                }
-            });
-
+                            )
+                        })
+                        .entry(
+                            SERVER_CAPABILITIES,
+                            None,
+                            cx.handler_for(&log_view, move |view, cx| {
+                                view.show_capabilities_for_server(server_id, cx);
+                            }),
+                        )
+                    }))
+                })
+        });
         h_flex()
             .size_full()
-            .child(lsp_menu)
+            .justify_between()
             .child(
-                div()
-                    .child(
-                        Button::new("clear_log_button", "Clear").on_click(cx.listener(
-                            |this, _, cx| {
-                                if let Some(log_view) = this.log_view.as_ref() {
-                                    log_view.update(cx, |log_view, cx| {
-                                        log_view.editor.update(cx, |editor, cx| {
-                                            editor.set_read_only(false);
-                                            editor.clear(cx);
-                                            editor.set_read_only(true);
-                                        });
-                                    })
-                                }
-                            },
-                        )),
-                    )
-                    .ml_2(),
-            )
-            .child(log_view.update(cx, |this, _| match this.active_entry_kind {
-                LogKind::Trace => {
-                    let log_view = log_view.clone();
-                    div().child(
-                        PopoverMenu::new("lsp-trace-level-menu")
-                            .anchor(AnchorCorner::TopLeft)
-                            .trigger(Button::new(
-                                "language_server_trace_level_selector",
-                                "Trace level",
-                            ))
-                            .menu({
-                                let log_view = log_view.clone();
-
-                                move |cx| {
-                                    let id = log_view.read(cx).current_server_id?;
-
-                                    let trace_level = log_view.update(cx, |this, cx| {
-                                        this.log_store.update(cx, |this, _| {
-                                            Some(this.get_language_server_state(id)?.trace_level)
-                                        })
-                                    })?;
-
-                                    ContextMenu::build(cx, |mut menu, _| {
+                h_flex()
+                    .child(lsp_menu)
+                    .children(view_selector)
+                    .child(log_view.update(cx, |this, _| match this.active_entry_kind {
+                        LogKind::Trace => {
+                            let log_view = log_view.clone();
+                            div().child(
+                                PopoverMenu::new("lsp-trace-level-menu")
+                                    .anchor(AnchorCorner::TopLeft)
+                                    .trigger(Button::new(
+                                        "language_server_trace_level_selector",
+                                        "Trace level",
+                                    ))
+                                    .menu({
                                         let log_view = log_view.clone();
 
-                                        for (option, label) in [
-                                            (TraceValue::Off, "Off"),
-                                            (TraceValue::Messages, "Messages"),
-                                            (TraceValue::Verbose, "Verbose"),
-                                        ] {
-                                            menu = menu.entry(label, None, {
+                                        move |cx| {
+                                            let id = log_view.read(cx).current_server_id?;
+
+                                            let trace_level = log_view.update(cx, |this, cx| {
+                                                this.log_store.update(cx, |this, _| {
+                                                    Some(
+                                                        this.get_language_server_state(id)?
+                                                            .trace_level,
+                                                    )
+                                                })
+                                            })?;
+
+                                            ContextMenu::build(cx, |mut menu, _| {
                                                 let log_view = log_view.clone();
-                                                move |cx| {
-                                                    log_view.update(cx, |this, cx| {
-                                                        if let Some(id) = this.current_server_id {
-                                                            this.update_trace_level(id, option, cx);
+
+                                                for (option, label) in [
+                                                    (TraceValue::Off, "Off"),
+                                                    (TraceValue::Messages, "Messages"),
+                                                    (TraceValue::Verbose, "Verbose"),
+                                                ] {
+                                                    menu = menu.entry(label, None, {
+                                                        let log_view = log_view.clone();
+                                                        move |cx| {
+                                                            log_view.update(cx, |this, cx| {
+                                                                if let Some(id) =
+                                                                    this.current_server_id
+                                                                {
+                                                                    this.update_trace_level(
+                                                                        id, option, cx,
+                                                                    );
+                                                                }
+                                                            });
                                                         }
                                                     });
+                                                    if option == trace_level {
+                                                        menu.select_last();
+                                                    }
                                                 }
-                                            });
-                                            if option == trace_level {
-                                                menu.select_last();
-                                            }
+
+                                                menu
+                                            })
+                                            .into()
                                         }
+                                    }),
+                            )
+                        }
+                        LogKind::Logs => {
+                            let log_view = log_view.clone();
+                            div().child(
+                                PopoverMenu::new("lsp-log-level-menu")
+                                    .anchor(AnchorCorner::TopLeft)
+                                    .trigger(Button::new(
+                                        "language_server_log_level_selector",
+                                        "Log level",
+                                    ))
+                                    .menu({
+                                        let log_view = log_view.clone();
 
-                                        menu
-                                    })
-                                    .into()
-                                }
-                            }),
-                    )
-                }
-                LogKind::Logs => {
-                    let log_view = log_view.clone();
-                    div().child(
-                        PopoverMenu::new("lsp-log-level-menu")
-                            .anchor(AnchorCorner::TopLeft)
-                            .trigger(Button::new(
-                                "language_server_log_level_selector",
-                                "Log level",
-                            ))
-                            .menu({
-                                let log_view = log_view.clone();
-
-                                move |cx| {
-                                    let id = log_view.read(cx).current_server_id?;
-
-                                    let log_level = log_view.update(cx, |this, cx| {
-                                        this.log_store.update(cx, |this, _| {
-                                            Some(this.get_language_server_state(id)?.log_level)
-                                        })
-                                    })?;
+                                        move |cx| {
+                                            let id = log_view.read(cx).current_server_id?;
 
-                                    ContextMenu::build(cx, |mut menu, _| {
-                                        let log_view = log_view.clone();
+                                            let log_level = log_view.update(cx, |this, cx| {
+                                                this.log_store.update(cx, |this, _| {
+                                                    Some(
+                                                        this.get_language_server_state(id)?
+                                                            .log_level,
+                                                    )
+                                                })
+                                            })?;
 
-                                        for (option, label) in [
-                                            (MessageType::LOG, "Log"),
-                                            (MessageType::INFO, "Info"),
-                                            (MessageType::WARNING, "Warning"),
-                                            (MessageType::ERROR, "Error"),
-                                        ] {
-                                            menu = menu.entry(label, None, {
+                                            ContextMenu::build(cx, |mut menu, _| {
                                                 let log_view = log_view.clone();
-                                                move |cx| {
-                                                    log_view.update(cx, |this, cx| {
-                                                        if let Some(id) = this.current_server_id {
-                                                            this.update_log_level(id, option, cx);
+
+                                                for (option, label) in [
+                                                    (MessageType::LOG, "Log"),
+                                                    (MessageType::INFO, "Info"),
+                                                    (MessageType::WARNING, "Warning"),
+                                                    (MessageType::ERROR, "Error"),
+                                                ] {
+                                                    menu = menu.entry(label, None, {
+                                                        let log_view = log_view.clone();
+                                                        move |cx| {
+                                                            log_view.update(cx, |this, cx| {
+                                                                if let Some(id) =
+                                                                    this.current_server_id
+                                                                {
+                                                                    this.update_log_level(
+                                                                        id, option, cx,
+                                                                    );
+                                                                }
+                                                            });
                                                         }
                                                     });
+                                                    if option == log_level {
+                                                        menu.select_last();
+                                                    }
                                                 }
-                                            });
-                                            if option == log_level {
-                                                menu.select_last();
-                                            }
-                                        }
 
-                                        menu
+                                                menu
+                                            })
+                                            .into()
+                                        }
+                                    }),
+                            )
+                        }
+                        _ => div(),
+                    })),
+            )
+            .child(
+                div()
+                    .child(
+                        Button::new("clear_log_button", "Clear").on_click(cx.listener(
+                            |this, _, cx| {
+                                if let Some(log_view) = this.log_view.as_ref() {
+                                    log_view.update(cx, |log_view, cx| {
+                                        log_view.editor.update(cx, |editor, cx| {
+                                            editor.set_read_only(false);
+                                            editor.clear(cx);
+                                            editor.set_read_only(true);
+                                        });
                                     })
-                                    .into()
                                 }
-                            }),
+                            },
+                        )),
                     )
-                }
-                _ => div(),
-            }))
+                    .ml_2(),
+            )
     }
 }