repl: Refactor outputs for externalization (#16971)

Working on addressing large outputs, refactored as part of it.



https://github.com/user-attachments/assets/48ea576c-e13a-4d09-b45a-4baa41bf6f72



Release Notes:

- N/A
This commit is contained in:
Kyle Kelley 2024-08-29 12:41:03 -07:00 committed by GitHub
parent 89487772b0
commit 82ceb4c091
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 359 additions and 202 deletions

View file

@ -5,7 +5,6 @@
//!
//! ## Key Components
//!
//! - `Output`: Represents a single output item, which can be of various types.
//! - `OutputContent`: An enum that encapsulates different types of output content.
//! - `ExecutionView`: Manages the display of outputs for a single execution.
//! - `ExecutionStatus`: Represents the current status of an execution.
@ -36,9 +35,12 @@
use std::time::Duration;
use editor::Editor;
use gpui::{
percentage, Animation, AnimationExt, AnyElement, ClipboardItem, Render, Transformation, View,
percentage, Animation, AnimationExt, AnyElement, ClipboardItem, Model, Render, Transformation,
View, WeakView,
};
use language::Buffer;
use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
use ui::{div, prelude::*, v_flex, IntoElement, Styled, Tooltip, ViewContext};
@ -56,6 +58,7 @@ use plain::TerminalOutput;
mod user_error;
use user_error::ErrorView;
use workspace::Workspace;
/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
fn rank_mime_type(mimetype: &MimeType) -> usize {
@ -70,105 +73,209 @@ fn rank_mime_type(mimetype: &MimeType) -> usize {
}
}
pub(crate) trait SupportsClipboard {
pub(crate) trait OutputContent {
fn clipboard_content(&self, cx: &WindowContext) -> Option<ClipboardItem>;
fn has_clipboard_content(&self, cx: &WindowContext) -> bool;
}
pub struct Output {
content: OutputContent,
display_id: Option<String>,
}
impl Output {
pub fn new(data: &MimeBundle, display_id: Option<String>, cx: &mut WindowContext) -> Self {
Self {
content: OutputContent::new(data, cx),
display_id,
fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
return false;
}
fn has_buffer_content(&self, _cx: &WindowContext) -> bool {
return false;
}
pub fn from(content: OutputContent) -> Self {
Self {
content,
display_id: None,
}
fn buffer_content(&mut self, _cx: &mut WindowContext) -> Option<Model<Buffer>> {
None
}
}
impl SupportsClipboard for Output {
impl<V: OutputContent + 'static> OutputContent for View<V> {
fn clipboard_content(&self, cx: &WindowContext) -> Option<ClipboardItem> {
match &self.content {
OutputContent::Plain(terminal) => terminal.clipboard_content(cx),
OutputContent::Stream(terminal) => terminal.clipboard_content(cx),
OutputContent::Image(image) => image.clipboard_content(cx),
OutputContent::ErrorOutput(error) => error.traceback.clipboard_content(cx),
OutputContent::Message(_) => None,
OutputContent::Table(table) => table.clipboard_content(cx),
OutputContent::Markdown(markdown) => markdown.read(cx).clipboard_content(cx),
OutputContent::ClearOutputWaitMarker => None,
}
self.read(cx).clipboard_content(cx)
}
fn has_clipboard_content(&self, cx: &WindowContext) -> bool {
match &self.content {
OutputContent::Plain(terminal) => terminal.has_clipboard_content(cx),
OutputContent::Stream(terminal) => terminal.has_clipboard_content(cx),
OutputContent::Image(image) => image.has_clipboard_content(cx),
OutputContent::ErrorOutput(error) => error.traceback.has_clipboard_content(cx),
OutputContent::Message(_) => false,
OutputContent::Table(table) => table.has_clipboard_content(cx),
OutputContent::Markdown(markdown) => markdown.read(cx).has_clipboard_content(cx),
OutputContent::ClearOutputWaitMarker => false,
self.read(cx).has_clipboard_content(cx)
}
fn has_buffer_content(&self, cx: &WindowContext) -> bool {
self.read(cx).has_buffer_content(cx)
}
fn buffer_content(&mut self, cx: &mut WindowContext) -> Option<Model<Buffer>> {
self.update(cx, |item, cx| item.buffer_content(cx))
}
}
pub enum OutputContent {
Plain(TerminalOutput),
Stream(TerminalOutput),
Image(ImageView),
pub enum Output {
Plain {
content: View<TerminalOutput>,
display_id: Option<String>,
},
Stream {
content: View<TerminalOutput>,
},
Image {
content: View<ImageView>,
display_id: Option<String>,
},
ErrorOutput(ErrorView),
Message(String),
Table(TableView),
Markdown(View<MarkdownView>),
Table {
content: View<TableView>,
display_id: Option<String>,
},
Markdown {
content: View<MarkdownView>,
display_id: Option<String>,
},
ClearOutputWaitMarker,
}
impl OutputContent {
fn render(&self, cx: &mut ViewContext<ExecutionView>) -> Option<AnyElement> {
let el = match self {
// Note: in typical frontends we would show the execute_result.execution_count
// Here we can just handle either
Self::Plain(stdio) => Some(stdio.render(cx)),
Self::Markdown(markdown) => Some(markdown.clone().into_any_element()),
Self::Stream(stdio) => Some(stdio.render(cx)),
Self::Image(image) => Some(image.render(cx)),
impl Output {
fn render_output_controls<V: OutputContent + 'static>(
v: View<V>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<ExecutionView>,
) -> Option<AnyElement> {
if !v.has_clipboard_content(cx) && !v.has_buffer_content(cx) {
return None;
}
Some(
h_flex()
.pl_1()
.when(v.has_clipboard_content(cx), |el| {
let v = v.clone();
el.child(
IconButton::new(ElementId::Name("copy-output".into()), IconName::Copy)
.style(ButtonStyle::Transparent)
.tooltip(move |cx| Tooltip::text("Copy Output", cx))
.on_click(cx.listener(move |_, _, cx| {
let clipboard_content = v.clipboard_content(cx);
if let Some(clipboard_content) = clipboard_content.as_ref() {
cx.write_to_clipboard(clipboard_content.clone());
}
})),
)
})
.when(v.has_buffer_content(cx), |el| {
let v = v.clone();
el.child(
IconButton::new(
ElementId::Name("open-in-buffer".into()),
IconName::FileText,
)
.style(ButtonStyle::Transparent)
.tooltip(move |cx| Tooltip::text("Open in Buffer", cx))
.on_click(cx.listener({
let workspace = workspace.clone();
move |_, _, cx| {
let buffer_content =
v.update(cx, |item, cx| item.buffer_content(cx));
if let Some(buffer_content) = buffer_content.as_ref() {
let buffer = buffer_content.clone();
let editor = Box::new(cx.new_view(|cx| {
Editor::for_buffer(buffer.clone(), None, cx)
}));
workspace
.update(cx, |workspace, cx| {
workspace
.add_item_to_active_pane(editor, None, true, cx);
})
.ok();
}
}
})),
)
})
.into_any_element(),
)
}
fn render(
&self,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<ExecutionView>,
) -> impl IntoElement {
let content = match self {
Self::Plain { content, .. } => Some(content.clone().into_any_element()),
Self::Markdown { content, .. } => Some(content.clone().into_any_element()),
Self::Stream { content, .. } => Some(content.clone().into_any_element()),
Self::Image { content, .. } => Some(content.clone().into_any_element()),
Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
Self::Table(table) => Some(table.render(cx)),
Self::Table { content, .. } => Some(content.clone().into_any_element()),
Self::ErrorOutput(error_view) => error_view.render(cx),
Self::ClearOutputWaitMarker => None,
};
el
h_flex()
.w_full()
.items_start()
.child(div().flex_1().children(content))
.children(match self {
Self::Plain { content, .. } => {
Self::render_output_controls(content.clone(), workspace.clone(), cx)
}
Self::Markdown { content, .. } => {
Self::render_output_controls(content.clone(), workspace.clone(), cx)
}
Self::Stream { content, .. } => {
Self::render_output_controls(content.clone(), workspace.clone(), cx)
}
Self::Image { content, .. } => {
Self::render_output_controls(content.clone(), workspace.clone(), cx)
}
Self::ErrorOutput(err) => {
Self::render_output_controls(err.traceback.clone(), workspace.clone(), cx)
}
Self::Message(_) => None,
Self::Table { content, .. } => {
Self::render_output_controls(content.clone(), workspace.clone(), cx)
}
Self::ClearOutputWaitMarker => None,
})
}
pub fn new(data: &MimeBundle, cx: &mut WindowContext) -> Self {
pub fn display_id(&self) -> Option<String> {
match self {
Output::Plain { display_id, .. } => display_id.clone(),
Output::Stream { .. } => None,
Output::Image { display_id, .. } => display_id.clone(),
Output::ErrorOutput(_) => None,
Output::Message(_) => None,
Output::Table { display_id, .. } => display_id.clone(),
Output::Markdown { display_id, .. } => display_id.clone(),
Output::ClearOutputWaitMarker => None,
}
}
pub fn new(data: &MimeBundle, display_id: Option<String>, cx: &mut WindowContext) -> Self {
match data.richest(rank_mime_type) {
Some(MimeType::Plain(text)) => OutputContent::Plain(TerminalOutput::from(text, cx)),
Some(MimeType::Plain(text)) => Output::Plain {
content: cx.new_view(|cx| TerminalOutput::from(text, cx)),
display_id,
},
Some(MimeType::Markdown(text)) => {
let view = cx.new_view(|cx| MarkdownView::from(text.clone(), cx));
OutputContent::Markdown(view)
Output::Markdown {
content: view,
display_id,
}
}
Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) {
Ok(view) => OutputContent::Image(view),
Err(error) => OutputContent::Message(format!("Failed to load image: {}", error)),
Ok(view) => Output::Image {
content: cx.new_view(|_| view),
display_id,
},
Err(error) => Output::Message(format!("Failed to load image: {}", error)),
},
Some(MimeType::DataTable(data)) => Output::Table {
content: cx.new_view(|cx| TableView::new(data, cx)),
display_id,
},
Some(MimeType::DataTable(data)) => {
OutputContent::Table(TableView::new(data.clone(), cx))
}
// Any other media types are not supported
_ => OutputContent::Message("Unsupported media type".to_string()),
_ => Output::Message("Unsupported media type".to_string()),
}
}
}
@ -191,13 +298,20 @@ pub enum ExecutionStatus {
/// It can hold zero or more outputs, which the user
/// sees as "the output" for a single execution.
pub struct ExecutionView {
#[allow(unused)]
workspace: WeakView<Workspace>,
pub outputs: Vec<Output>,
pub status: ExecutionStatus,
}
impl ExecutionView {
pub fn new(status: ExecutionStatus, _cx: &mut ViewContext<Self>) -> Self {
pub fn new(
status: ExecutionStatus,
workspace: WeakView<Workspace>,
_cx: &mut ViewContext<Self>,
) -> Self {
Self {
workspace,
outputs: Default::default(),
status,
}
@ -217,20 +331,20 @@ impl ExecutionView {
JupyterMessageContent::StreamContent(result) => {
// Previous stream data will combine together, handling colors, carriage returns, etc
if let Some(new_terminal) = self.apply_terminal_text(&result.text, cx) {
Output::from(new_terminal)
new_terminal
} else {
return;
}
}
JupyterMessageContent::ErrorOutput(result) => {
let mut terminal = TerminalOutput::new(cx);
terminal.append_text(&result.traceback.join("\n"));
let terminal =
cx.new_view(|cx| TerminalOutput::from(&result.traceback.join("\n"), cx));
Output::from(OutputContent::ErrorOutput(ErrorView {
Output::ErrorOutput(ErrorView {
ename: result.ename.clone(),
evalue: result.evalue.clone(),
traceback: terminal,
}))
})
}
JupyterMessageContent::ExecuteReply(reply) => {
for payload in reply.payload.iter() {
@ -271,7 +385,7 @@ impl ExecutionView {
}
// Create a marker to clear the output after we get in a new output
Output::from(OutputContent::ClearOutputWaitMarker)
Output::ClearOutputWaitMarker
}
JupyterMessageContent::Status(status) => {
match status.execution_state {
@ -290,7 +404,7 @@ impl ExecutionView {
// Check for a clear output marker as the previous output, so we can clear it out
if let Some(output) = self.outputs.last() {
if let OutputContent::ClearOutputWaitMarker = output.content {
if let Output::ClearOutputWaitMarker = output {
self.outputs.clear();
}
}
@ -309,9 +423,9 @@ impl ExecutionView {
let mut any = false;
self.outputs.iter_mut().for_each(|output| {
if let Some(other_display_id) = output.display_id.as_ref() {
if let Some(other_display_id) = output.display_id().as_ref() {
if other_display_id == display_id {
output.content = OutputContent::new(data, cx);
*output = Output::new(data, Some(display_id.to_owned()), cx);
any = true;
}
}
@ -322,33 +436,29 @@ impl ExecutionView {
}
}
fn apply_terminal_text(
&mut self,
text: &str,
cx: &mut ViewContext<Self>,
) -> Option<OutputContent> {
fn apply_terminal_text(&mut self, text: &str, cx: &mut ViewContext<Self>) -> Option<Output> {
if let Some(last_output) = self.outputs.last_mut() {
match &mut last_output.content {
OutputContent::Stream(last_stream) => {
last_stream.append_text(text);
match last_output {
Output::Stream {
content: last_stream,
} => {
// Don't need to add a new output, we already have a terminal output
// and can just update the most recent terminal output
last_stream.update(cx, |last_stream, cx| {
last_stream.append_text(text, cx);
cx.notify();
});
return None;
}
// Edge case note: a clear output marker
OutputContent::ClearOutputWaitMarker => {
// Edge case note: a clear output marker is handled by the caller
// since we will return a new output at the end here as a new terminal output
}
// A different output type is "in the way", so we need to create a new output,
// which is the same as having no prior output
// which is the same as having no prior stream/terminal text
_ => {}
}
}
let mut new_terminal = TerminalOutput::new(cx);
new_terminal.append_text(text);
Some(OutputContent::Stream(new_terminal))
Some(Output::Stream {
content: cx.new_view(|cx| TerminalOutput::from(text, cx)),
})
}
}
@ -405,42 +515,11 @@ impl Render for ExecutionView {
div()
.w_full()
.children(self.outputs.iter().enumerate().map(|(index, output)| {
h_flex()
.w_full()
.items_start()
.child(
div().flex_1().child(
output
.content
.render(cx)
.unwrap_or_else(|| div().into_any_element()),
),
.children(
self.outputs
.iter()
.map(|output| output.render(self.workspace.clone(), cx)),
)
.when(output.has_clipboard_content(cx), |el| {
let clipboard_content = output.clipboard_content(cx);
el.child(
div().pl_1().child(
IconButton::new(
ElementId::Name(format!("copy-output-{}", index).into()),
IconName::Copy,
)
.style(ButtonStyle::Transparent)
.tooltip(move |cx| Tooltip::text("Copy Output", cx))
.on_click(cx.listener(
move |_, _, cx| {
if let Some(clipboard_content) = clipboard_content.as_ref()
{
cx.write_to_clipboard(clipboard_content.clone());
// todo!(): let the user know that the content was copied
}
},
)),
),
)
})
}))
.children(match self.status {
ExecutionStatus::Executing => vec![status],
ExecutionStatus::Queued => vec![status],

View file

@ -1,12 +1,10 @@
use anyhow::Result;
use base64::prelude::*;
use gpui::{
img, AnyElement, ClipboardItem, Image, ImageFormat, Pixels, RenderImage, WindowContext,
};
use gpui::{img, ClipboardItem, Image, ImageFormat, Pixels, RenderImage, WindowContext};
use std::sync::Arc;
use ui::{div, prelude::*, IntoElement, Styled};
use crate::outputs::SupportsClipboard;
use crate::outputs::OutputContent;
/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
pub struct ImageView {
@ -59,8 +57,10 @@ impl ImageView {
image: Arc::new(gpui_image_data),
});
}
}
pub fn render(&self, cx: &mut WindowContext) -> AnyElement {
impl Render for ImageView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let line_height = cx.line_height();
let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
@ -73,15 +73,11 @@ impl ImageView {
let image = self.image.clone();
div()
.h(Pixels(height))
.w(Pixels(width))
.child(img(image))
.into_any_element()
div().h(Pixels(height)).w(Pixels(width)).child(img(image))
}
}
impl SupportsClipboard for ImageView {
impl OutputContent for ImageView {
fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
Some(ClipboardItem::new_image(self.clipboard_image.as_ref()))
}

View file

@ -1,12 +1,13 @@
use anyhow::Result;
use gpui::{div, prelude::*, ClipboardItem, Task, ViewContext, WindowContext};
use gpui::{div, prelude::*, ClipboardItem, Model, Task, ViewContext, WindowContext};
use language::Buffer;
use markdown_preview::{
markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
markdown_renderer::render_markdown_block,
};
use ui::v_flex;
use crate::outputs::SupportsClipboard;
use crate::outputs::OutputContent;
pub struct MarkdownView {
raw_text: String,
@ -41,7 +42,7 @@ impl MarkdownView {
}
}
impl SupportsClipboard for MarkdownView {
impl OutputContent for MarkdownView {
fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
Some(ClipboardItem::new_string(self.raw_text.clone()))
}
@ -49,6 +50,18 @@ impl SupportsClipboard for MarkdownView {
fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
true
}
fn has_buffer_content(&self, _cx: &WindowContext) -> bool {
true
}
fn buffer_content(&mut self, cx: &mut WindowContext) -> Option<Model<Buffer>> {
let buffer = cx.new_model(|cx| {
// todo!(): Bring in the language registry so we can set the language to markdown
Buffer::local(self.raw_text.clone(), cx).with_language(language::PLAIN_TEXT.clone(), cx)
});
Some(buffer)
}
}
impl Render for MarkdownView {

View file

@ -15,8 +15,14 @@
//! - Error tracebacks
//!
use alacritty_terminal::{grid::Dimensions as _, term::Config, vte::ansi::Processor};
use gpui::{canvas, size, AnyElement, ClipboardItem, FontStyle, TextStyle, WhiteSpace};
use alacritty_terminal::{
grid::Dimensions as _,
index::{Column, Line, Point},
term::Config,
vte::ansi::Processor,
};
use gpui::{canvas, size, ClipboardItem, FontStyle, Model, TextStyle, WhiteSpace};
use language::Buffer;
use settings::Settings as _;
use std::mem;
use terminal::ZedListener;
@ -24,7 +30,7 @@ use terminal_view::terminal_element::TerminalElement;
use theme::ThemeSettings;
use ui::{prelude::*, IntoElement};
use crate::outputs::SupportsClipboard;
use crate::outputs::OutputContent;
/// The `TerminalOutput` struct handles the parsing and rendering of text input,
/// simulating a basic terminal environment within REPL output.
@ -40,6 +46,7 @@ use crate::outputs::SupportsClipboard;
/// supporting ANSI escape sequences for text formatting and colors.
///
pub struct TerminalOutput {
full_buffer: Option<Model<Buffer>>,
/// ANSI escape sequence processor for parsing input text.
parser: Processor,
/// Alacritty terminal instance that manages the terminal state and content.
@ -67,7 +74,6 @@ pub fn text_style(cx: &mut WindowContext) -> TextStyle {
font_fallbacks,
font_size: theme::get_buffer_font_size(cx).into(),
font_style: FontStyle::Normal,
// todo
line_height: cx.line_height().into(),
background_color: Some(theme.colors().terminal_background),
white_space: WhiteSpace::Normal,
@ -128,6 +134,7 @@ impl TerminalOutput {
Self {
parser: Processor::new(),
handler: term,
full_buffer: None,
}
}
@ -145,7 +152,7 @@ impl TerminalOutput {
/// A new instance of `TerminalOutput` containing the provided text.
pub fn from(text: &str, cx: &mut WindowContext) -> Self {
let mut output = Self::new(cx);
output.append_text(text);
output.append_text(text, cx);
output
}
@ -175,7 +182,7 @@ impl TerminalOutput {
/// # Arguments
///
/// * `text` - A string slice containing the text to be appended.
pub fn append_text(&mut self, text: &str) {
pub fn append_text(&mut self, text: &str, cx: &mut WindowContext) {
for byte in text.as_bytes() {
if *byte == b'\n' {
// Dirty (?) hack to move the cursor down
@ -184,17 +191,62 @@ impl TerminalOutput {
} else {
self.parser.advance(&mut self.handler, *byte);
}
}
// self.parser.advance(&mut self.handler, *byte);
// This will keep the buffer up to date, though with some terminal codes it won't be perfect
if let Some(buffer) = self.full_buffer.as_ref() {
buffer.update(cx, |buffer, cx| {
buffer.edit([(buffer.len()..buffer.len(), text)], None, cx);
});
}
}
fn full_text(&self) -> String {
let mut full_text = String::new();
// Get the total number of lines, including history
let total_lines = self.handler.grid().total_lines();
let visible_lines = self.handler.screen_lines();
let history_lines = total_lines - visible_lines;
// Capture history lines in correct order (oldest to newest)
for line in (0..history_lines).rev() {
let line_index = Line(-(line as i32) - 1);
let start = Point::new(line_index, Column(0));
let end = Point::new(line_index, Column(self.handler.columns() - 1));
let line_content = self.handler.bounds_to_string(start, end);
if !line_content.trim().is_empty() {
full_text.push_str(&line_content);
full_text.push('\n');
}
}
// Capture visible lines
for line in 0..visible_lines {
let line_index = Line(line as i32);
let start = Point::new(line_index, Column(0));
let end = Point::new(line_index, Column(self.handler.columns() - 1));
let line_content = self.handler.bounds_to_string(start, end);
if !line_content.trim().is_empty() {
full_text.push_str(&line_content);
full_text.push('\n');
}
}
// Trim any trailing newlines
full_text.trim_end().to_string()
}
}
impl Render for TerminalOutput {
/// Renders the terminal output as a GPUI element.
///
/// Converts the current terminal state into a renderable GPUI element. It handles
/// the layout of the terminal grid, calculates the dimensions of the output, and
/// creates a canvas element that paints the terminal cells and background rectangles.
pub fn render(&self, cx: &mut WindowContext) -> AnyElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let text_style = text_style(cx);
let text_system = cx.text_system();
@ -254,25 +306,31 @@ impl TerminalOutput {
)
// We must set the height explicitly for the editor block to size itself correctly
.h(height)
.into_any_element()
}
}
impl SupportsClipboard for TerminalOutput {
impl OutputContent for TerminalOutput {
fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
let start = alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(0),
alacritty_terminal::index::Column(0),
);
let end = alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(self.handler.screen_lines() as i32 - 1),
alacritty_terminal::index::Column(self.handler.columns() - 1),
);
let text = self.handler.bounds_to_string(start, end);
Some(ClipboardItem::new_string(text.trim().into()))
Some(ClipboardItem::new_string(self.full_text()))
}
fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
true
}
fn has_buffer_content(&self, _cx: &WindowContext) -> bool {
true
}
fn buffer_content(&mut self, cx: &mut WindowContext) -> Option<Model<Buffer>> {
if let Some(_) = self.full_buffer.as_ref() {
return self.full_buffer.clone();
}
let buffer = cx.new_model(|cx| {
Buffer::local(self.full_text(), cx).with_language(language::PLAIN_TEXT.clone(), cx)
});
self.full_buffer = Some(buffer.clone());
Some(buffer)
}
}

View file

@ -62,7 +62,7 @@ use settings::Settings;
use theme::ThemeSettings;
use ui::{div, prelude::*, v_flex, IntoElement, Styled};
use crate::outputs::SupportsClipboard;
use crate::outputs::OutputContent;
/// TableView renders a static table inline in a buffer.
/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
@ -87,7 +87,7 @@ fn cell_content(row: &Value, field: &str) -> String {
const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5;
impl TableView {
pub fn new(table: TabularDataResource, cx: &mut WindowContext) -> Self {
pub fn new(table: &TabularDataResource, cx: &mut WindowContext) -> Self {
let mut widths = Vec::with_capacity(table.schema.fields.len());
let text_system = cx.text_system();
@ -133,7 +133,7 @@ impl TableView {
let cached_clipboard_content = Self::create_clipboard_content(&table);
Self {
table,
table: table.clone(),
widths,
cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
}
@ -194,31 +194,6 @@ impl TableView {
markdown
}
pub fn render(&self, cx: &WindowContext) -> AnyElement {
let data = match &self.table.data {
Some(data) => data,
None => return div().into_any_element(),
};
let mut headings = serde_json::Map::new();
for field in &self.table.schema.fields {
headings.insert(field.name.clone(), Value::String(field.name.clone()));
}
let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx);
let body = data
.iter()
.map(|row| self.render_row(&self.table.schema, false, &row, cx));
v_flex()
.id("table")
.overflow_x_scroll()
.w_full()
.child(header)
.children(body)
.into_any_element()
}
pub fn render_row(
&self,
schema: &TableSchema,
@ -282,7 +257,34 @@ impl TableView {
}
}
impl SupportsClipboard for TableView {
impl Render for TableView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let data = match &self.table.data {
Some(data) => data,
None => return div().into_any_element(),
};
let mut headings = serde_json::Map::new();
for field in &self.table.schema.fields {
headings.insert(field.name.clone(), Value::String(field.name.clone()));
}
let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx);
let body = data
.iter()
.map(|row| self.render_row(&self.table.schema, false, &row, cx));
v_flex()
.id("table")
.overflow_x_scroll()
.w_full()
.child(header)
.children(body)
.into_any_element()
}
}
impl OutputContent for TableView {
fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
Some(self.cached_clipboard_content.clone())
}

View file

@ -1,4 +1,4 @@
use gpui::{AnyElement, FontWeight, WindowContext};
use gpui::{AnyElement, FontWeight, View, WindowContext};
use ui::{h_flex, prelude::*, v_flex, Label};
use crate::outputs::plain::TerminalOutput;
@ -7,7 +7,7 @@ use crate::outputs::plain::TerminalOutput;
pub struct ErrorView {
pub ename: String,
pub evalue: String,
pub traceback: TerminalOutput,
pub traceback: View<TerminalOutput>,
}
impl ErrorView {
@ -41,7 +41,7 @@ impl ErrorView {
.py(padding)
.border_l_1()
.border_color(theme.status().error_border)
.child(self.traceback.render(cx)),
.child(self.traceback.clone()),
)
.into_any_element(),
)

View file

@ -60,7 +60,16 @@ impl EditorBlock {
on_close: CloseBlockFn,
cx: &mut ViewContext<Session>,
) -> anyhow::Result<Self> {
let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx));
let editor = editor
.upgrade()
.ok_or_else(|| anyhow::anyhow!("editor is not open"))?;
let workspace = editor
.read(cx)
.workspace()
.ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
let execution_view =
cx.new_view(|cx| ExecutionView::new(status, workspace.downgrade(), cx));
let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| {
let buffer = editor.buffer().clone();
@ -93,7 +102,7 @@ impl EditorBlock {
let block_id = editor.insert_blocks([block], None, cx)[0];
(block_id, invalidation_anchor)
})?;
});
anyhow::Ok(Self {
code_range,