repl: Refactor outputs for externalization (#16971)

Kyle Kelley created

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

Change summary

crates/repl/src/outputs.rs            | 347 +++++++++++++++++-----------
crates/repl/src/outputs/image.rs      |  18 
crates/repl/src/outputs/markdown.rs   |  19 +
crates/repl/src/outputs/plain.rs      |  98 ++++++-
crates/repl/src/outputs/table.rs      |  60 ++--
crates/repl/src/outputs/user_error.rs |   6 
crates/repl/src/session.rs            |  13 
7 files changed, 359 insertions(+), 202 deletions(-)

Detailed changes

crates/repl/src/outputs.rs 🔗

@@ -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;
     }
-
-    pub fn from(content: OutputContent) -> Self {
-        Self {
-            content,
-            display_id: None,
-        }
+    fn has_buffer_content(&self, _cx: &WindowContext) -> bool {
+        return false;
+    }
+    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 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, cx: &mut WindowContext) -> Self {
+    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
-                    cx.notify();
+                    // 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()),
-                        ),
-                    )
-                    .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(
+                self.outputs
+                    .iter()
+                    .map(|output| output.render(self.workspace.clone(), cx)),
+            )
             .children(match self.status {
                 ExecutionStatus::Executing => vec![status],
                 ExecutionStatus::Queued => vec![status],

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

@@ -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()))
     }

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

@@ -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 {

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

@@ -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);
             }
+        }
+
+        // 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);
 
-            // self.parser.advance(&mut self.handler, *byte);
+            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)
+    }
 }

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

@@ -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())
     }

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

@@ -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(),
         )

crates/repl/src/session.rs 🔗

@@ -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,