Add LSP logs clear button (#2913)

Kirill Bulatov created

LSP logs tend to accumulate and hinder performance (e.g. search is
slower over 20 MB of json files).
Add a way to clear them.

Release Notes:

- N/A

Change summary

crates/language_tools/src/lsp_log.rs | 55 ++++++++++++++++++++++++++---
crates/theme/src/theme.rs            |  1 
styles/src/component/text_button.ts  |  6 +++
styles/src/style_tree/toolbar.ts     | 22 +++++++++++
4 files changed, 76 insertions(+), 8 deletions(-)

Detailed changes

crates/language_tools/src/lsp_log.rs 🔗

@@ -570,10 +570,12 @@ impl View for LspLogToolbarItemView {
         let Some(log_view) = self.log_view.as_ref() else {
             return Empty::new().into_any();
         };
-        let log_view = log_view.read(cx);
-        let menu_rows = log_view.menu_items(cx).unwrap_or_default();
+        let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
+            let menu_rows = log_view.menu_items(cx).unwrap_or_default();
+            let current_server_id = log_view.current_server_id;
+            (menu_rows, current_server_id)
+        });
 
-        let current_server_id = log_view.current_server_id;
         let current_server = current_server_id.and_then(|current_server_id| {
             if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
                 Some(menu_rows[ix].clone())
@@ -581,10 +583,10 @@ impl View for LspLogToolbarItemView {
                 None
             }
         });
+        let server_selected = current_server.is_some();
 
         enum Menu {}
-
-        Stack::new()
+        let lsp_menu = Stack::new()
             .with_child(Self::render_language_server_menu_header(
                 current_server,
                 &theme,
@@ -631,8 +633,47 @@ impl View for LspLogToolbarItemView {
             })
             .aligned()
             .left()
-            .clipped()
-            .into_any()
+            .clipped();
+
+        enum LspCleanupButton {}
+        let log_cleanup_button =
+            MouseEventHandler::new::<LspCleanupButton, _>(1, cx, |state, cx| {
+                let theme = theme::current(cx).clone();
+                let style = theme
+                    .workspace
+                    .toolbar
+                    .toggleable_text_tool
+                    .in_state(server_selected)
+                    .style_for(state);
+                Label::new("Clear", style.text.clone())
+                    .aligned()
+                    .contained()
+                    .with_style(style.container)
+                    .constrained()
+                    .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0)
+            })
+            .on_click(MouseButton::Left, move |_, 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);
+                        });
+                    })
+                }
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .aligned()
+            .right();
+
+        Flex::row()
+            .with_child(lsp_menu)
+            .with_child(log_cleanup_button)
+            .contained()
+            .aligned()
+            .left()
+            .into_any_named("lsp log controls")
     }
 }
 

crates/theme/src/theme.rs 🔗

@@ -408,6 +408,7 @@ pub struct Toolbar {
     pub height: f32,
     pub item_spacing: f32,
     pub toggleable_tool: Toggleable<Interactive<IconButton>>,
+    pub toggleable_text_tool: Toggleable<Interactive<ContainedText>>,
     pub breadcrumb_height: f32,
     pub breadcrumbs: Interactive<ContainedText>,
 }

styles/src/component/text_button.ts 🔗

@@ -1,5 +1,6 @@
 import { interactive, toggleable } from "../element"
 import {
+    Border,
     TextProperties,
     background,
     foreground,
@@ -16,6 +17,7 @@ interface TextButtonOptions {
     margin?: Partial<Margin>
     disabled?: boolean
     text_properties?: TextProperties
+    border?: Border
 }
 
 type ToggleableTextButtonOptions = TextButtonOptions & {
@@ -29,6 +31,7 @@ export function text_button({
     margin,
     disabled,
     text_properties,
+    border,
 }: TextButtonOptions = {}) {
     const theme = useTheme()
     if (!color) color = "base"
@@ -66,6 +69,7 @@ export function text_button({
         },
         state: {
             default: {
+                border,
                 background: background_color,
                 color: disabled
                     ? foreground(layer ?? theme.lowest, "disabled")
@@ -74,6 +78,7 @@ export function text_button({
             hovered: disabled
                 ? {}
                 : {
+                    border,
                     background: background(
                         layer ?? theme.lowest,
                         color,
@@ -88,6 +93,7 @@ export function text_button({
             clicked: disabled
                 ? {}
                 : {
+                    border,
                     background: background(
                         layer ?? theme.lowest,
                         color,

styles/src/style_tree/toolbar.ts 🔗

@@ -1,7 +1,8 @@
 import { useTheme } from "../common"
 import { toggleable_icon_button } from "../component/icon_button"
-import { interactive } from "../element"
+import { interactive, toggleable } from "../element"
 import { background, border, foreground, text } from "./components"
+import { text_button } from "../component";
 
 export const toolbar = () => {
     const theme = useTheme()
@@ -34,5 +35,24 @@ export const toolbar = () => {
                 },
             },
         }),
+        toggleable_text_tool: toggleable({
+            state: {
+                inactive: text_button({
+                    disabled: true,
+                    variant: "ghost",
+                    layer: theme.highest,
+                    margin: { left: 4 },
+                    text_properties: { size: "sm" },
+                    border: border(theme.middle),
+                }),
+                active: text_button({
+                    variant: "ghost",
+                    layer: theme.highest,
+                    margin: { left: 4 },
+                    text_properties: { size: "sm" },
+                    border: border(theme.middle),
+                }),
+            }
+        }),
     }
 }