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