REPL: Refactor output (#16927)

Kyle Kelley created

Shuffle `outputs.rs` into individual `outputs/*.rs` files and start
documenting them more.

Release Notes:

- N/A

Change summary

crates/repl/src/outputs.rs            | 455 +---------------------------
crates/repl/src/outputs/image.rs      |  92 +++++
crates/repl/src/outputs/markdown.rs   |  75 ++++
crates/repl/src/outputs/plain.rs      |   7 
crates/repl/src/outputs/table.rs      | 293 ++++++++++++++++++
crates/repl/src/outputs/user_error.rs |  49 +++
crates/repl/src/repl.rs               |   1 
crates/repl/src/session.rs            |   4 
8 files changed, 533 insertions(+), 443 deletions(-)

Detailed changes

crates/repl/src/outputs.rs 🔗

@@ -1,25 +1,25 @@
-use std::sync::Arc;
 use std::time::Duration;
 
-use crate::stdio::TerminalOutput;
-use anyhow::Result;
-use base64::prelude::*;
 use gpui::{
-    img, percentage, Animation, AnimationExt, AnyElement, ClipboardItem, FontWeight, Image,
-    ImageFormat, Render, RenderImage, Task, TextRun, Transformation, View,
+    percentage, Animation, AnimationExt, AnyElement, ClipboardItem, Render, Transformation, View,
 };
-use runtimelib::datatable::TableSchema;
-use runtimelib::media::datatable::TabularDataResource;
 use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
-use serde_json::Value;
-use settings::Settings;
-use theme::ThemeSettings;
 use ui::{div, prelude::*, v_flex, IntoElement, Styled, Tooltip, ViewContext};
 
-use markdown_preview::{
-    markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
-    markdown_renderer::render_markdown_block,
-};
+mod image;
+use image::ImageView;
+
+mod markdown;
+use markdown::MarkdownView;
+
+mod table;
+use table::TableView;
+
+pub mod plain;
+use plain::TerminalOutput;
+
+mod user_error;
+use user_error::ErrorView;
 
 /// 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 {
@@ -39,428 +39,6 @@ pub(crate) trait SupportsClipboard {
     fn has_clipboard_content(&self, cx: &WindowContext) -> bool;
 }
 
-/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
-pub struct ImageView {
-    clipboard_image: Arc<Image>,
-    height: u32,
-    width: u32,
-    image: Arc<RenderImage>,
-}
-
-impl ImageView {
-    fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
-        let line_height = cx.line_height();
-
-        let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
-            let height = u8::MAX as f32 * line_height.0;
-            let width = self.width as f32 * height / self.height as f32;
-            (height, width)
-        } else {
-            (self.height as f32, self.width as f32)
-        };
-
-        let image = self.image.clone();
-
-        div()
-            .h(Pixels(height))
-            .w(Pixels(width))
-            .child(img(image))
-            .into_any_element()
-    }
-
-    fn from(base64_encoded_data: &str) -> Result<Self> {
-        let bytes = BASE64_STANDARD.decode(base64_encoded_data)?;
-
-        let format = image::guess_format(&bytes)?;
-        let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
-
-        // Convert from RGBA to BGRA.
-        for pixel in data.chunks_exact_mut(4) {
-            pixel.swap(0, 2);
-        }
-
-        let height = data.height();
-        let width = data.width();
-
-        let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]);
-
-        let format = match format {
-            image::ImageFormat::Png => ImageFormat::Png,
-            image::ImageFormat::Jpeg => ImageFormat::Jpeg,
-            image::ImageFormat::Gif => ImageFormat::Gif,
-            image::ImageFormat::WebP => ImageFormat::Webp,
-            image::ImageFormat::Tiff => ImageFormat::Tiff,
-            image::ImageFormat::Bmp => ImageFormat::Bmp,
-            _ => {
-                return Err(anyhow::anyhow!("unsupported image format"));
-            }
-        };
-
-        // Convert back to a GPUI image for use with the clipboard
-        let clipboard_image = Arc::new(Image {
-            format,
-            bytes,
-            id: gpui_image_data.id.0 as u64,
-        });
-
-        return Ok(ImageView {
-            clipboard_image,
-            height,
-            width,
-            image: Arc::new(gpui_image_data),
-        });
-    }
-}
-
-impl SupportsClipboard for ImageView {
-    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
-        Some(ClipboardItem::new_image(self.clipboard_image.as_ref()))
-    }
-
-    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
-        true
-    }
-}
-
-/// TableView renders a static table inline in a buffer.
-/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
-pub struct TableView {
-    pub table: TabularDataResource,
-    pub widths: Vec<Pixels>,
-    cached_clipboard_content: ClipboardItem,
-}
-
-fn cell_content(row: &Value, field: &str) -> String {
-    match row.get(&field) {
-        Some(Value::String(s)) => s.clone(),
-        Some(Value::Number(n)) => n.to_string(),
-        Some(Value::Bool(b)) => b.to_string(),
-        Some(Value::Array(arr)) => format!("{:?}", arr),
-        Some(Value::Object(obj)) => format!("{:?}", obj),
-        Some(Value::Null) | None => String::new(),
-    }
-}
-
-// Declare constant for the padding multiple on the line height
-const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5;
-
-impl TableView {
-    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();
-        let text_style = cx.text_style();
-        let text_font = ThemeSettings::get_global(cx).buffer_font.clone();
-        let font_size = ThemeSettings::get_global(cx).buffer_font_size;
-        let mut runs = [TextRun {
-            len: 0,
-            font: text_font,
-            color: text_style.color,
-            background_color: None,
-            underline: None,
-            strikethrough: None,
-        }];
-
-        for field in table.schema.fields.iter() {
-            runs[0].len = field.name.len();
-            let mut width = text_system
-                .layout_line(&field.name, font_size, &runs)
-                .map(|layout| layout.width)
-                .unwrap_or(px(0.));
-
-            let Some(data) = table.data.as_ref() else {
-                widths.push(width);
-                continue;
-            };
-
-            for row in data {
-                let content = cell_content(&row, &field.name);
-                runs[0].len = content.len();
-                let cell_width = cx
-                    .text_system()
-                    .layout_line(&content, font_size, &runs)
-                    .map(|layout| layout.width)
-                    .unwrap_or(px(0.));
-
-                width = width.max(cell_width)
-            }
-
-            widths.push(width)
-        }
-
-        let cached_clipboard_content = Self::create_clipboard_content(&table);
-
-        Self {
-            table,
-            widths,
-            cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
-        }
-    }
-
-    fn escape_markdown(s: &str) -> String {
-        s.replace('|', "\\|")
-            .replace('*', "\\*")
-            .replace('_', "\\_")
-            .replace('`', "\\`")
-            .replace('[', "\\[")
-            .replace(']', "\\]")
-            .replace('<', "&lt;")
-            .replace('>', "&gt;")
-    }
-
-    fn create_clipboard_content(table: &TabularDataResource) -> String {
-        let data = match table.data.as_ref() {
-            Some(data) => data,
-            None => &Vec::new(),
-        };
-        let schema = table.schema.clone();
-
-        let mut markdown = format!(
-            "| {} |\n",
-            table
-                .schema
-                .fields
-                .iter()
-                .map(|field| field.name.clone())
-                .collect::<Vec<_>>()
-                .join(" | ")
-        );
-
-        markdown.push_str("|---");
-        for _ in 1..table.schema.fields.len() {
-            markdown.push_str("|---");
-        }
-        markdown.push_str("|\n");
-
-        let body = data
-            .iter()
-            .map(|record: &Value| {
-                let row_content = schema
-                    .fields
-                    .iter()
-                    .map(|field| Self::escape_markdown(&cell_content(record, &field.name)))
-                    .collect::<Vec<_>>();
-
-                row_content.join(" | ")
-            })
-            .collect::<Vec<String>>();
-
-        for row in body {
-            markdown.push_str(&format!("| {} |\n", row));
-        }
-
-        markdown
-    }
-
-    pub fn render(&self, cx: &ViewContext<ExecutionView>) -> 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,
-        is_header: bool,
-        row: &Value,
-        cx: &ViewContext<ExecutionView>,
-    ) -> AnyElement {
-        let theme = cx.theme();
-
-        let line_height = cx.line_height();
-
-        let row_cells = schema
-            .fields
-            .iter()
-            .zip(self.widths.iter())
-            .map(|(field, width)| {
-                let container = match field.field_type {
-                    runtimelib::datatable::FieldType::String => div(),
-
-                    runtimelib::datatable::FieldType::Number
-                    | runtimelib::datatable::FieldType::Integer
-                    | runtimelib::datatable::FieldType::Date
-                    | runtimelib::datatable::FieldType::Time
-                    | runtimelib::datatable::FieldType::Datetime
-                    | runtimelib::datatable::FieldType::Year
-                    | runtimelib::datatable::FieldType::Duration
-                    | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
-
-                    _ => div(),
-                };
-
-                let value = cell_content(row, &field.name);
-
-                let mut cell = container
-                    .min_w(*width + px(22.))
-                    .w(*width + px(22.))
-                    .child(value)
-                    .px_2()
-                    .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
-                    .border_color(theme.colors().border);
-
-                if is_header {
-                    cell = cell.border_1().bg(theme.colors().border_focused)
-                } else {
-                    cell = cell.border_1()
-                }
-                cell
-            })
-            .collect::<Vec<_>>();
-
-        let mut total_width = px(0.);
-        for width in self.widths.iter() {
-            // Width fudge factor: border + 2 (heading), padding
-            total_width += *width + px(22.);
-        }
-
-        h_flex()
-            .w(total_width)
-            .children(row_cells)
-            .into_any_element()
-    }
-}
-
-impl SupportsClipboard for TableView {
-    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
-        Some(self.cached_clipboard_content.clone())
-    }
-
-    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
-        true
-    }
-}
-
-/// Userspace error from the kernel
-pub struct ErrorView {
-    pub ename: String,
-    pub evalue: String,
-    pub traceback: TerminalOutput,
-}
-
-impl ErrorView {
-    fn render(&self, cx: &mut ViewContext<ExecutionView>) -> Option<AnyElement> {
-        let theme = cx.theme();
-
-        let padding = cx.line_height() / 2.;
-
-        Some(
-            v_flex()
-                .gap_3()
-                .child(
-                    h_flex()
-                        .font_buffer(cx)
-                        .child(
-                            Label::new(format!("{}: ", self.ename.clone()))
-                                // .size(LabelSize::Large)
-                                .color(Color::Error)
-                                .weight(FontWeight::BOLD),
-                        )
-                        .child(
-                            Label::new(self.evalue.clone())
-                                // .size(LabelSize::Large)
-                                .weight(FontWeight::BOLD),
-                        ),
-                )
-                .child(
-                    div()
-                        .w_full()
-                        .px(padding)
-                        .py(padding)
-                        .border_l_1()
-                        .border_color(theme.status().error_border)
-                        .child(self.traceback.render(cx)),
-                )
-                .into_any_element(),
-        )
-    }
-}
-
-pub struct MarkdownView {
-    raw_text: String,
-    contents: Option<ParsedMarkdown>,
-    parsing_markdown_task: Option<Task<Result<()>>>,
-}
-
-impl MarkdownView {
-    pub fn from(text: String, cx: &mut ViewContext<Self>) -> Self {
-        let task = cx.spawn(|markdown_view, mut cx| {
-            let text = text.clone();
-            let parsed = cx
-                .background_executor()
-                .spawn(async move { parse_markdown(&text, None, None).await });
-
-            async move {
-                let content = parsed.await;
-
-                markdown_view.update(&mut cx, |markdown, cx| {
-                    markdown.parsing_markdown_task.take();
-                    markdown.contents = Some(content);
-                    cx.notify();
-                })
-            }
-        });
-
-        Self {
-            raw_text: text.clone(),
-            contents: None,
-            parsing_markdown_task: Some(task),
-        }
-    }
-}
-
-impl SupportsClipboard for MarkdownView {
-    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
-        Some(ClipboardItem::new_string(self.raw_text.clone()))
-    }
-
-    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
-        true
-    }
-}
-
-impl Render for MarkdownView {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let Some(parsed) = self.contents.as_ref() else {
-            return div().into_any_element();
-        };
-
-        let mut markdown_render_context =
-            markdown_preview::markdown_renderer::RenderContext::new(None, cx);
-
-        v_flex()
-            .gap_3()
-            .py_4()
-            .children(parsed.children.iter().map(|child| {
-                div().relative().child(
-                    div()
-                        .relative()
-                        .child(render_markdown_block(child, &mut markdown_render_context)),
-                )
-            }))
-            .into_any_element()
-    }
-}
-
 pub struct Output {
     content: OutputContent,
     display_id: Option<String>,
@@ -573,6 +151,9 @@ pub enum ExecutionStatus {
     Restarting,
 }
 
+/// An ExecutionView shows the outputs of an execution.
+/// It can hold zero or more outputs, which the user
+/// sees as "the output" for a single execution.
 pub struct ExecutionView {
     pub outputs: Vec<Output>,
     pub status: ExecutionStatus,

crates/repl/src/outputs/image.rs 🔗

@@ -0,0 +1,92 @@
+use anyhow::Result;
+use base64::prelude::*;
+use gpui::{
+    img, AnyElement, ClipboardItem, Image, ImageFormat, Pixels, RenderImage, WindowContext,
+};
+use std::sync::Arc;
+use ui::{div, prelude::*, IntoElement, Styled};
+
+use crate::outputs::SupportsClipboard;
+
+/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
+pub struct ImageView {
+    clipboard_image: Arc<Image>,
+    height: u32,
+    width: u32,
+    image: Arc<RenderImage>,
+}
+
+impl ImageView {
+    pub fn from(base64_encoded_data: &str) -> Result<Self> {
+        let bytes = BASE64_STANDARD.decode(base64_encoded_data)?;
+
+        let format = image::guess_format(&bytes)?;
+        let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
+
+        // Convert from RGBA to BGRA.
+        for pixel in data.chunks_exact_mut(4) {
+            pixel.swap(0, 2);
+        }
+
+        let height = data.height();
+        let width = data.width();
+
+        let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]);
+
+        let format = match format {
+            image::ImageFormat::Png => ImageFormat::Png,
+            image::ImageFormat::Jpeg => ImageFormat::Jpeg,
+            image::ImageFormat::Gif => ImageFormat::Gif,
+            image::ImageFormat::WebP => ImageFormat::Webp,
+            image::ImageFormat::Tiff => ImageFormat::Tiff,
+            image::ImageFormat::Bmp => ImageFormat::Bmp,
+            _ => {
+                return Err(anyhow::anyhow!("unsupported image format"));
+            }
+        };
+
+        // Convert back to a GPUI image for use with the clipboard
+        let clipboard_image = Arc::new(Image {
+            format,
+            bytes,
+            id: gpui_image_data.id.0 as u64,
+        });
+
+        return Ok(ImageView {
+            clipboard_image,
+            height,
+            width,
+            image: Arc::new(gpui_image_data),
+        });
+    }
+
+    pub fn render(&self, cx: &mut WindowContext) -> AnyElement {
+        let line_height = cx.line_height();
+
+        let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
+            let height = u8::MAX as f32 * line_height.0;
+            let width = self.width as f32 * height / self.height as f32;
+            (height, width)
+        } else {
+            (self.height as f32, self.width as f32)
+        };
+
+        let image = self.image.clone();
+
+        div()
+            .h(Pixels(height))
+            .w(Pixels(width))
+            .child(img(image))
+            .into_any_element()
+    }
+}
+
+impl SupportsClipboard for ImageView {
+    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
+        Some(ClipboardItem::new_image(self.clipboard_image.as_ref()))
+    }
+
+    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
+        true
+    }
+}

crates/repl/src/outputs/markdown.rs 🔗

@@ -0,0 +1,75 @@
+use anyhow::Result;
+use gpui::{div, prelude::*, ClipboardItem, Task, ViewContext, WindowContext};
+use markdown_preview::{
+    markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
+    markdown_renderer::render_markdown_block,
+};
+use ui::v_flex;
+
+use crate::outputs::SupportsClipboard;
+
+pub struct MarkdownView {
+    raw_text: String,
+    contents: Option<ParsedMarkdown>,
+    parsing_markdown_task: Option<Task<Result<()>>>,
+}
+
+impl MarkdownView {
+    pub fn from(text: String, cx: &mut ViewContext<Self>) -> Self {
+        let task = cx.spawn(|markdown_view, mut cx| {
+            let text = text.clone();
+            let parsed = cx
+                .background_executor()
+                .spawn(async move { parse_markdown(&text, None, None).await });
+
+            async move {
+                let content = parsed.await;
+
+                markdown_view.update(&mut cx, |markdown, cx| {
+                    markdown.parsing_markdown_task.take();
+                    markdown.contents = Some(content);
+                    cx.notify();
+                })
+            }
+        });
+
+        Self {
+            raw_text: text.clone(),
+            contents: None,
+            parsing_markdown_task: Some(task),
+        }
+    }
+}
+
+impl SupportsClipboard for MarkdownView {
+    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
+        Some(ClipboardItem::new_string(self.raw_text.clone()))
+    }
+
+    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
+        true
+    }
+}
+
+impl Render for MarkdownView {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let Some(parsed) = self.contents.as_ref() else {
+            return div().into_any_element();
+        };
+
+        let mut markdown_render_context =
+            markdown_preview::markdown_renderer::RenderContext::new(None, cx);
+
+        v_flex()
+            .gap_3()
+            .py_4()
+            .children(parsed.children.iter().map(|child| {
+                div().relative().child(
+                    div()
+                        .relative()
+                        .child(render_markdown_block(child, &mut markdown_render_context)),
+                )
+            }))
+            .into_any_element()
+    }
+}

crates/repl/src/stdio.rs → crates/repl/src/outputs/plain.rs 🔗

@@ -1,4 +1,3 @@
-use crate::outputs::{ExecutionView, SupportsClipboard};
 use alacritty_terminal::{grid::Dimensions as _, term::Config, vte::ansi::Processor};
 use gpui::{canvas, size, AnyElement, ClipboardItem, FontStyle, TextStyle, WhiteSpace};
 use settings::Settings as _;
@@ -6,7 +5,9 @@ use std::mem;
 use terminal::ZedListener;
 use terminal_view::terminal_element::TerminalElement;
 use theme::ThemeSettings;
-use ui::{prelude::*, IntoElement, ViewContext};
+use ui::{prelude::*, IntoElement};
+
+use crate::outputs::SupportsClipboard;
 
 /// Implements the most basic of terminal output for use by Jupyter outputs
 /// whether:
@@ -119,7 +120,7 @@ impl TerminalOutput {
         }
     }
 
-    pub fn render(&self, cx: &mut ViewContext<ExecutionView>) -> AnyElement {
+    pub fn render(&self, cx: &mut WindowContext) -> AnyElement {
         let text_style = text_style(cx);
         let text_system = cx.text_system();
 

crates/repl/src/outputs/table.rs 🔗

@@ -0,0 +1,293 @@
+//! # Table Output for REPL
+//!
+//! This module provides functionality to render tabular data in Zed's REPL output.
+//!
+//! It supports the [Frictionless Data Table Schema](https://specs.frictionlessdata.io/table-schema/)
+//! for data interchange, implemented by Pandas in Python and Polars for Deno.
+//!
+//! # Python Example
+//!
+//! Tables can be created and displayed in two main ways:
+//!
+//! 1. Using raw JSON data conforming to the Tabular Data Resource specification.
+//! 2. Using Pandas DataFrames (in Python kernels).
+//!
+//! ## Raw JSON Method
+//!
+//! To create a table using raw JSON, you need to provide a JSON object that conforms
+//! to the Tabular Data Resource specification. Here's an example:
+//!
+//! ```json
+//! {
+//!     "schema": {
+//!         "fields": [
+//!             {"name": "id", "type": "integer"},
+//!             {"name": "name", "type": "string"},
+//!             {"name": "age", "type": "integer"}
+//!         ]
+//!     },
+//!     "data": [
+//!         {"id": 1, "name": "Alice", "age": 30},
+//!         {"id": 2, "name": "Bob", "age": 28},
+//!         {"id": 3, "name": "Charlie", "age": 35}
+//!     ]
+//! }
+//! ```
+//!
+//! ## Pandas Method
+//!
+//! To create a table using Pandas in a Python kernel, you can use the following steps:
+//!
+//! ```python
+//! import pandas as pd
+//!
+//! # Enable table schema output
+//! pd.set_option('display.html.table_schema', True)
+//!
+//! # Create a DataFrame
+//! df = pd.DataFrame({
+//!     'id': [1, 2, 3],
+//!     'name': ['Alice', 'Bob', 'Charlie'],
+//!     'age': [30, 28, 35]
+//! })
+//!
+//! # Display the DataFrame
+//! display(df)
+//! ```
+use gpui::{AnyElement, ClipboardItem, TextRun};
+use runtimelib::datatable::TableSchema;
+use runtimelib::media::datatable::TabularDataResource;
+use serde_json::Value;
+use settings::Settings;
+use theme::ThemeSettings;
+use ui::{div, prelude::*, v_flex, IntoElement, Styled};
+
+use crate::outputs::SupportsClipboard;
+
+/// TableView renders a static table inline in a buffer.
+/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
+pub struct TableView {
+    pub table: TabularDataResource,
+    pub widths: Vec<Pixels>,
+    cached_clipboard_content: ClipboardItem,
+}
+
+fn cell_content(row: &Value, field: &str) -> String {
+    match row.get(&field) {
+        Some(Value::String(s)) => s.clone(),
+        Some(Value::Number(n)) => n.to_string(),
+        Some(Value::Bool(b)) => b.to_string(),
+        Some(Value::Array(arr)) => format!("{:?}", arr),
+        Some(Value::Object(obj)) => format!("{:?}", obj),
+        Some(Value::Null) | None => String::new(),
+    }
+}
+
+// Declare constant for the padding multiple on the line height
+const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5;
+
+impl TableView {
+    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();
+        let text_style = cx.text_style();
+        let text_font = ThemeSettings::get_global(cx).buffer_font.clone();
+        let font_size = ThemeSettings::get_global(cx).buffer_font_size;
+        let mut runs = [TextRun {
+            len: 0,
+            font: text_font,
+            color: text_style.color,
+            background_color: None,
+            underline: None,
+            strikethrough: None,
+        }];
+
+        for field in table.schema.fields.iter() {
+            runs[0].len = field.name.len();
+            let mut width = text_system
+                .layout_line(&field.name, font_size, &runs)
+                .map(|layout| layout.width)
+                .unwrap_or(px(0.));
+
+            let Some(data) = table.data.as_ref() else {
+                widths.push(width);
+                continue;
+            };
+
+            for row in data {
+                let content = cell_content(&row, &field.name);
+                runs[0].len = content.len();
+                let cell_width = cx
+                    .text_system()
+                    .layout_line(&content, font_size, &runs)
+                    .map(|layout| layout.width)
+                    .unwrap_or(px(0.));
+
+                width = width.max(cell_width)
+            }
+
+            widths.push(width)
+        }
+
+        let cached_clipboard_content = Self::create_clipboard_content(&table);
+
+        Self {
+            table,
+            widths,
+            cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
+        }
+    }
+
+    fn escape_markdown(s: &str) -> String {
+        s.replace('|', "\\|")
+            .replace('*', "\\*")
+            .replace('_', "\\_")
+            .replace('`', "\\`")
+            .replace('[', "\\[")
+            .replace(']', "\\]")
+            .replace('<', "&lt;")
+            .replace('>', "&gt;")
+    }
+
+    fn create_clipboard_content(table: &TabularDataResource) -> String {
+        let data = match table.data.as_ref() {
+            Some(data) => data,
+            None => &Vec::new(),
+        };
+        let schema = table.schema.clone();
+
+        let mut markdown = format!(
+            "| {} |\n",
+            table
+                .schema
+                .fields
+                .iter()
+                .map(|field| field.name.clone())
+                .collect::<Vec<_>>()
+                .join(" | ")
+        );
+
+        markdown.push_str("|---");
+        for _ in 1..table.schema.fields.len() {
+            markdown.push_str("|---");
+        }
+        markdown.push_str("|\n");
+
+        let body = data
+            .iter()
+            .map(|record: &Value| {
+                let row_content = schema
+                    .fields
+                    .iter()
+                    .map(|field| Self::escape_markdown(&cell_content(record, &field.name)))
+                    .collect::<Vec<_>>();
+
+                row_content.join(" | ")
+            })
+            .collect::<Vec<String>>();
+
+        for row in body {
+            markdown.push_str(&format!("| {} |\n", row));
+        }
+
+        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,
+        is_header: bool,
+        row: &Value,
+        cx: &WindowContext,
+    ) -> AnyElement {
+        let theme = cx.theme();
+
+        let line_height = cx.line_height();
+
+        let row_cells = schema
+            .fields
+            .iter()
+            .zip(self.widths.iter())
+            .map(|(field, width)| {
+                let container = match field.field_type {
+                    runtimelib::datatable::FieldType::String => div(),
+
+                    runtimelib::datatable::FieldType::Number
+                    | runtimelib::datatable::FieldType::Integer
+                    | runtimelib::datatable::FieldType::Date
+                    | runtimelib::datatable::FieldType::Time
+                    | runtimelib::datatable::FieldType::Datetime
+                    | runtimelib::datatable::FieldType::Year
+                    | runtimelib::datatable::FieldType::Duration
+                    | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
+
+                    _ => div(),
+                };
+
+                let value = cell_content(row, &field.name);
+
+                let mut cell = container
+                    .min_w(*width + px(22.))
+                    .w(*width + px(22.))
+                    .child(value)
+                    .px_2()
+                    .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
+                    .border_color(theme.colors().border);
+
+                if is_header {
+                    cell = cell.border_1().bg(theme.colors().border_focused)
+                } else {
+                    cell = cell.border_1()
+                }
+                cell
+            })
+            .collect::<Vec<_>>();
+
+        let mut total_width = px(0.);
+        for width in self.widths.iter() {
+            // Width fudge factor: border + 2 (heading), padding
+            total_width += *width + px(22.);
+        }
+
+        h_flex()
+            .w(total_width)
+            .children(row_cells)
+            .into_any_element()
+    }
+}
+
+impl SupportsClipboard for TableView {
+    fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
+        Some(self.cached_clipboard_content.clone())
+    }
+
+    fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
+        true
+    }
+}

crates/repl/src/outputs/user_error.rs 🔗

@@ -0,0 +1,49 @@
+use gpui::{AnyElement, FontWeight, WindowContext};
+use ui::{h_flex, prelude::*, v_flex, Label};
+
+use crate::outputs::plain::TerminalOutput;
+
+/// Userspace error from the kernel
+pub struct ErrorView {
+    pub ename: String,
+    pub evalue: String,
+    pub traceback: TerminalOutput,
+}
+
+impl ErrorView {
+    pub fn render(&self, cx: &mut WindowContext) -> Option<AnyElement> {
+        let theme = cx.theme();
+
+        let padding = cx.line_height() / 2.;
+
+        Some(
+            v_flex()
+                .gap_3()
+                .child(
+                    h_flex()
+                        .font_buffer(cx)
+                        .child(
+                            Label::new(format!("{}: ", self.ename.clone()))
+                                // .size(LabelSize::Large)
+                                .color(Color::Error)
+                                .weight(FontWeight::BOLD),
+                        )
+                        .child(
+                            Label::new(self.evalue.clone())
+                                // .size(LabelSize::Large)
+                                .weight(FontWeight::BOLD),
+                        ),
+                )
+                .child(
+                    div()
+                        .w_full()
+                        .px(padding)
+                        .py(padding)
+                        .border_l_1()
+                        .border_color(theme.status().error_border)
+                        .child(self.traceback.render(cx)),
+                )
+                .into_any_element(),
+        )
+    }
+}

crates/repl/src/repl.rs 🔗

@@ -6,7 +6,6 @@ mod repl_editor;
 mod repl_sessions_ui;
 mod repl_store;
 mod session;
-mod stdio;
 
 use std::{sync::Arc, time::Duration};
 

crates/repl/src/session.rs 🔗

@@ -1,9 +1,9 @@
 use crate::components::KernelListItem;
+use crate::KernelStatus;
 use crate::{
     kernels::{Kernel, KernelSpecification, RunningKernel},
     outputs::{ExecutionStatus, ExecutionView},
 };
-use crate::{stdio, KernelStatus};
 use client::telemetry::Telemetry;
 use collections::{HashMap, HashSet};
 use editor::{
@@ -115,7 +115,7 @@ impl EditorBlock {
     ) -> RenderBlock {
         let render = move |cx: &mut BlockContext| {
             let execution_view = execution_view.clone();
-            let text_style = stdio::text_style(cx);
+            let text_style = crate::outputs::plain::text_style(cx);
 
             let gutter = cx.gutter_dimensions;