diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 3796d30ec1..287f705318 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -5,7 +5,6 @@ //! //! ## Key Components //! -//! - `Output`: Represents a single output item, which can be of various types. //! - `OutputContent`: An enum that encapsulates different types of output content. //! - `ExecutionView`: Manages the display of outputs for a single execution. //! - `ExecutionStatus`: Represents the current status of an execution. @@ -36,9 +35,12 @@ use std::time::Duration; +use editor::Editor; use gpui::{ - percentage, Animation, AnimationExt, AnyElement, ClipboardItem, Render, Transformation, View, + percentage, Animation, AnimationExt, AnyElement, ClipboardItem, Model, Render, Transformation, + View, WeakView, }; +use language::Buffer; use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType}; use ui::{div, prelude::*, v_flex, IntoElement, Styled, Tooltip, ViewContext}; @@ -56,6 +58,7 @@ use plain::TerminalOutput; mod user_error; use user_error::ErrorView; +use workspace::Workspace; /// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance fn rank_mime_type(mimetype: &MimeType) -> usize { @@ -70,105 +73,209 @@ fn rank_mime_type(mimetype: &MimeType) -> usize { } } -pub(crate) trait SupportsClipboard { +pub(crate) trait OutputContent { fn clipboard_content(&self, cx: &WindowContext) -> Option; - fn has_clipboard_content(&self, cx: &WindowContext) -> bool; -} - -pub struct Output { - content: OutputContent, - display_id: Option, -} - -impl Output { - pub fn new(data: &MimeBundle, display_id: Option, cx: &mut WindowContext) -> Self { - Self { - content: OutputContent::new(data, cx), - display_id, - } + fn has_clipboard_content(&self, _cx: &WindowContext) -> bool { + return false; } - - pub fn from(content: OutputContent) -> Self { - Self { - content, - display_id: None, - } + fn has_buffer_content(&self, _cx: &WindowContext) -> bool { + return false; + } + fn buffer_content(&mut self, _cx: &mut WindowContext) -> Option> { + None } } -impl SupportsClipboard for Output { +impl OutputContent for View { fn clipboard_content(&self, cx: &WindowContext) -> Option { - match &self.content { - OutputContent::Plain(terminal) => terminal.clipboard_content(cx), - OutputContent::Stream(terminal) => terminal.clipboard_content(cx), - OutputContent::Image(image) => image.clipboard_content(cx), - OutputContent::ErrorOutput(error) => error.traceback.clipboard_content(cx), - OutputContent::Message(_) => None, - OutputContent::Table(table) => table.clipboard_content(cx), - OutputContent::Markdown(markdown) => markdown.read(cx).clipboard_content(cx), - OutputContent::ClearOutputWaitMarker => None, - } + self.read(cx).clipboard_content(cx) } fn has_clipboard_content(&self, cx: &WindowContext) -> bool { - match &self.content { - OutputContent::Plain(terminal) => terminal.has_clipboard_content(cx), - OutputContent::Stream(terminal) => terminal.has_clipboard_content(cx), - OutputContent::Image(image) => image.has_clipboard_content(cx), - OutputContent::ErrorOutput(error) => error.traceback.has_clipboard_content(cx), - OutputContent::Message(_) => false, - OutputContent::Table(table) => table.has_clipboard_content(cx), - OutputContent::Markdown(markdown) => markdown.read(cx).has_clipboard_content(cx), - OutputContent::ClearOutputWaitMarker => false, - } + self.read(cx).has_clipboard_content(cx) + } + + fn has_buffer_content(&self, cx: &WindowContext) -> bool { + self.read(cx).has_buffer_content(cx) + } + + fn buffer_content(&mut self, cx: &mut WindowContext) -> Option> { + self.update(cx, |item, cx| item.buffer_content(cx)) } } -pub enum OutputContent { - Plain(TerminalOutput), - Stream(TerminalOutput), - Image(ImageView), +pub enum Output { + Plain { + content: View, + display_id: Option, + }, + Stream { + content: View, + }, + Image { + content: View, + display_id: Option, + }, ErrorOutput(ErrorView), Message(String), - Table(TableView), - Markdown(View), + Table { + content: View, + display_id: Option, + }, + Markdown { + content: View, + display_id: Option, + }, ClearOutputWaitMarker, } -impl OutputContent { - fn render(&self, cx: &mut ViewContext) -> Option { - let el = match self { - // Note: in typical frontends we would show the execute_result.execution_count - // Here we can just handle either - Self::Plain(stdio) => Some(stdio.render(cx)), - Self::Markdown(markdown) => Some(markdown.clone().into_any_element()), - Self::Stream(stdio) => Some(stdio.render(cx)), - Self::Image(image) => Some(image.render(cx)), +impl Output { + fn render_output_controls( + v: View, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Option { + if !v.has_clipboard_content(cx) && !v.has_buffer_content(cx) { + return None; + } + + Some( + h_flex() + .pl_1() + .when(v.has_clipboard_content(cx), |el| { + let v = v.clone(); + el.child( + IconButton::new(ElementId::Name("copy-output".into()), IconName::Copy) + .style(ButtonStyle::Transparent) + .tooltip(move |cx| Tooltip::text("Copy Output", cx)) + .on_click(cx.listener(move |_, _, cx| { + let clipboard_content = v.clipboard_content(cx); + + if let Some(clipboard_content) = clipboard_content.as_ref() { + cx.write_to_clipboard(clipboard_content.clone()); + } + })), + ) + }) + .when(v.has_buffer_content(cx), |el| { + let v = v.clone(); + el.child( + IconButton::new( + ElementId::Name("open-in-buffer".into()), + IconName::FileText, + ) + .style(ButtonStyle::Transparent) + .tooltip(move |cx| Tooltip::text("Open in Buffer", cx)) + .on_click(cx.listener({ + let workspace = workspace.clone(); + + move |_, _, cx| { + let buffer_content = + v.update(cx, |item, cx| item.buffer_content(cx)); + + if let Some(buffer_content) = buffer_content.as_ref() { + let buffer = buffer_content.clone(); + let editor = Box::new(cx.new_view(|cx| { + Editor::for_buffer(buffer.clone(), None, cx) + })); + workspace + .update(cx, |workspace, cx| { + workspace + .add_item_to_active_pane(editor, None, true, cx); + }) + .ok(); + } + } + })), + ) + }) + .into_any_element(), + ) + } + + fn render( + &self, + workspace: WeakView, + cx: &mut ViewContext, + ) -> impl IntoElement { + let content = match self { + Self::Plain { content, .. } => Some(content.clone().into_any_element()), + Self::Markdown { content, .. } => Some(content.clone().into_any_element()), + Self::Stream { content, .. } => Some(content.clone().into_any_element()), + Self::Image { content, .. } => Some(content.clone().into_any_element()), Self::Message(message) => Some(div().child(message.clone()).into_any_element()), - Self::Table(table) => Some(table.render(cx)), + Self::Table { content, .. } => Some(content.clone().into_any_element()), Self::ErrorOutput(error_view) => error_view.render(cx), Self::ClearOutputWaitMarker => None, }; - el + h_flex() + .w_full() + .items_start() + .child(div().flex_1().children(content)) + .children(match self { + Self::Plain { content, .. } => { + Self::render_output_controls(content.clone(), workspace.clone(), cx) + } + Self::Markdown { content, .. } => { + Self::render_output_controls(content.clone(), workspace.clone(), cx) + } + Self::Stream { content, .. } => { + Self::render_output_controls(content.clone(), workspace.clone(), cx) + } + Self::Image { content, .. } => { + Self::render_output_controls(content.clone(), workspace.clone(), cx) + } + Self::ErrorOutput(err) => { + Self::render_output_controls(err.traceback.clone(), workspace.clone(), cx) + } + Self::Message(_) => None, + Self::Table { content, .. } => { + Self::render_output_controls(content.clone(), workspace.clone(), cx) + } + Self::ClearOutputWaitMarker => None, + }) } - pub fn new(data: &MimeBundle, cx: &mut WindowContext) -> Self { + pub fn display_id(&self) -> Option { + match self { + Output::Plain { display_id, .. } => display_id.clone(), + Output::Stream { .. } => None, + Output::Image { display_id, .. } => display_id.clone(), + Output::ErrorOutput(_) => None, + Output::Message(_) => None, + Output::Table { display_id, .. } => display_id.clone(), + Output::Markdown { display_id, .. } => display_id.clone(), + Output::ClearOutputWaitMarker => None, + } + } + + pub fn new(data: &MimeBundle, display_id: Option, cx: &mut WindowContext) -> Self { match data.richest(rank_mime_type) { - Some(MimeType::Plain(text)) => OutputContent::Plain(TerminalOutput::from(text, cx)), + Some(MimeType::Plain(text)) => Output::Plain { + content: cx.new_view(|cx| TerminalOutput::from(text, cx)), + display_id, + }, Some(MimeType::Markdown(text)) => { let view = cx.new_view(|cx| MarkdownView::from(text.clone(), cx)); - OutputContent::Markdown(view) + Output::Markdown { + content: view, + display_id, + } } Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) { - Ok(view) => OutputContent::Image(view), - Err(error) => OutputContent::Message(format!("Failed to load image: {}", error)), + Ok(view) => Output::Image { + content: cx.new_view(|_| view), + display_id, + }, + Err(error) => Output::Message(format!("Failed to load image: {}", error)), + }, + Some(MimeType::DataTable(data)) => Output::Table { + content: cx.new_view(|cx| TableView::new(data, cx)), + display_id, }, - Some(MimeType::DataTable(data)) => { - OutputContent::Table(TableView::new(data.clone(), cx)) - } // Any other media types are not supported - _ => OutputContent::Message("Unsupported media type".to_string()), + _ => Output::Message("Unsupported media type".to_string()), } } } @@ -191,13 +298,20 @@ pub enum ExecutionStatus { /// It can hold zero or more outputs, which the user /// sees as "the output" for a single execution. pub struct ExecutionView { + #[allow(unused)] + workspace: WeakView, pub outputs: Vec, pub status: ExecutionStatus, } impl ExecutionView { - pub fn new(status: ExecutionStatus, _cx: &mut ViewContext) -> Self { + pub fn new( + status: ExecutionStatus, + workspace: WeakView, + _cx: &mut ViewContext, + ) -> Self { Self { + workspace, outputs: Default::default(), status, } @@ -217,20 +331,20 @@ impl ExecutionView { JupyterMessageContent::StreamContent(result) => { // Previous stream data will combine together, handling colors, carriage returns, etc if let Some(new_terminal) = self.apply_terminal_text(&result.text, cx) { - Output::from(new_terminal) + new_terminal } else { return; } } JupyterMessageContent::ErrorOutput(result) => { - let mut terminal = TerminalOutput::new(cx); - terminal.append_text(&result.traceback.join("\n")); + let terminal = + cx.new_view(|cx| TerminalOutput::from(&result.traceback.join("\n"), cx)); - Output::from(OutputContent::ErrorOutput(ErrorView { + Output::ErrorOutput(ErrorView { ename: result.ename.clone(), evalue: result.evalue.clone(), traceback: terminal, - })) + }) } JupyterMessageContent::ExecuteReply(reply) => { for payload in reply.payload.iter() { @@ -271,7 +385,7 @@ impl ExecutionView { } // Create a marker to clear the output after we get in a new output - Output::from(OutputContent::ClearOutputWaitMarker) + Output::ClearOutputWaitMarker } JupyterMessageContent::Status(status) => { match status.execution_state { @@ -290,7 +404,7 @@ impl ExecutionView { // Check for a clear output marker as the previous output, so we can clear it out if let Some(output) = self.outputs.last() { - if let OutputContent::ClearOutputWaitMarker = output.content { + if let Output::ClearOutputWaitMarker = output { self.outputs.clear(); } } @@ -309,9 +423,9 @@ impl ExecutionView { let mut any = false; self.outputs.iter_mut().for_each(|output| { - if let Some(other_display_id) = output.display_id.as_ref() { + if let Some(other_display_id) = output.display_id().as_ref() { if other_display_id == display_id { - output.content = OutputContent::new(data, cx); + *output = Output::new(data, Some(display_id.to_owned()), cx); any = true; } } @@ -322,33 +436,29 @@ impl ExecutionView { } } - fn apply_terminal_text( - &mut self, - text: &str, - cx: &mut ViewContext, - ) -> Option { + fn apply_terminal_text(&mut self, text: &str, cx: &mut ViewContext) -> Option { if let Some(last_output) = self.outputs.last_mut() { - match &mut last_output.content { - OutputContent::Stream(last_stream) => { - last_stream.append_text(text); + match last_output { + Output::Stream { + content: last_stream, + } => { // Don't need to add a new output, we already have a terminal output - cx.notify(); + // and can just update the most recent terminal output + last_stream.update(cx, |last_stream, cx| { + last_stream.append_text(text, cx); + cx.notify(); + }); return None; } - // Edge case note: a clear output marker - OutputContent::ClearOutputWaitMarker => { - // Edge case note: a clear output marker is handled by the caller - // since we will return a new output at the end here as a new terminal output - } // A different output type is "in the way", so we need to create a new output, - // which is the same as having no prior output + // which is the same as having no prior stream/terminal text _ => {} } } - let mut new_terminal = TerminalOutput::new(cx); - new_terminal.append_text(text); - Some(OutputContent::Stream(new_terminal)) + Some(Output::Stream { + content: cx.new_view(|cx| TerminalOutput::from(text, cx)), + }) } } @@ -405,42 +515,11 @@ impl Render for ExecutionView { div() .w_full() - .children(self.outputs.iter().enumerate().map(|(index, output)| { - h_flex() - .w_full() - .items_start() - .child( - div().flex_1().child( - output - .content - .render(cx) - .unwrap_or_else(|| div().into_any_element()), - ), - ) - .when(output.has_clipboard_content(cx), |el| { - let clipboard_content = output.clipboard_content(cx); - - el.child( - div().pl_1().child( - IconButton::new( - ElementId::Name(format!("copy-output-{}", index).into()), - IconName::Copy, - ) - .style(ButtonStyle::Transparent) - .tooltip(move |cx| Tooltip::text("Copy Output", cx)) - .on_click(cx.listener( - move |_, _, cx| { - if let Some(clipboard_content) = clipboard_content.as_ref() - { - cx.write_to_clipboard(clipboard_content.clone()); - // todo!(): let the user know that the content was copied - } - }, - )), - ), - ) - }) - })) + .children( + self.outputs + .iter() + .map(|output| output.render(self.workspace.clone(), cx)), + ) .children(match self.status { ExecutionStatus::Executing => vec![status], ExecutionStatus::Queued => vec![status], diff --git a/crates/repl/src/outputs/image.rs b/crates/repl/src/outputs/image.rs index 3bcb94a418..1158b728be 100644 --- a/crates/repl/src/outputs/image.rs +++ b/crates/repl/src/outputs/image.rs @@ -1,12 +1,10 @@ use anyhow::Result; use base64::prelude::*; -use gpui::{ - img, AnyElement, ClipboardItem, Image, ImageFormat, Pixels, RenderImage, WindowContext, -}; +use gpui::{img, ClipboardItem, Image, ImageFormat, Pixels, RenderImage, WindowContext}; use std::sync::Arc; use ui::{div, prelude::*, IntoElement, Styled}; -use crate::outputs::SupportsClipboard; +use crate::outputs::OutputContent; /// ImageView renders an image inline in an editor, adapting to the line height to fit the image. pub struct ImageView { @@ -59,8 +57,10 @@ impl ImageView { image: Arc::new(gpui_image_data), }); } +} - pub fn render(&self, cx: &mut WindowContext) -> AnyElement { +impl Render for ImageView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let line_height = cx.line_height(); let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 { @@ -73,15 +73,11 @@ impl ImageView { let image = self.image.clone(); - div() - .h(Pixels(height)) - .w(Pixels(width)) - .child(img(image)) - .into_any_element() + div().h(Pixels(height)).w(Pixels(width)).child(img(image)) } } -impl SupportsClipboard for ImageView { +impl OutputContent for ImageView { fn clipboard_content(&self, _cx: &WindowContext) -> Option { Some(ClipboardItem::new_image(self.clipboard_image.as_ref())) } diff --git a/crates/repl/src/outputs/markdown.rs b/crates/repl/src/outputs/markdown.rs index c7c5e50f09..25fa14e73b 100644 --- a/crates/repl/src/outputs/markdown.rs +++ b/crates/repl/src/outputs/markdown.rs @@ -1,12 +1,13 @@ use anyhow::Result; -use gpui::{div, prelude::*, ClipboardItem, Task, ViewContext, WindowContext}; +use gpui::{div, prelude::*, ClipboardItem, Model, Task, ViewContext, WindowContext}; +use language::Buffer; use markdown_preview::{ markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown, markdown_renderer::render_markdown_block, }; use ui::v_flex; -use crate::outputs::SupportsClipboard; +use crate::outputs::OutputContent; pub struct MarkdownView { raw_text: String, @@ -41,7 +42,7 @@ impl MarkdownView { } } -impl SupportsClipboard for MarkdownView { +impl OutputContent for MarkdownView { fn clipboard_content(&self, _cx: &WindowContext) -> Option { Some(ClipboardItem::new_string(self.raw_text.clone())) } @@ -49,6 +50,18 @@ impl SupportsClipboard for MarkdownView { fn has_clipboard_content(&self, _cx: &WindowContext) -> bool { true } + + fn has_buffer_content(&self, _cx: &WindowContext) -> bool { + true + } + + fn buffer_content(&mut self, cx: &mut WindowContext) -> Option> { + let buffer = cx.new_model(|cx| { + // todo!(): Bring in the language registry so we can set the language to markdown + Buffer::local(self.raw_text.clone(), cx).with_language(language::PLAIN_TEXT.clone(), cx) + }); + Some(buffer) + } } impl Render for MarkdownView { diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs index 713eca8a60..5ca3b873fa 100644 --- a/crates/repl/src/outputs/plain.rs +++ b/crates/repl/src/outputs/plain.rs @@ -15,8 +15,14 @@ //! - Error tracebacks //! -use alacritty_terminal::{grid::Dimensions as _, term::Config, vte::ansi::Processor}; -use gpui::{canvas, size, AnyElement, ClipboardItem, FontStyle, TextStyle, WhiteSpace}; +use alacritty_terminal::{ + grid::Dimensions as _, + index::{Column, Line, Point}, + term::Config, + vte::ansi::Processor, +}; +use gpui::{canvas, size, ClipboardItem, FontStyle, Model, TextStyle, WhiteSpace}; +use language::Buffer; use settings::Settings as _; use std::mem; use terminal::ZedListener; @@ -24,7 +30,7 @@ use terminal_view::terminal_element::TerminalElement; use theme::ThemeSettings; use ui::{prelude::*, IntoElement}; -use crate::outputs::SupportsClipboard; +use crate::outputs::OutputContent; /// The `TerminalOutput` struct handles the parsing and rendering of text input, /// simulating a basic terminal environment within REPL output. @@ -40,6 +46,7 @@ use crate::outputs::SupportsClipboard; /// supporting ANSI escape sequences for text formatting and colors. /// pub struct TerminalOutput { + full_buffer: Option>, /// ANSI escape sequence processor for parsing input text. parser: Processor, /// Alacritty terminal instance that manages the terminal state and content. @@ -67,7 +74,6 @@ pub fn text_style(cx: &mut WindowContext) -> TextStyle { font_fallbacks, font_size: theme::get_buffer_font_size(cx).into(), font_style: FontStyle::Normal, - // todo line_height: cx.line_height().into(), background_color: Some(theme.colors().terminal_background), white_space: WhiteSpace::Normal, @@ -128,6 +134,7 @@ impl TerminalOutput { Self { parser: Processor::new(), handler: term, + full_buffer: None, } } @@ -145,7 +152,7 @@ impl TerminalOutput { /// A new instance of `TerminalOutput` containing the provided text. pub fn from(text: &str, cx: &mut WindowContext) -> Self { let mut output = Self::new(cx); - output.append_text(text); + output.append_text(text, cx); output } @@ -175,7 +182,7 @@ impl TerminalOutput { /// # Arguments /// /// * `text` - A string slice containing the text to be appended. - pub fn append_text(&mut self, text: &str) { + pub fn append_text(&mut self, text: &str, cx: &mut WindowContext) { for byte in text.as_bytes() { if *byte == b'\n' { // Dirty (?) hack to move the cursor down @@ -184,17 +191,62 @@ impl TerminalOutput { } else { self.parser.advance(&mut self.handler, *byte); } + } - // self.parser.advance(&mut self.handler, *byte); + // This will keep the buffer up to date, though with some terminal codes it won't be perfect + if let Some(buffer) = self.full_buffer.as_ref() { + buffer.update(cx, |buffer, cx| { + buffer.edit([(buffer.len()..buffer.len(), text)], None, cx); + }); } } + fn full_text(&self) -> String { + let mut full_text = String::new(); + + // Get the total number of lines, including history + let total_lines = self.handler.grid().total_lines(); + let visible_lines = self.handler.screen_lines(); + let history_lines = total_lines - visible_lines; + + // Capture history lines in correct order (oldest to newest) + for line in (0..history_lines).rev() { + let line_index = Line(-(line as i32) - 1); + let start = Point::new(line_index, Column(0)); + let end = Point::new(line_index, Column(self.handler.columns() - 1)); + let line_content = self.handler.bounds_to_string(start, end); + + if !line_content.trim().is_empty() { + full_text.push_str(&line_content); + full_text.push('\n'); + } + } + + // Capture visible lines + for line in 0..visible_lines { + let line_index = Line(line as i32); + let start = Point::new(line_index, Column(0)); + let end = Point::new(line_index, Column(self.handler.columns() - 1)); + let line_content = self.handler.bounds_to_string(start, end); + + if !line_content.trim().is_empty() { + full_text.push_str(&line_content); + full_text.push('\n'); + } + } + + // Trim any trailing newlines + full_text.trim_end().to_string() + } +} + +impl Render for TerminalOutput { /// Renders the terminal output as a GPUI element. /// /// Converts the current terminal state into a renderable GPUI element. It handles /// the layout of the terminal grid, calculates the dimensions of the output, and /// creates a canvas element that paints the terminal cells and background rectangles. - pub fn render(&self, cx: &mut WindowContext) -> AnyElement { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let text_style = text_style(cx); let text_system = cx.text_system(); @@ -254,25 +306,31 @@ impl TerminalOutput { ) // We must set the height explicitly for the editor block to size itself correctly .h(height) - .into_any_element() } } -impl SupportsClipboard for TerminalOutput { +impl OutputContent for TerminalOutput { fn clipboard_content(&self, _cx: &WindowContext) -> Option { - let start = alacritty_terminal::index::Point::new( - alacritty_terminal::index::Line(0), - alacritty_terminal::index::Column(0), - ); - let end = alacritty_terminal::index::Point::new( - alacritty_terminal::index::Line(self.handler.screen_lines() as i32 - 1), - alacritty_terminal::index::Column(self.handler.columns() - 1), - ); - let text = self.handler.bounds_to_string(start, end); - Some(ClipboardItem::new_string(text.trim().into())) + Some(ClipboardItem::new_string(self.full_text())) } fn has_clipboard_content(&self, _cx: &WindowContext) -> bool { true } + + fn has_buffer_content(&self, _cx: &WindowContext) -> bool { + true + } + + fn buffer_content(&mut self, cx: &mut WindowContext) -> Option> { + if let Some(_) = self.full_buffer.as_ref() { + return self.full_buffer.clone(); + } + + let buffer = cx.new_model(|cx| { + Buffer::local(self.full_text(), cx).with_language(language::PLAIN_TEXT.clone(), cx) + }); + self.full_buffer = Some(buffer.clone()); + Some(buffer) + } } diff --git a/crates/repl/src/outputs/table.rs b/crates/repl/src/outputs/table.rs index f9238dd5a9..dfe0bbda56 100644 --- a/crates/repl/src/outputs/table.rs +++ b/crates/repl/src/outputs/table.rs @@ -62,7 +62,7 @@ use settings::Settings; use theme::ThemeSettings; use ui::{div, prelude::*, v_flex, IntoElement, Styled}; -use crate::outputs::SupportsClipboard; +use crate::outputs::OutputContent; /// TableView renders a static table inline in a buffer. /// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange. @@ -87,7 +87,7 @@ fn cell_content(row: &Value, field: &str) -> String { const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5; impl TableView { - pub fn new(table: TabularDataResource, cx: &mut WindowContext) -> Self { + pub fn new(table: &TabularDataResource, cx: &mut WindowContext) -> Self { let mut widths = Vec::with_capacity(table.schema.fields.len()); let text_system = cx.text_system(); @@ -133,7 +133,7 @@ impl TableView { let cached_clipboard_content = Self::create_clipboard_content(&table); Self { - table, + table: table.clone(), widths, cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content), } @@ -194,31 +194,6 @@ impl TableView { markdown } - pub fn render(&self, cx: &WindowContext) -> AnyElement { - let data = match &self.table.data { - Some(data) => data, - None => return div().into_any_element(), - }; - - let mut headings = serde_json::Map::new(); - for field in &self.table.schema.fields { - headings.insert(field.name.clone(), Value::String(field.name.clone())); - } - let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx); - - let body = data - .iter() - .map(|row| self.render_row(&self.table.schema, false, &row, cx)); - - v_flex() - .id("table") - .overflow_x_scroll() - .w_full() - .child(header) - .children(body) - .into_any_element() - } - pub fn render_row( &self, schema: &TableSchema, @@ -282,7 +257,34 @@ impl TableView { } } -impl SupportsClipboard for TableView { +impl Render for TableView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let data = match &self.table.data { + Some(data) => data, + None => return div().into_any_element(), + }; + + let mut headings = serde_json::Map::new(); + for field in &self.table.schema.fields { + headings.insert(field.name.clone(), Value::String(field.name.clone())); + } + let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx); + + let body = data + .iter() + .map(|row| self.render_row(&self.table.schema, false, &row, cx)); + + v_flex() + .id("table") + .overflow_x_scroll() + .w_full() + .child(header) + .children(body) + .into_any_element() + } +} + +impl OutputContent for TableView { fn clipboard_content(&self, _cx: &WindowContext) -> Option { Some(self.cached_clipboard_content.clone()) } diff --git a/crates/repl/src/outputs/user_error.rs b/crates/repl/src/outputs/user_error.rs index 00d301c321..4e000635b3 100644 --- a/crates/repl/src/outputs/user_error.rs +++ b/crates/repl/src/outputs/user_error.rs @@ -1,4 +1,4 @@ -use gpui::{AnyElement, FontWeight, WindowContext}; +use gpui::{AnyElement, FontWeight, View, WindowContext}; use ui::{h_flex, prelude::*, v_flex, Label}; use crate::outputs::plain::TerminalOutput; @@ -7,7 +7,7 @@ use crate::outputs::plain::TerminalOutput; pub struct ErrorView { pub ename: String, pub evalue: String, - pub traceback: TerminalOutput, + pub traceback: View, } impl ErrorView { @@ -41,7 +41,7 @@ impl ErrorView { .py(padding) .border_l_1() .border_color(theme.status().error_border) - .child(self.traceback.render(cx)), + .child(self.traceback.clone()), ) .into_any_element(), ) diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 0791532d1e..7eef03773a 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -60,7 +60,16 @@ impl EditorBlock { on_close: CloseBlockFn, cx: &mut ViewContext, ) -> anyhow::Result { - let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx)); + let editor = editor + .upgrade() + .ok_or_else(|| anyhow::anyhow!("editor is not open"))?; + let workspace = editor + .read(cx) + .workspace() + .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?; + + let execution_view = + cx.new_view(|cx| ExecutionView::new(status, workspace.downgrade(), cx)); let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| { let buffer = editor.buffer().clone(); @@ -93,7 +102,7 @@ impl EditorBlock { let block_id = editor.insert_blocks([block], None, cx)[0]; (block_id, invalidation_anchor) - })?; + }); anyhow::Ok(Self { code_range,