repl: Push button to clear outputs (#14873)

Kyle Kelley created

Change summary

crates/repl/src/outputs.rs       |  55 +++++++++-------
crates/repl/src/session.rs       | 111 ++++++++++++++++++++++++++++-----
crates/ui/src/components/icon.rs |  15 +++
3 files changed, 139 insertions(+), 42 deletions(-)

Detailed changes

crates/repl/src/outputs.rs 🔗

@@ -503,29 +503,38 @@ impl ExecutionView {
 impl Render for ExecutionView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         if self.outputs.len() == 0 {
-            return match &self.status {
-                ExecutionStatus::ConnectingToKernel => {
-                    div().child(Label::new("Connecting to kernel...").color(Color::Muted))
-                }
-                ExecutionStatus::Executing => {
-                    div().child(Label::new("Executing...").color(Color::Muted))
-                }
-                ExecutionStatus::Finished => div().child(Icon::new(IconName::Check)),
-                ExecutionStatus::Unknown => {
-                    div().child(div().child(Label::new("Unknown status").color(Color::Muted)))
-                }
-                ExecutionStatus::ShuttingDown => {
-                    div().child(Label::new("Kernel shutting down...").color(Color::Muted))
-                }
-                ExecutionStatus::Shutdown => {
-                    div().child(Label::new("Kernel shutdown").color(Color::Muted))
-                }
-                ExecutionStatus::Queued => div().child(Label::new("Queued").color(Color::Muted)),
-                ExecutionStatus::KernelErrored(error) => {
-                    div().child(Label::new(format!("Kernel error: {}", error)).color(Color::Error))
-                }
-            }
-            .into_any_element();
+            return v_flex()
+                .min_h(cx.line_height())
+                .justify_center()
+                .child(match &self.status {
+                    ExecutionStatus::ConnectingToKernel => Label::new("Connecting to kernel...")
+                        .color(Color::Muted)
+                        .into_any_element(),
+                    ExecutionStatus::Executing => Label::new("Executing...")
+                        .color(Color::Muted)
+                        .into_any_element(),
+                    ExecutionStatus::Finished => Icon::new(IconName::Check)
+                        .size(IconSize::Small)
+                        .into_any_element(),
+                    ExecutionStatus::Unknown => Label::new("Unknown status")
+                        .color(Color::Muted)
+                        .into_any_element(),
+                    ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...")
+                        .color(Color::Muted)
+                        .into_any_element(),
+                    ExecutionStatus::Shutdown => Label::new("Kernel shutdown")
+                        .color(Color::Muted)
+                        .into_any_element(),
+                    ExecutionStatus::Queued => {
+                        Label::new("Queued").color(Color::Muted).into_any_element()
+                    }
+                    ExecutionStatus::KernelErrored(error) => {
+                        Label::new(format!("Kernel error: {}", error))
+                            .color(Color::Error)
+                            .into_any_element()
+                    }
+                })
+                .into_any_element();
         }
 
         div()

crates/repl/src/session.rs 🔗

@@ -5,14 +5,16 @@ use crate::{
 use collections::{HashMap, HashSet};
 use editor::{
     display_map::{
-        BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
+        BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId,
+        RenderBlock,
     },
     scroll::Autoscroll,
     Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint,
 };
 use futures::{FutureExt as _, StreamExt as _};
 use gpui::{
-    div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView,
+    div, prelude::*, EntityId, EventEmitter, Model, Render, Subscription, Task, View, ViewContext,
+    WeakView,
 };
 use language::Point;
 use project::Fs;
@@ -22,7 +24,7 @@ use runtimelib::{
 use settings::Settings as _;
 use std::{env::temp_dir, ops::Range, path::PathBuf, sync::Arc, time::Duration};
 use theme::{ActiveTheme, ThemeSettings};
-use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, Label};
+use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, IconButtonShape, Label, Tooltip};
 
 pub struct Session {
     pub editor: WeakView<Editor>,
@@ -39,13 +41,18 @@ struct EditorBlock {
     invalidation_anchor: Anchor,
     block_id: CustomBlockId,
     execution_view: View<ExecutionView>,
+    on_close: CloseBlockFn,
 }
 
+type CloseBlockFn =
+    Arc<dyn for<'a> Fn(CustomBlockId, &'a mut WindowContext) + Send + Sync + 'static>;
+
 impl EditorBlock {
     fn new(
         editor: WeakView<Editor>,
         code_range: Range<Anchor>,
         status: ExecutionStatus,
+        on_close: CloseBlockFn,
         cx: &mut ViewContext<Session>,
     ) -> anyhow::Result<Self> {
         let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx));
@@ -73,7 +80,7 @@ impl EditorBlock {
                 position: code_range.end,
                 height: execution_view.num_lines(cx).saturating_add(1),
                 style: BlockStyle::Sticky,
-                render: Self::create_output_area_render(execution_view.clone()),
+                render: Self::create_output_area_render(execution_view.clone(), on_close.clone()),
                 disposition: BlockDisposition::Below,
             };
 
@@ -87,6 +94,7 @@ impl EditorBlock {
             invalidation_anchor,
             block_id,
             execution_view,
+            on_close,
         })
     }
 
@@ -98,11 +106,15 @@ impl EditorBlock {
         self.editor
             .update(cx, |editor, cx| {
                 let mut replacements = HashMap::default();
+
                 replacements.insert(
                     self.block_id,
                     (
                         Some(self.execution_view.num_lines(cx).saturating_add(1)),
-                        Self::create_output_area_render(self.execution_view.clone()),
+                        Self::create_output_area_render(
+                            self.execution_view.clone(),
+                            self.on_close.clone(),
+                        ),
                     ),
                 );
                 editor.replace_blocks(replacements, None, cx);
@@ -110,31 +122,74 @@ impl EditorBlock {
             .ok();
     }
 
-    fn create_output_area_render(execution_view: View<ExecutionView>) -> RenderBlock {
+    fn create_output_area_render(
+        execution_view: View<ExecutionView>,
+        on_close: CloseBlockFn,
+    ) -> RenderBlock {
         let render = move |cx: &mut BlockContext| {
             let execution_view = execution_view.clone();
             let text_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
             let text_font_size = ThemeSettings::get_global(cx).buffer_font_size;
-            // Note: we'll want to use `cx.anchor_x` when someone runs something with no output -- just show a checkmark and not make the full block below the line
 
-            let gutter_width = cx.gutter_dimensions.width;
+            let gutter = cx.gutter_dimensions;
+            let close_button_size = IconSize::XSmall;
+
+            let block_id = cx.block_id;
+            let on_close = on_close.clone();
+
+            let rem_size = cx.rem_size();
+            let line_height = cx.text_style().line_height_in_pixels(rem_size);
+
+            let (close_button_width, close_button_padding) =
+                close_button_size.square_components(cx);
 
-            h_flex()
+            div()
+                .min_h(line_height)
+                .flex()
+                .flex_row()
+                .items_start()
                 .w_full()
                 .bg(cx.theme().colors().background)
                 .border_y_1()
                 .border_color(cx.theme().colors().border)
-                .pl(gutter_width)
+                .child(
+                    v_flex().min_h(cx.line_height()).justify_center().child(
+                        h_flex()
+                            .w(gutter.full_width())
+                            .justify_end()
+                            .pt(line_height / 2.)
+                            .child(
+                                h_flex()
+                                    .pr(gutter.width / 2. - close_button_width
+                                        + close_button_padding / 2.)
+                                    .child(
+                                        IconButton::new(
+                                            ("close_output_area", EntityId::from(cx.block_id)),
+                                            IconName::Close,
+                                        )
+                                        .shape(IconButtonShape::Square)
+                                        .icon_size(close_button_size)
+                                        .icon_color(Color::Muted)
+                                        .tooltip(|cx| Tooltip::text("Close output area", cx))
+                                        .on_click(
+                                            move |_, cx| {
+                                                if let BlockId::Custom(block_id) = block_id {
+                                                    (on_close)(block_id, cx)
+                                                }
+                                            },
+                                        ),
+                                    ),
+                            ),
+                    ),
+                )
                 .child(
                     div()
+                        .flex_1()
+                        .size_full()
+                        .my_2()
+                        .mr(gutter.width)
                         .text_size(text_font_size)
                         .font_family(text_font)
-                        // .ml(gutter_width)
-                        .mx_1()
-                        .my_2()
-                        .h_full()
-                        .w_full()
-                        .mr(gutter_width)
                         .child(execution_view),
                 )
                 .into_any_element()
@@ -373,8 +428,30 @@ impl Session {
             Kernel::Shutdown => ExecutionStatus::Shutdown,
         };
 
+        let parent_message_id = message.header.msg_id.clone();
+        let session_view = cx.view().downgrade();
+        let weak_editor = self.editor.clone();
+
+        let on_close: CloseBlockFn =
+            Arc::new(move |block_id: CustomBlockId, cx: &mut WindowContext| {
+                if let Some(session) = session_view.upgrade() {
+                    session.update(cx, |session, cx| {
+                        session.blocks.remove(&parent_message_id);
+                        cx.notify();
+                    });
+                }
+
+                if let Some(editor) = weak_editor.upgrade() {
+                    editor.update(cx, |editor, cx| {
+                        let mut block_ids = HashSet::default();
+                        block_ids.insert(block_id);
+                        editor.remove_blocks(block_ids, None, cx);
+                    });
+                }
+            });
+
         let editor_block = if let Ok(editor_block) =
-            EditorBlock::new(self.editor.clone(), anchor_range, status, cx)
+            EditorBlock::new(self.editor.clone(), anchor_range, status, on_close, cx)
         {
             editor_block
         } else {

crates/ui/src/components/icon.rs 🔗

@@ -76,8 +76,12 @@ impl IconSize {
         }
     }
 
-    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
-    pub(crate) fn square(&self, cx: &mut WindowContext) -> Pixels {
+    /// Returns the individual components of the square that contains this [`IconSize`].
+    ///
+    /// The returned tuple contains:
+    ///   1. The length of one side of the square
+    ///   2. The padding of one side of the square
+    pub fn square_components(&self, cx: &mut WindowContext) -> (Pixels, Pixels) {
         let icon_size = self.rems() * cx.rem_size();
         let padding = match self {
             IconSize::Indicator => Spacing::None.px(cx),
@@ -86,6 +90,13 @@ impl IconSize {
             IconSize::Medium => Spacing::XSmall.px(cx),
         };
 
+        (icon_size, padding)
+    }
+
+    /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
+    pub fn square(&self, cx: &mut WindowContext) -> Pixels {
+        let (icon_size, padding) = self.square_components(cx);
+
         icon_size + padding * 2.
     }
 }