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