diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index c7de7c6930..17dc8b0180 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -503,29 +503,38 @@ impl ExecutionView { impl Render for ExecutionView { fn render(&mut self, cx: &mut ViewContext) -> 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() diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index c8709a961f..c6b010c7d5 100644 --- a/crates/repl/src/session.rs +++ b/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, @@ -39,13 +41,18 @@ struct EditorBlock { invalidation_anchor: Anchor, block_id: CustomBlockId, execution_view: View, + on_close: CloseBlockFn, } +type CloseBlockFn = + Arc Fn(CustomBlockId, &'a mut WindowContext) + Send + Sync + 'static>; + impl EditorBlock { fn new( editor: WeakView, code_range: Range, status: ExecutionStatus, + on_close: CloseBlockFn, cx: &mut ViewContext, ) -> anyhow::Result { 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) -> RenderBlock { + fn create_output_area_render( + execution_view: View, + 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; - h_flex() + 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); + + 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 { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index b87d80232c..26a5d93083 100644 --- a/crates/ui/src/components/icon.rs +++ b/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. } }