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