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:
parent
89487772b0
commit
82ceb4c091
7 changed files with 359 additions and 202 deletions
|
@ -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],
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue