1use std::sync::Arc;
2use std::time::Duration;
3
4use crate::stdio::TerminalOutput;
5use anyhow::Result;
6use base64::prelude::*;
7use gpui::{
8 img, percentage, Animation, AnimationExt, AnyElement, FontWeight, ImageData, Render, TextRun,
9 Transformation, View,
10};
11use runtimelib::datatable::TableSchema;
12use runtimelib::media::datatable::TabularDataResource;
13use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
14use serde_json::Value;
15use settings::Settings;
16use theme::ThemeSettings;
17use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext};
18
19/// Given these outputs are destined for the editor with the block decorations API, all of them must report
20/// how many lines they will take up in the editor.
21pub trait LineHeight: Sized {
22 fn num_lines(&self, cx: &mut WindowContext) -> usize;
23}
24
25/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
26fn rank_mime_type(mimetype: &MimeType) -> usize {
27 match mimetype {
28 MimeType::DataTable(_) => 6,
29 MimeType::Png(_) => 4,
30 MimeType::Jpeg(_) => 3,
31 MimeType::Markdown(_) => 2,
32 MimeType::Plain(_) => 1,
33 // All other media types are not supported in Zed at this time
34 _ => 0,
35 }
36}
37
38/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
39pub struct ImageView {
40 height: u32,
41 width: u32,
42 image: Arc<ImageData>,
43}
44
45impl ImageView {
46 fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
47 let line_height = cx.line_height();
48
49 let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
50 let height = u8::MAX as f32 * line_height.0;
51 let width = self.width as f32 * height / self.height as f32;
52 (height, width)
53 } else {
54 (self.height as f32, self.width as f32)
55 };
56
57 let image = self.image.clone();
58
59 div()
60 .h(Pixels(height))
61 .w(Pixels(width))
62 .child(img(image))
63 .into_any_element()
64 }
65
66 fn from(base64_encoded_data: &str) -> Result<Self> {
67 let bytes = BASE64_STANDARD.decode(base64_encoded_data)?;
68
69 let format = image::guess_format(&bytes)?;
70 let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
71
72 // Convert from RGBA to BGRA.
73 for pixel in data.chunks_exact_mut(4) {
74 pixel.swap(0, 2);
75 }
76
77 let height = data.height();
78 let width = data.width();
79
80 let gpui_image_data = ImageData::new(vec![image::Frame::new(data)]);
81
82 return Ok(ImageView {
83 height,
84 width,
85 image: Arc::new(gpui_image_data),
86 });
87 }
88}
89
90impl LineHeight for ImageView {
91 fn num_lines(&self, cx: &mut WindowContext) -> usize {
92 let line_height = cx.line_height();
93 (self.height as f32 / line_height.0) as usize
94 }
95}
96
97/// TableView renders a static table inline in a buffer.
98/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
99pub struct TableView {
100 pub table: TabularDataResource,
101 pub widths: Vec<Pixels>,
102}
103
104fn cell_content(row: &Value, field: &str) -> String {
105 match row.get(&field) {
106 Some(Value::String(s)) => s.clone(),
107 Some(Value::Number(n)) => n.to_string(),
108 Some(Value::Bool(b)) => b.to_string(),
109 Some(Value::Array(arr)) => format!("{:?}", arr),
110 Some(Value::Object(obj)) => format!("{:?}", obj),
111 Some(Value::Null) | None => String::new(),
112 }
113}
114
115// Declare constant for the padding multiple on the line height
116const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5;
117
118impl TableView {
119 pub fn new(table: TabularDataResource, cx: &mut WindowContext) -> Self {
120 let mut widths = Vec::with_capacity(table.schema.fields.len());
121
122 let text_system = cx.text_system();
123 let text_style = cx.text_style();
124 let text_font = ThemeSettings::get_global(cx).buffer_font.clone();
125 let font_size = ThemeSettings::get_global(cx).buffer_font_size;
126 let mut runs = [TextRun {
127 len: 0,
128 font: text_font,
129 color: text_style.color,
130 background_color: None,
131 underline: None,
132 strikethrough: None,
133 }];
134
135 for field in table.schema.fields.iter() {
136 runs[0].len = field.name.len();
137 let mut width = text_system
138 .layout_line(&field.name, font_size, &runs)
139 .map(|layout| layout.width)
140 .unwrap_or(px(0.));
141
142 let Some(data) = table.data.as_ref() else {
143 widths.push(width);
144 continue;
145 };
146
147 for row in data {
148 let content = cell_content(&row, &field.name);
149 runs[0].len = content.len();
150 let cell_width = cx
151 .text_system()
152 .layout_line(&content, font_size, &runs)
153 .map(|layout| layout.width)
154 .unwrap_or(px(0.));
155
156 width = width.max(cell_width)
157 }
158
159 widths.push(width)
160 }
161
162 Self { table, widths }
163 }
164
165 pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
166 let data = match &self.table.data {
167 Some(data) => data,
168 None => return div().into_any_element(),
169 };
170
171 let mut headings = serde_json::Map::new();
172 for field in &self.table.schema.fields {
173 headings.insert(field.name.clone(), Value::String(field.name.clone()));
174 }
175 let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx);
176
177 let body = data
178 .iter()
179 .map(|row| self.render_row(&self.table.schema, false, &row, cx));
180
181 v_flex()
182 .id("table")
183 .overflow_x_scroll()
184 .w_full()
185 .child(header)
186 .children(body)
187 .into_any_element()
188 }
189
190 pub fn render_row(
191 &self,
192 schema: &TableSchema,
193 is_header: bool,
194 row: &Value,
195 cx: &ViewContext<ExecutionView>,
196 ) -> AnyElement {
197 let theme = cx.theme();
198
199 let line_height = cx.line_height();
200
201 let row_cells = schema
202 .fields
203 .iter()
204 .zip(self.widths.iter())
205 .map(|(field, width)| {
206 let container = match field.field_type {
207 runtimelib::datatable::FieldType::String => div(),
208
209 runtimelib::datatable::FieldType::Number
210 | runtimelib::datatable::FieldType::Integer
211 | runtimelib::datatable::FieldType::Date
212 | runtimelib::datatable::FieldType::Time
213 | runtimelib::datatable::FieldType::Datetime
214 | runtimelib::datatable::FieldType::Year
215 | runtimelib::datatable::FieldType::Duration
216 | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
217
218 _ => div(),
219 };
220
221 let value = cell_content(row, &field.name);
222
223 let mut cell = container
224 .min_w(*width + px(22.))
225 .w(*width + px(22.))
226 .child(value)
227 .px_2()
228 .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
229 .border_color(theme.colors().border);
230
231 if is_header {
232 cell = cell.border_1().bg(theme.colors().border_focused)
233 } else {
234 cell = cell.border_1()
235 }
236 cell
237 })
238 .collect::<Vec<_>>();
239
240 let mut total_width = px(0.);
241 for width in self.widths.iter() {
242 // Width fudge factor: border + 2 (heading), padding
243 total_width += *width + px(22.);
244 }
245
246 h_flex()
247 .w(total_width)
248 .children(row_cells)
249 .into_any_element()
250 }
251}
252
253impl LineHeight for TableView {
254 fn num_lines(&self, _cx: &mut WindowContext) -> usize {
255 let num_rows = match &self.table.data {
256 // Rows + header
257 Some(data) => data.len() + 1,
258 // We don't support Path based data sources, however we
259 // still render the header and padding
260 None => 1 + 1,
261 };
262
263 let num_lines = num_rows as f32 * (1.0 + TABLE_Y_PADDING_MULTIPLE) + 1.0;
264 num_lines.ceil() as usize
265 }
266}
267
268/// Userspace error from the kernel
269pub struct ErrorView {
270 pub ename: String,
271 pub evalue: String,
272 pub traceback: TerminalOutput,
273}
274
275impl ErrorView {
276 fn render(&self, cx: &ViewContext<ExecutionView>) -> Option<AnyElement> {
277 let theme = cx.theme();
278
279 let padding = cx.line_height() / 2.;
280
281 Some(
282 v_flex()
283 .w_full()
284 .px(padding)
285 .py(padding)
286 .border_1()
287 .border_color(theme.status().error_border)
288 .child(
289 h_flex()
290 .font_weight(FontWeight::BOLD)
291 .child(format!("{}: {}", self.ename, self.evalue)),
292 )
293 .child(self.traceback.render(cx))
294 .into_any_element(),
295 )
296 }
297}
298
299impl LineHeight for ErrorView {
300 fn num_lines(&self, cx: &mut WindowContext) -> usize {
301 // Start at 1 to account for the y padding
302 1 + self.ename.lines().count() + self.evalue.lines().count() + self.traceback.num_lines(cx)
303 }
304}
305
306pub enum OutputType {
307 Plain(TerminalOutput),
308 Stream(TerminalOutput),
309 Image(ImageView),
310 ErrorOutput(ErrorView),
311 Message(String),
312 Table(TableView),
313 ClearOutputWaitMarker,
314}
315
316impl OutputType {
317 fn render(&self, cx: &ViewContext<ExecutionView>) -> Option<AnyElement> {
318 let el = match self {
319 // Note: in typical frontends we would show the execute_result.execution_count
320 // Here we can just handle either
321 Self::Plain(stdio) => Some(stdio.render(cx)),
322 // Self::Markdown(markdown) => Some(markdown.render(theme)),
323 Self::Stream(stdio) => Some(stdio.render(cx)),
324 Self::Image(image) => Some(image.render(cx)),
325 Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
326 Self::Table(table) => Some(table.render(cx)),
327 Self::ErrorOutput(error_view) => error_view.render(cx),
328 Self::ClearOutputWaitMarker => None,
329 };
330
331 el
332 }
333
334 pub fn new(data: &MimeBundle, cx: &mut WindowContext) -> Self {
335 match data.richest(rank_mime_type) {
336 Some(MimeType::Plain(text)) => OutputType::Plain(TerminalOutput::from(text)),
337 Some(MimeType::Markdown(text)) => OutputType::Plain(TerminalOutput::from(text)),
338 Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) {
339 Ok(view) => OutputType::Image(view),
340 Err(error) => OutputType::Message(format!("Failed to load image: {}", error)),
341 },
342 Some(MimeType::DataTable(data)) => OutputType::Table(TableView::new(data.clone(), cx)),
343 // Any other media types are not supported
344 _ => OutputType::Message("Unsupported media type".to_string()),
345 }
346 }
347}
348
349impl LineHeight for OutputType {
350 /// Calculates the expected number of lines
351 fn num_lines(&self, cx: &mut WindowContext) -> usize {
352 match self {
353 Self::Plain(stdio) => stdio.num_lines(cx),
354 Self::Stream(stdio) => stdio.num_lines(cx),
355 Self::Image(image) => image.num_lines(cx),
356 Self::Message(message) => message.lines().count(),
357 Self::Table(table) => table.num_lines(cx),
358 Self::ErrorOutput(error_view) => error_view.num_lines(cx),
359 Self::ClearOutputWaitMarker => 0,
360 }
361 }
362}
363
364#[derive(Default, Clone, Debug)]
365pub enum ExecutionStatus {
366 #[default]
367 Unknown,
368 ConnectingToKernel,
369 Queued,
370 Executing,
371 Finished,
372 ShuttingDown,
373 Shutdown,
374 KernelErrored(String),
375}
376
377pub struct ExecutionView {
378 pub outputs: Vec<OutputType>,
379 pub status: ExecutionStatus,
380}
381
382impl ExecutionView {
383 pub fn new(status: ExecutionStatus, _cx: &mut ViewContext<Self>) -> Self {
384 Self {
385 outputs: Default::default(),
386 status,
387 }
388 }
389
390 /// Accept a Jupyter message belonging to this execution
391 pub fn push_message(&mut self, message: &JupyterMessageContent, cx: &mut ViewContext<Self>) {
392 let output: OutputType = match message {
393 JupyterMessageContent::ExecuteResult(result) => OutputType::new(&result.data, cx),
394 JupyterMessageContent::DisplayData(result) => OutputType::new(&result.data, cx),
395 JupyterMessageContent::StreamContent(result) => {
396 // Previous stream data will combine together, handling colors, carriage returns, etc
397 if let Some(new_terminal) = self.apply_terminal_text(&result.text) {
398 new_terminal
399 } else {
400 cx.notify();
401 return;
402 }
403 }
404 JupyterMessageContent::ErrorOutput(result) => {
405 let mut terminal = TerminalOutput::new();
406 terminal.append_text(&result.traceback.join("\n"));
407
408 OutputType::ErrorOutput(ErrorView {
409 ename: result.ename.clone(),
410 evalue: result.evalue.clone(),
411 traceback: terminal,
412 })
413 }
414 JupyterMessageContent::ExecuteReply(reply) => {
415 for payload in reply.payload.iter() {
416 match payload {
417 // Pager data comes in via `?` at the end of a statement in Python, used for showing documentation.
418 // Some UI will show this as a popup. For ease of implementation, it's included as an output here.
419 runtimelib::Payload::Page { data, .. } => {
420 let output = OutputType::new(data, cx);
421 self.outputs.push(output);
422 }
423
424 // There are other payloads that could be handled here, such as updating the input.
425 // Below are the other payloads that _could_ be handled, but are not required for Zed.
426
427 // Set next input adds text to the next cell. Not required to support.
428 // However, this could be implemented by adding text to the buffer.
429 // Trigger in python using `get_ipython().set_next_input("text")`
430 //
431 // runtimelib::Payload::SetNextInput { text, replace } => {},
432
433 // Not likely to be used in the context of Zed, where someone could just open the buffer themselves
434 // Python users can trigger this with the `%edit` magic command
435 // runtimelib::Payload::EditMagic { filename, line_number } => {},
436
437 // Ask the user if they want to exit the kernel. Not required to support.
438 // runtimelib::Payload::AskExit { keepkernel } => {},
439 _ => {}
440 }
441 }
442 cx.notify();
443 return;
444 }
445 JupyterMessageContent::ClearOutput(options) => {
446 if !options.wait {
447 self.outputs.clear();
448 cx.notify();
449 return;
450 }
451
452 // Create a marker to clear the output after we get in a new output
453 OutputType::ClearOutputWaitMarker
454 }
455 JupyterMessageContent::Status(status) => {
456 match status.execution_state {
457 ExecutionState::Busy => {
458 self.status = ExecutionStatus::Executing;
459 }
460 ExecutionState::Idle => self.status = ExecutionStatus::Finished,
461 }
462 cx.notify();
463 return;
464 }
465 _msg => {
466 return;
467 }
468 };
469
470 // Check for a clear output marker as the previous output, so we can clear it out
471 if let Some(OutputType::ClearOutputWaitMarker) = self.outputs.last() {
472 self.outputs.clear();
473 }
474
475 self.outputs.push(output);
476
477 cx.notify();
478 }
479
480 fn apply_terminal_text(&mut self, text: &str) -> Option<OutputType> {
481 if let Some(last_output) = self.outputs.last_mut() {
482 match last_output {
483 OutputType::Stream(last_stream) => {
484 last_stream.append_text(text);
485 // Don't need to add a new output, we already have a terminal output
486 return None;
487 }
488 // Edge case note: a clear output marker
489 OutputType::ClearOutputWaitMarker => {
490 // Edge case note: a clear output marker is handled by the caller
491 // since we will return a new output at the end here as a new terminal output
492 }
493 // A different output type is "in the way", so we need to create a new output,
494 // which is the same as having no prior output
495 _ => {}
496 }
497 }
498
499 let mut new_terminal = TerminalOutput::new();
500 new_terminal.append_text(text);
501 Some(OutputType::Stream(new_terminal))
502 }
503}
504
505impl Render for ExecutionView {
506 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
507 let status = match &self.status {
508 ExecutionStatus::ConnectingToKernel => Label::new("Connecting to kernel...")
509 .color(Color::Muted)
510 .into_any_element(),
511 ExecutionStatus::Executing => h_flex()
512 .gap_2()
513 .child(
514 Icon::new(IconName::ArrowCircle)
515 .size(IconSize::Small)
516 .color(Color::Muted)
517 .with_animation(
518 "arrow-circle",
519 Animation::new(Duration::from_secs(3)).repeat(),
520 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
521 ),
522 )
523 .child(Label::new("Executing...").color(Color::Muted))
524 .into_any_element(),
525 ExecutionStatus::Finished => Icon::new(IconName::Check)
526 .size(IconSize::Small)
527 .into_any_element(),
528 ExecutionStatus::Unknown => Label::new("Unknown status")
529 .color(Color::Muted)
530 .into_any_element(),
531 ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...")
532 .color(Color::Muted)
533 .into_any_element(),
534 ExecutionStatus::Shutdown => Label::new("Kernel shutdown")
535 .color(Color::Muted)
536 .into_any_element(),
537 ExecutionStatus::Queued => Label::new("Queued...")
538 .color(Color::Muted)
539 .into_any_element(),
540 ExecutionStatus::KernelErrored(error) => Label::new(format!("Kernel error: {}", error))
541 .color(Color::Error)
542 .into_any_element(),
543 };
544
545 if self.outputs.len() == 0 {
546 return v_flex()
547 .min_h(cx.line_height())
548 .justify_center()
549 .child(status)
550 .into_any_element();
551 }
552
553 div()
554 .w_full()
555 .children(self.outputs.iter().filter_map(|output| output.render(cx)))
556 .children(match self.status {
557 ExecutionStatus::Executing => vec![status],
558 ExecutionStatus::Queued => vec![status],
559 _ => vec![],
560 })
561 .into_any_element()
562 }
563}
564
565impl LineHeight for ExecutionView {
566 fn num_lines(&self, cx: &mut WindowContext) -> usize {
567 if self.outputs.is_empty() {
568 return 1; // For the status message if outputs are not there
569 }
570
571 let num_lines = self
572 .outputs
573 .iter()
574 .map(|output| output.num_lines(cx))
575 .sum::<usize>()
576 .max(1);
577
578 let num_lines = match self.status {
579 // Account for the status message if the execution is still ongoing
580 ExecutionStatus::Executing => num_lines.saturating_add(1),
581 ExecutionStatus::Queued => num_lines.saturating_add(1),
582 _ => num_lines,
583 };
584 num_lines
585 }
586}
587
588impl LineHeight for View<ExecutionView> {
589 fn num_lines(&self, cx: &mut WindowContext) -> usize {
590 self.update(cx, |execution_view, cx| execution_view.num_lines(cx))
591 }
592}