From 26d943287bd720ed504e09f4eb7301101b114151 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Mon, 26 Aug 2024 18:03:06 -0700 Subject: [PATCH] REPL: Refactor output (#16927) Shuffle `outputs.rs` into individual `outputs/*.rs` files and start documenting them more. Release Notes: - N/A --- crates/repl/src/outputs.rs | 455 +----------------- crates/repl/src/outputs/image.rs | 92 ++++ crates/repl/src/outputs/markdown.rs | 75 +++ .../repl/src/{stdio.rs => 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(-) create mode 100644 crates/repl/src/outputs/image.rs create mode 100644 crates/repl/src/outputs/markdown.rs rename crates/repl/src/{stdio.rs => outputs/plain.rs} (97%) create mode 100644 crates/repl/src/outputs/table.rs create mode 100644 crates/repl/src/outputs/user_error.rs diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 5a04ff35bddd50ebe7abba6267db7dcb505a7364..5865afb10ef9013eaa9bc5d964b16ee944a78c3f 100644 --- a/crates/repl/src/outputs.rs +++ b/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, - height: u32, - width: u32, - image: Arc, -} - -impl ImageView { - fn render(&self, cx: &ViewContext) -> 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 { - 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 { - 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, - 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('<', "<") - .replace('>', ">") - } - - 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::>() - .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::>(); - - row_content.join(" | ") - }) - .collect::>(); - - for row in body { - markdown.push_str(&format!("| {} |\n", row)); - } - - markdown - } - - pub fn render(&self, cx: &ViewContext) -> 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, - ) -> 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::>(); - - 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 { - 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) -> Option { - 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, - parsing_markdown_task: Option>>, -} - -impl MarkdownView { - pub fn from(text: String, cx: &mut ViewContext) -> 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 { - 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) -> 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, @@ -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, pub status: ExecutionStatus, diff --git a/crates/repl/src/outputs/image.rs b/crates/repl/src/outputs/image.rs new file mode 100644 index 0000000000000000000000000000000000000000..3bcb94a4187caa0428064e7d8ca1350f1f94e9c1 --- /dev/null +++ b/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, + height: u32, + width: u32, + image: Arc, +} + +impl ImageView { + pub fn from(base64_encoded_data: &str) -> Result { + 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 { + Some(ClipboardItem::new_image(self.clipboard_image.as_ref())) + } + + fn has_clipboard_content(&self, _cx: &WindowContext) -> bool { + true + } +} diff --git a/crates/repl/src/outputs/markdown.rs b/crates/repl/src/outputs/markdown.rs new file mode 100644 index 0000000000000000000000000000000000000000..c7c5e50f09c3933f2b39b8e47935f40447414399 --- /dev/null +++ b/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, + parsing_markdown_task: Option>>, +} + +impl MarkdownView { + pub fn from(text: String, cx: &mut ViewContext) -> 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 { + 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) -> 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() + } +} diff --git a/crates/repl/src/stdio.rs b/crates/repl/src/outputs/plain.rs similarity index 97% rename from crates/repl/src/stdio.rs rename to crates/repl/src/outputs/plain.rs index 47a7ac0f54cf7fef575ca368f8485953dab4307e..ebedda6757b9a9833bca058d93c6bfdc5f2f1ed3 100644 --- a/crates/repl/src/stdio.rs +++ b/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) -> AnyElement { + pub fn render(&self, cx: &mut WindowContext) -> AnyElement { let text_style = text_style(cx); let text_system = cx.text_system(); diff --git a/crates/repl/src/outputs/table.rs b/crates/repl/src/outputs/table.rs new file mode 100644 index 0000000000000000000000000000000000000000..f9238dd5a98b00f5bc68c840dd25e201bba3e6e0 --- /dev/null +++ b/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, + 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('<', "<") + .replace('>', ">") + } + + 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::>() + .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::>(); + + row_content.join(" | ") + }) + .collect::>(); + + 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::>(); + + 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 { + Some(self.cached_clipboard_content.clone()) + } + + fn has_clipboard_content(&self, _cx: &WindowContext) -> bool { + true + } +} diff --git a/crates/repl/src/outputs/user_error.rs b/crates/repl/src/outputs/user_error.rs new file mode 100644 index 0000000000000000000000000000000000000000..00d301c32160a011db69cd4a6a7d794bf12d8784 --- /dev/null +++ b/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 { + 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(), + ) + } +} diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs index 640ec16cea1c8acd90a271712e28a3e005e4e545..b5b791665bca57897bfd82ea24885cead122e31d 100644 --- a/crates/repl/src/repl.rs +++ b/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}; diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 2e8fc4431ed9c555aa7d696a6f45e8d1ed25e8e4..0791532d1ed87bf0854978fd37f5902d90f0e054 100644 --- a/crates/repl/src/session.rs +++ b/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;