REPL: Refactor output (#16927)
Shuffle `outputs.rs` into individual `outputs/*.rs` files and start documenting them more. Release Notes: - N/A
This commit is contained in:
parent
bea6786f14
commit
26d943287b
8 changed files with 533 additions and 443 deletions
|
@ -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('<', "<")
|
||||
.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::<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,
|
||||
|
|
92
crates/repl/src/outputs/image.rs
Normal file
92
crates/repl/src/outputs/image.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
75
crates/repl/src/outputs/markdown.rs
Normal file
75
crates/repl/src/outputs/markdown.rs
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
293
crates/repl/src/outputs/table.rs
Normal file
293
crates/repl/src/outputs/table.rs
Normal file
|
@ -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('<', "<")
|
||||
.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::<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
|
||||
}
|
||||
}
|
49
crates/repl/src/outputs/user_error.rs
Normal file
49
crates/repl/src/outputs/user_error.rs
Normal file
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ mod repl_editor;
|
|||
mod repl_sessions_ui;
|
||||
mod repl_store;
|
||||
mod session;
|
||||
mod stdio;
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue