outputs.rs

  1//! # REPL Output Module
  2//!
  3//! This module provides the core functionality for handling and displaying
  4//! various types of output from Jupyter kernels.
  5//!
  6//! ## Key Components
  7//!
  8//! - `OutputContent`: An enum that encapsulates different types of output content.
  9//! - `ExecutionView`: Manages the display of outputs for a single execution.
 10//! - `ExecutionStatus`: Represents the current status of an execution.
 11//!
 12//! ## Output Types
 13//!
 14//! The module supports several output types, including:
 15//! - Plain text
 16//! - Markdown
 17//! - Images (PNG and JPEG)
 18//! - Tables
 19//! - Error messages
 20//!
 21//! ## Clipboard Support
 22//!
 23//! Most output types implement the `SupportsClipboard` trait, allowing
 24//! users to easily copy output content to the system clipboard.
 25//!
 26//! ## Rendering
 27//!
 28//! The module provides rendering capabilities for each output type,
 29//! ensuring proper display within the REPL interface.
 30//!
 31//! ## Jupyter Integration
 32//!
 33//! This module is designed to work with Jupyter message protocols,
 34//! interpreting and displaying various types of Jupyter output.
 35
 36use std::time::Duration;
 37
 38use editor::{Editor, MultiBuffer};
 39use gpui::{
 40    percentage, Animation, AnimationExt, AnyElement, ClipboardItem, Entity, Render, Transformation,
 41    WeakEntity,
 42};
 43use language::Buffer;
 44use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
 45use ui::{div, prelude::*, v_flex, Context, IntoElement, Styled, Tooltip, Window};
 46
 47mod image;
 48use image::ImageView;
 49
 50mod markdown;
 51use markdown::MarkdownView;
 52
 53mod table;
 54use table::TableView;
 55
 56pub mod plain;
 57use plain::TerminalOutput;
 58
 59pub(crate) mod user_error;
 60use user_error::ErrorView;
 61use workspace::Workspace;
 62
 63/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
 64fn rank_mime_type(mimetype: &MimeType) -> usize {
 65    match mimetype {
 66        MimeType::DataTable(_) => 6,
 67        MimeType::Png(_) => 4,
 68        MimeType::Jpeg(_) => 3,
 69        MimeType::Markdown(_) => 2,
 70        MimeType::Plain(_) => 1,
 71        // All other media types are not supported in Zed at this time
 72        _ => 0,
 73    }
 74}
 75
 76pub(crate) trait OutputContent {
 77    fn clipboard_content(&self, window: &Window, cx: &App) -> Option<ClipboardItem>;
 78    fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
 79        false
 80    }
 81    fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool {
 82        false
 83    }
 84    fn buffer_content(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Entity<Buffer>> {
 85        None
 86    }
 87}
 88
 89impl<V: OutputContent + 'static> OutputContent for Entity<V> {
 90    fn clipboard_content(&self, window: &Window, cx: &App) -> Option<ClipboardItem> {
 91        self.read(cx).clipboard_content(window, cx)
 92    }
 93
 94    fn has_clipboard_content(&self, window: &Window, cx: &App) -> bool {
 95        self.read(cx).has_clipboard_content(window, cx)
 96    }
 97
 98    fn has_buffer_content(&self, window: &Window, cx: &App) -> bool {
 99        self.read(cx).has_buffer_content(window, cx)
100    }
101
102    fn buffer_content(&mut self, window: &mut Window, cx: &mut App) -> Option<Entity<Buffer>> {
103        self.update(cx, |item, cx| item.buffer_content(window, cx))
104    }
105}
106
107pub enum Output {
108    Plain {
109        content: Entity<TerminalOutput>,
110        display_id: Option<String>,
111    },
112    Stream {
113        content: Entity<TerminalOutput>,
114    },
115    Image {
116        content: Entity<ImageView>,
117        display_id: Option<String>,
118    },
119    ErrorOutput(ErrorView),
120    Message(String),
121    Table {
122        content: Entity<TableView>,
123        display_id: Option<String>,
124    },
125    Markdown {
126        content: Entity<MarkdownView>,
127        display_id: Option<String>,
128    },
129    ClearOutputWaitMarker,
130}
131
132impl Output {
133    fn render_output_controls<V: OutputContent + 'static>(
134        v: Entity<V>,
135        workspace: WeakEntity<Workspace>,
136        window: &mut Window,
137        cx: &mut Context<ExecutionView>,
138    ) -> Option<AnyElement> {
139        if !v.has_clipboard_content(window, cx) && !v.has_buffer_content(window, cx) {
140            return None;
141        }
142
143        Some(
144            h_flex()
145                .pl_1()
146                .when(v.has_clipboard_content(window, cx), |el| {
147                    let v = v.clone();
148                    el.child(
149                        IconButton::new(ElementId::Name("copy-output".into()), IconName::Copy)
150                            .style(ButtonStyle::Transparent)
151                            .tooltip(Tooltip::text("Copy Output"))
152                            .on_click(cx.listener(move |_, _, window, cx| {
153                                let clipboard_content = v.clipboard_content(window, cx);
154
155                                if let Some(clipboard_content) = clipboard_content.as_ref() {
156                                    cx.write_to_clipboard(clipboard_content.clone());
157                                }
158                            })),
159                    )
160                })
161                .when(v.has_buffer_content(window, cx), |el| {
162                    let v = v.clone();
163                    el.child(
164                        IconButton::new(
165                            ElementId::Name("open-in-buffer".into()),
166                            IconName::FileText,
167                        )
168                        .style(ButtonStyle::Transparent)
169                        .tooltip(Tooltip::text("Open in Buffer"))
170                        .on_click(cx.listener({
171                            let workspace = workspace.clone();
172
173                            move |_, _, window, cx| {
174                                let buffer_content =
175                                    v.update(cx, |item, cx| item.buffer_content(window, cx));
176
177                                if let Some(buffer_content) = buffer_content.as_ref() {
178                                    let buffer = buffer_content.clone();
179                                    let editor = Box::new(cx.new(|cx| {
180                                        let multibuffer = cx.new(|cx| {
181                                            let mut multi_buffer =
182                                                MultiBuffer::singleton(buffer.clone(), cx);
183
184                                            multi_buffer.set_title("REPL Output".to_string(), cx);
185                                            multi_buffer
186                                        });
187
188                                        Editor::for_multibuffer(
189                                            multibuffer,
190                                            None,
191                                            false,
192                                            window,
193                                            cx,
194                                        )
195                                    }));
196                                    workspace
197                                        .update(cx, |workspace, cx| {
198                                            workspace.add_item_to_active_pane(
199                                                editor, None, true, window, cx,
200                                            );
201                                        })
202                                        .ok();
203                                }
204                            }
205                        })),
206                    )
207                })
208                .into_any_element(),
209        )
210    }
211
212    pub fn render(
213        &self,
214
215        workspace: WeakEntity<Workspace>,
216        window: &mut Window,
217        cx: &mut Context<ExecutionView>,
218    ) -> impl IntoElement {
219        let content = match self {
220            Self::Plain { content, .. } => Some(content.clone().into_any_element()),
221            Self::Markdown { content, .. } => Some(content.clone().into_any_element()),
222            Self::Stream { content, .. } => Some(content.clone().into_any_element()),
223            Self::Image { content, .. } => Some(content.clone().into_any_element()),
224            Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
225            Self::Table { content, .. } => Some(content.clone().into_any_element()),
226            Self::ErrorOutput(error_view) => error_view.render(window, cx),
227            Self::ClearOutputWaitMarker => None,
228        };
229
230        h_flex()
231            .w_full()
232            .items_start()
233            .child(div().flex_1().children(content))
234            .children(match self {
235                Self::Plain { content, .. } => {
236                    Self::render_output_controls(content.clone(), workspace.clone(), window, cx)
237                }
238                Self::Markdown { content, .. } => {
239                    Self::render_output_controls(content.clone(), workspace.clone(), window, cx)
240                }
241                Self::Stream { content, .. } => {
242                    Self::render_output_controls(content.clone(), workspace.clone(), window, cx)
243                }
244                Self::Image { content, .. } => {
245                    Self::render_output_controls(content.clone(), workspace.clone(), window, cx)
246                }
247                Self::ErrorOutput(err) => Self::render_output_controls(
248                    err.traceback.clone(),
249                    workspace.clone(),
250                    window,
251                    cx,
252                ),
253                Self::Message(_) => None,
254                Self::Table { content, .. } => {
255                    Self::render_output_controls(content.clone(), workspace.clone(), window, cx)
256                }
257                Self::ClearOutputWaitMarker => None,
258            })
259    }
260
261    pub fn display_id(&self) -> Option<String> {
262        match self {
263            Output::Plain { display_id, .. } => display_id.clone(),
264            Output::Stream { .. } => None,
265            Output::Image { display_id, .. } => display_id.clone(),
266            Output::ErrorOutput(_) => None,
267            Output::Message(_) => None,
268            Output::Table { display_id, .. } => display_id.clone(),
269            Output::Markdown { display_id, .. } => display_id.clone(),
270            Output::ClearOutputWaitMarker => None,
271        }
272    }
273
274    pub fn new(
275        data: &MimeBundle,
276        display_id: Option<String>,
277        window: &mut Window,
278        cx: &mut App,
279    ) -> Self {
280        match data.richest(rank_mime_type) {
281            Some(MimeType::Plain(text)) => Output::Plain {
282                content: cx.new(|cx| TerminalOutput::from(text, window, cx)),
283                display_id,
284            },
285            Some(MimeType::Markdown(text)) => {
286                let content = cx.new(|cx| MarkdownView::from(text.clone(), cx));
287                Output::Markdown {
288                    content,
289                    display_id,
290                }
291            }
292            Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) {
293                Ok(view) => Output::Image {
294                    content: cx.new(|_| view),
295                    display_id,
296                },
297                Err(error) => Output::Message(format!("Failed to load image: {}", error)),
298            },
299            Some(MimeType::DataTable(data)) => Output::Table {
300                content: cx.new(|cx| TableView::new(data, window, cx)),
301                display_id,
302            },
303            // Any other media types are not supported
304            _ => Output::Message("Unsupported media type".to_string()),
305        }
306    }
307}
308
309#[derive(Default, Clone, Debug)]
310pub enum ExecutionStatus {
311    #[default]
312    Unknown,
313    ConnectingToKernel,
314    Queued,
315    Executing,
316    Finished,
317    ShuttingDown,
318    Shutdown,
319    KernelErrored(String),
320    Restarting,
321}
322
323/// An ExecutionView shows the outputs of an execution.
324/// It can hold zero or more outputs, which the user
325/// sees as "the output" for a single execution.
326pub struct ExecutionView {
327    #[allow(unused)]
328    workspace: WeakEntity<Workspace>,
329    pub outputs: Vec<Output>,
330    pub status: ExecutionStatus,
331}
332
333impl ExecutionView {
334    pub fn new(
335        status: ExecutionStatus,
336        workspace: WeakEntity<Workspace>,
337        _cx: &mut Context<Self>,
338    ) -> Self {
339        Self {
340            workspace,
341            outputs: Default::default(),
342            status,
343        }
344    }
345
346    /// Accept a Jupyter message belonging to this execution
347    pub fn push_message(
348        &mut self,
349        message: &JupyterMessageContent,
350        window: &mut Window,
351        cx: &mut Context<Self>,
352    ) {
353        let output: Output = match message {
354            JupyterMessageContent::ExecuteResult(result) => Output::new(
355                &result.data,
356                result.transient.as_ref().and_then(|t| t.display_id.clone()),
357                window,
358                cx,
359            ),
360            JupyterMessageContent::DisplayData(result) => Output::new(
361                &result.data,
362                result.transient.as_ref().and_then(|t| t.display_id.clone()),
363                window,
364                cx,
365            ),
366            JupyterMessageContent::StreamContent(result) => {
367                // Previous stream data will combine together, handling colors, carriage returns, etc
368                if let Some(new_terminal) = self.apply_terminal_text(&result.text, window, cx) {
369                    new_terminal
370                } else {
371                    return;
372                }
373            }
374            JupyterMessageContent::ErrorOutput(result) => {
375                let terminal =
376                    cx.new(|cx| TerminalOutput::from(&result.traceback.join("\n"), window, cx));
377
378                Output::ErrorOutput(ErrorView {
379                    ename: result.ename.clone(),
380                    evalue: result.evalue.clone(),
381                    traceback: terminal,
382                })
383            }
384            JupyterMessageContent::ExecuteReply(reply) => {
385                for payload in reply.payload.iter() {
386                    if let runtimelib::Payload::Page { data, .. } = payload {
387                        let output = Output::new(data, None, window, cx);
388                        self.outputs.push(output);
389                    }
390                }
391                cx.notify();
392                return;
393            }
394            JupyterMessageContent::ClearOutput(options) => {
395                if !options.wait {
396                    self.outputs.clear();
397                    cx.notify();
398                    return;
399                }
400
401                // Create a marker to clear the output after we get in a new output
402                Output::ClearOutputWaitMarker
403            }
404            JupyterMessageContent::Status(status) => {
405                match status.execution_state {
406                    ExecutionState::Busy => {
407                        self.status = ExecutionStatus::Executing;
408                    }
409                    ExecutionState::Idle => self.status = ExecutionStatus::Finished,
410                }
411                cx.notify();
412                return;
413            }
414            _msg => {
415                return;
416            }
417        };
418
419        // Check for a clear output marker as the previous output, so we can clear it out
420        if let Some(output) = self.outputs.last() {
421            if let Output::ClearOutputWaitMarker = output {
422                self.outputs.clear();
423            }
424        }
425
426        self.outputs.push(output);
427
428        cx.notify();
429    }
430
431    pub fn update_display_data(
432        &mut self,
433        data: &MimeBundle,
434        display_id: &str,
435        window: &mut Window,
436        cx: &mut Context<Self>,
437    ) {
438        let mut any = false;
439
440        self.outputs.iter_mut().for_each(|output| {
441            if let Some(other_display_id) = output.display_id().as_ref() {
442                if other_display_id == display_id {
443                    *output = Output::new(data, Some(display_id.to_owned()), window, cx);
444                    any = true;
445                }
446            }
447        });
448
449        if any {
450            cx.notify();
451        }
452    }
453
454    fn apply_terminal_text(
455        &mut self,
456        text: &str,
457        window: &mut Window,
458        cx: &mut Context<Self>,
459    ) -> Option<Output> {
460        if let Some(last_output) = self.outputs.last_mut() {
461            if let Output::Stream {
462                content: last_stream,
463            } = last_output
464            {
465                // Don't need to add a new output, we already have a terminal output
466                // and can just update the most recent terminal output
467                last_stream.update(cx, |last_stream, cx| {
468                    last_stream.append_text(text, cx);
469                    cx.notify();
470                });
471                return None;
472            }
473        }
474
475        Some(Output::Stream {
476            content: cx.new(|cx| TerminalOutput::from(text, window, cx)),
477        })
478    }
479}
480
481impl Render for ExecutionView {
482    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
483        let status = match &self.status {
484            ExecutionStatus::ConnectingToKernel => Label::new("Connecting to kernel...")
485                .color(Color::Muted)
486                .into_any_element(),
487            ExecutionStatus::Executing => h_flex()
488                .gap_2()
489                .child(
490                    Icon::new(IconName::ArrowCircle)
491                        .size(IconSize::Small)
492                        .color(Color::Muted)
493                        .with_animation(
494                            "arrow-circle",
495                            Animation::new(Duration::from_secs(3)).repeat(),
496                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
497                        ),
498                )
499                .child(Label::new("Executing...").color(Color::Muted))
500                .into_any_element(),
501            ExecutionStatus::Finished => Icon::new(IconName::Check)
502                .size(IconSize::Small)
503                .into_any_element(),
504            ExecutionStatus::Unknown => Label::new("Unknown status")
505                .color(Color::Muted)
506                .into_any_element(),
507            ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...")
508                .color(Color::Muted)
509                .into_any_element(),
510            ExecutionStatus::Restarting => Label::new("Kernel restarting...")
511                .color(Color::Muted)
512                .into_any_element(),
513            ExecutionStatus::Shutdown => Label::new("Kernel shutdown")
514                .color(Color::Muted)
515                .into_any_element(),
516            ExecutionStatus::Queued => Label::new("Queued...")
517                .color(Color::Muted)
518                .into_any_element(),
519            ExecutionStatus::KernelErrored(error) => Label::new(format!("Kernel error: {}", error))
520                .color(Color::Error)
521                .into_any_element(),
522        };
523
524        if self.outputs.is_empty() {
525            return v_flex()
526                .min_h(window.line_height())
527                .justify_center()
528                .child(status)
529                .into_any_element();
530        }
531
532        div()
533            .w_full()
534            .children(
535                self.outputs
536                    .iter()
537                    .map(|output| output.render(self.workspace.clone(), window, cx)),
538            )
539            .children(match self.status {
540                ExecutionStatus::Executing => vec![status],
541                ExecutionStatus::Queued => vec![status],
542                _ => vec![],
543            })
544            .into_any_element()
545    }
546}