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 std::time::Duration;
|
||||||
|
|
||||||
use crate::stdio::TerminalOutput;
|
|
||||||
use anyhow::Result;
|
|
||||||
use base64::prelude::*;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
img, percentage, Animation, AnimationExt, AnyElement, ClipboardItem, FontWeight, Image,
|
percentage, Animation, AnimationExt, AnyElement, ClipboardItem, Render, Transformation, View,
|
||||||
ImageFormat, Render, RenderImage, Task, TextRun, Transformation, View,
|
|
||||||
};
|
};
|
||||||
use runtimelib::datatable::TableSchema;
|
|
||||||
use runtimelib::media::datatable::TabularDataResource;
|
|
||||||
use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
|
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 ui::{div, prelude::*, v_flex, IntoElement, Styled, Tooltip, ViewContext};
|
||||||
|
|
||||||
use markdown_preview::{
|
mod image;
|
||||||
markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
|
use image::ImageView;
|
||||||
markdown_renderer::render_markdown_block,
|
|
||||||
};
|
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
|
/// 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 {
|
fn rank_mime_type(mimetype: &MimeType) -> usize {
|
||||||
|
@ -39,428 +39,6 @@ pub(crate) trait SupportsClipboard {
|
||||||
fn has_clipboard_content(&self, cx: &WindowContext) -> bool;
|
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 {
|
pub struct Output {
|
||||||
content: OutputContent,
|
content: OutputContent,
|
||||||
display_id: Option<String>,
|
display_id: Option<String>,
|
||||||
|
@ -573,6 +151,9 @@ pub enum ExecutionStatus {
|
||||||
Restarting,
|
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 struct ExecutionView {
|
||||||
pub outputs: Vec<Output>,
|
pub outputs: Vec<Output>,
|
||||||
pub status: ExecutionStatus,
|
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 alacritty_terminal::{grid::Dimensions as _, term::Config, vte::ansi::Processor};
|
||||||
use gpui::{canvas, size, AnyElement, ClipboardItem, FontStyle, TextStyle, WhiteSpace};
|
use gpui::{canvas, size, AnyElement, ClipboardItem, FontStyle, TextStyle, WhiteSpace};
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
|
@ -6,7 +5,9 @@ use std::mem;
|
||||||
use terminal::ZedListener;
|
use terminal::ZedListener;
|
||||||
use terminal_view::terminal_element::TerminalElement;
|
use terminal_view::terminal_element::TerminalElement;
|
||||||
use theme::ThemeSettings;
|
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
|
/// Implements the most basic of terminal output for use by Jupyter outputs
|
||||||
/// whether:
|
/// 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_style = text_style(cx);
|
||||||
let text_system = cx.text_system();
|
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_sessions_ui;
|
||||||
mod repl_store;
|
mod repl_store;
|
||||||
mod session;
|
mod session;
|
||||||
mod stdio;
|
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::components::KernelListItem;
|
use crate::components::KernelListItem;
|
||||||
|
use crate::KernelStatus;
|
||||||
use crate::{
|
use crate::{
|
||||||
kernels::{Kernel, KernelSpecification, RunningKernel},
|
kernels::{Kernel, KernelSpecification, RunningKernel},
|
||||||
outputs::{ExecutionStatus, ExecutionView},
|
outputs::{ExecutionStatus, ExecutionView},
|
||||||
};
|
};
|
||||||
use crate::{stdio, KernelStatus};
|
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -115,7 +115,7 @@ impl EditorBlock {
|
||||||
) -> RenderBlock {
|
) -> RenderBlock {
|
||||||
let render = move |cx: &mut BlockContext| {
|
let render = move |cx: &mut BlockContext| {
|
||||||
let execution_view = execution_view.clone();
|
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;
|
let gutter = cx.gutter_dimensions;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue