Style new lsp log menu, add a test for it

Max Brunsfeld created

Change summary

Cargo.lock                          |  2 
crates/lsp_log/Cargo.toml           |  2 
crates/lsp_log/src/lsp_log.rs       | 94 +++++++++++++++++------------
crates/lsp_log/src/lsp_log_tests.rs | 97 +++++++++++++++++++++++++++++++
crates/theme/src/theme.rs           | 10 +++
styles/src/styleTree/app.ts         |  2 
styles/src/styleTree/lspLogMenu.ts  | 41 +++++++++++++
7 files changed, 210 insertions(+), 38 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3763,8 +3763,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_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,3 +1,6 @@
+#[cfg(test)]
+mod lsp_log_tests;
+
 use collections::HashMap;
 use editor::Editor;
 use futures::{channel::mpsc, StreamExt};
@@ -521,7 +524,7 @@ impl View for LspLogToolbarItemView {
                                     )
                                 }))
                                 .contained()
-                                .with_style(theme.context_menu.container)
+                                .with_style(theme.lsp_log_menu.container)
                                 .constrained()
                                 .with_width(400.)
                                 .with_height(400.)
@@ -548,6 +551,9 @@ impl View for LspLogToolbarItemView {
     }
 }
 
+const RPC_MESSAGES: &str = "RPC Messages";
+const SERVER_LOGS: &str = "Server Logs";
+
 impl LspLogToolbarItemView {
     pub fn new() -> Self {
         Self {
@@ -605,18 +611,25 @@ impl LspLogToolbarItemView {
             let label: Cow<str> = current_server
                 .and_then(|row| {
                     let worktree = row.worktree.read(cx);
-                    Some(format!("{} - ({})", row.server_name.0, worktree.root_name()).into())
+                    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| {
@@ -628,7 +641,7 @@ impl LspLogToolbarItemView {
         id: LanguageServerId,
         name: LanguageServerName,
         worktree: ModelHandle<Worktree>,
-        logging_enabled: bool,
+        rpc_trace_enabled: bool,
         logs_selected: bool,
         rpc_trace_selected: bool,
         theme: &Arc<Theme>,
@@ -637,23 +650,25 @@ impl LspLogToolbarItemView {
         enum ActivateLog {}
         enum ActivateRpcTrace {}
 
-        let header = format!("{} - ({})", name.0, worktree.read(cx).root_name());
-
-        let item_style = &theme.context_menu.item.default;
         Flex::column()
-            .with_child(
-                Label::new(header, item_style.label.clone())
-                    .aligned()
-                    .left(),
-            )
+            .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)
+                .aligned()
+                .left()
+            })
             .with_child(
                 MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, _| {
-                    let item_style = &theme.context_menu.item.style_for(state, logs_selected);
-                    Label::new("logs", item_style.label.clone())
-                        .aligned()
-                        .left()
+                    let style = theme.lsp_log_menu.item.style_for(state, logs_selected);
+                    Flex::row()
+                        .with_child(Label::new(SERVER_LOGS, style.text.clone()).aligned().left())
                         .contained()
-                        .with_style(item_style.container)
+                        .with_style(style.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, view, cx| {
@@ -662,26 +677,29 @@ impl LspLogToolbarItemView {
             )
             .with_child(
                 MouseEventHandler::<ActivateRpcTrace, _>::new(id.0, cx, move |state, cx| {
-                    let item_style = &theme.context_menu.item.style_for(state, rpc_trace_selected);
+                    let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_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("rpc trace", item_style.label.clone())
+                            Label::new(RPC_MESSAGES, style.text.clone())
                                 .aligned()
                                 .left(),
                         )
+                        .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(item_style.container)
+                        .with_style(style.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, view, 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/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,15 @@ 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>,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct TabBar {
     #[serde(flatten)]

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 "../themes/common/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,41 @@
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
+
+export default function contactsPanel(colorScheme: ColorScheme) {
+    let layer = colorScheme.middle
+
+    return {
+        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"),
+            },
+        },
+    }
+}