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