1#![allow(unused, dead_code)]
2use std::sync::Arc;
3use std::time::{Duration, Instant};
4
5use editor::{Editor, EditorMode, MultiBuffer};
6use futures::future::Shared;
7use gpui::{
8 App, Entity, EventEmitter, Focusable, Hsla, InteractiveElement, RetainAllImageCache,
9 StatefulInteractiveElement, Task, TextStyleRefinement, image_cache, prelude::*,
10};
11use language::{Buffer, Language, LanguageRegistry};
12use markdown::{Markdown, MarkdownElement, MarkdownStyle};
13use nbformat::v4::{CellId, CellMetadata, CellType};
14use runtimelib::{JupyterMessage, JupyterMessageContent};
15use settings::Settings as _;
16use theme::ThemeSettings;
17use ui::{CommonAnimationExt, IconButtonShape, prelude::*};
18use util::ResultExt;
19
20use crate::{
21 notebook::{CODE_BLOCK_INSET, GUTTER_WIDTH},
22 outputs::{Output, plain::TerminalOutput, user_error::ErrorView},
23};
24
25#[derive(Copy, Clone, PartialEq, PartialOrd)]
26pub enum CellPosition {
27 First,
28 Middle,
29 Last,
30}
31
32pub enum CellControlType {
33 RunCell,
34 RerunCell,
35 ClearCell,
36 CellOptions,
37 CollapseCell,
38 ExpandCell,
39}
40
41pub enum CellEvent {
42 Run(CellId),
43 FocusedIn(CellId),
44}
45
46pub enum MarkdownCellEvent {
47 FinishedEditing,
48 Run(CellId),
49}
50
51impl CellControlType {
52 fn icon_name(&self) -> IconName {
53 match self {
54 CellControlType::RunCell => IconName::PlayFilled,
55 CellControlType::RerunCell => IconName::ArrowCircle,
56 CellControlType::ClearCell => IconName::ListX,
57 CellControlType::CellOptions => IconName::Ellipsis,
58 CellControlType::CollapseCell => IconName::ChevronDown,
59 CellControlType::ExpandCell => IconName::ChevronRight,
60 }
61 }
62}
63
64pub struct CellControl {
65 button: IconButton,
66}
67
68impl CellControl {
69 fn new(id: impl Into<SharedString>, control_type: CellControlType) -> Self {
70 let icon_name = control_type.icon_name();
71 let id = id.into();
72 let button = IconButton::new(id, icon_name)
73 .icon_size(IconSize::Small)
74 .shape(IconButtonShape::Square);
75 Self { button }
76 }
77}
78
79impl Clickable for CellControl {
80 fn on_click(
81 self,
82 handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
83 ) -> Self {
84 let button = self.button.on_click(handler);
85 Self { button }
86 }
87
88 fn cursor_style(self, _cursor_style: gpui::CursorStyle) -> Self {
89 self
90 }
91}
92
93/// A notebook cell
94#[derive(Clone)]
95pub enum Cell {
96 Code(Entity<CodeCell>),
97 Markdown(Entity<MarkdownCell>),
98 Raw(Entity<RawCell>),
99}
100
101fn convert_outputs(
102 outputs: &Vec<nbformat::v4::Output>,
103 window: &mut Window,
104 cx: &mut App,
105) -> Vec<Output> {
106 outputs
107 .iter()
108 .map(|output| match output {
109 nbformat::v4::Output::Stream { text, .. } => Output::Stream {
110 content: cx.new(|cx| TerminalOutput::from(&text.0, window, cx)),
111 },
112 nbformat::v4::Output::DisplayData(display_data) => {
113 Output::new(&display_data.data, None, window, cx)
114 }
115 nbformat::v4::Output::ExecuteResult(execute_result) => {
116 Output::new(&execute_result.data, None, window, cx)
117 }
118 nbformat::v4::Output::Error(error) => Output::ErrorOutput(ErrorView {
119 ename: error.ename.clone(),
120 evalue: error.evalue.clone(),
121 traceback: cx
122 .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
123 }),
124 })
125 .collect()
126}
127
128impl Cell {
129 pub fn id(&self, cx: &App) -> CellId {
130 match self {
131 Cell::Code(code_cell) => code_cell.read(cx).id().clone(),
132 Cell::Markdown(markdown_cell) => markdown_cell.read(cx).id().clone(),
133 Cell::Raw(raw_cell) => raw_cell.read(cx).id().clone(),
134 }
135 }
136
137 pub fn current_source(&self, cx: &App) -> String {
138 match self {
139 Cell::Code(code_cell) => code_cell.read(cx).current_source(cx),
140 Cell::Markdown(markdown_cell) => markdown_cell.read(cx).current_source(cx),
141 Cell::Raw(raw_cell) => raw_cell.read(cx).source.clone(),
142 }
143 }
144
145 pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
146 match self {
147 Cell::Code(code_cell) => code_cell.read(cx).to_nbformat_cell(cx),
148 Cell::Markdown(markdown_cell) => markdown_cell.read(cx).to_nbformat_cell(cx),
149 Cell::Raw(raw_cell) => raw_cell.read(cx).to_nbformat_cell(),
150 }
151 }
152
153 pub fn is_dirty(&self, cx: &App) -> bool {
154 match self {
155 Cell::Code(code_cell) => code_cell.read(cx).is_dirty(cx),
156 Cell::Markdown(markdown_cell) => markdown_cell.read(cx).is_dirty(cx),
157 Cell::Raw(_) => false,
158 }
159 }
160
161 pub fn load(
162 cell: &nbformat::v4::Cell,
163 languages: &Arc<LanguageRegistry>,
164 notebook_language: Shared<Task<Option<Arc<Language>>>>,
165 window: &mut Window,
166 cx: &mut App,
167 ) -> Self {
168 match cell {
169 nbformat::v4::Cell::Markdown {
170 id,
171 metadata,
172 source,
173 ..
174 } => {
175 let source = source.join("");
176
177 let entity = cx.new(|cx| {
178 MarkdownCell::new(
179 id.clone(),
180 metadata.clone(),
181 source,
182 languages.clone(),
183 window,
184 cx,
185 )
186 });
187
188 Cell::Markdown(entity)
189 }
190 nbformat::v4::Cell::Code {
191 id,
192 metadata,
193 execution_count,
194 source,
195 outputs,
196 } => {
197 let text = source.join("");
198 let outputs = convert_outputs(outputs, window, cx);
199
200 Cell::Code(cx.new(|cx| {
201 CodeCell::load(
202 id.clone(),
203 metadata.clone(),
204 *execution_count,
205 text,
206 outputs,
207 notebook_language,
208 window,
209 cx,
210 )
211 }))
212 }
213 nbformat::v4::Cell::Raw {
214 id,
215 metadata,
216 source,
217 } => Cell::Raw(cx.new(|_| RawCell {
218 id: id.clone(),
219 metadata: metadata.clone(),
220 source: source.join(""),
221 selected: false,
222 cell_position: None,
223 })),
224 }
225 }
226}
227
228pub trait RenderableCell: Render {
229 const CELL_TYPE: CellType;
230
231 fn id(&self) -> &CellId;
232 fn cell_type(&self) -> CellType;
233 fn metadata(&self) -> &CellMetadata;
234 fn source(&self) -> &String;
235 fn selected(&self) -> bool;
236 fn set_selected(&mut self, selected: bool) -> &mut Self;
237 fn selected_bg_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
238 if self.selected() {
239 let mut color = cx.theme().colors().element_hover;
240 color.fade_out(0.5);
241 color
242 } else {
243 // Not sure if this is correct, previous was TODO: this is wrong
244 gpui::transparent_black()
245 }
246 }
247 fn control(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<CellControl> {
248 None
249 }
250
251 fn cell_position_spacer(
252 &self,
253 is_first: bool,
254 window: &mut Window,
255 cx: &mut Context<Self>,
256 ) -> Option<impl IntoElement> {
257 let cell_position = self.cell_position();
258
259 if (cell_position == Some(&CellPosition::First) && is_first)
260 || (cell_position == Some(&CellPosition::Last) && !is_first)
261 {
262 Some(div().flex().w_full().h(DynamicSpacing::Base12.px(cx)))
263 } else {
264 None
265 }
266 }
267
268 fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
269 let is_selected = self.selected();
270
271 div()
272 .relative()
273 .h_full()
274 .w(px(GUTTER_WIDTH))
275 .child(
276 div()
277 .w(px(GUTTER_WIDTH))
278 .flex()
279 .flex_none()
280 .justify_center()
281 .h_full()
282 .child(
283 div()
284 .flex_none()
285 .w(px(1.))
286 .h_full()
287 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
288 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
289 ),
290 )
291 .when_some(self.control(window, cx), |this, control| {
292 this.child(
293 div()
294 .absolute()
295 .top(px(CODE_BLOCK_INSET - 2.0))
296 .left_0()
297 .flex()
298 .flex_none()
299 .w(px(GUTTER_WIDTH))
300 .h(px(GUTTER_WIDTH + 12.0))
301 .items_center()
302 .justify_center()
303 .bg(cx.theme().colors().tab_bar_background)
304 .child(control.button),
305 )
306 })
307 }
308
309 fn cell_position(&self) -> Option<&CellPosition>;
310 fn set_cell_position(&mut self, position: CellPosition) -> &mut Self;
311}
312
313pub trait RunnableCell: RenderableCell {
314 fn execution_count(&self) -> Option<i32>;
315 fn set_execution_count(&mut self, count: i32) -> &mut Self;
316 fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) -> ();
317}
318
319pub struct MarkdownCell {
320 id: CellId,
321 metadata: CellMetadata,
322 image_cache: Entity<RetainAllImageCache>,
323 source: String,
324 editor: Entity<Editor>,
325 markdown: Entity<Markdown>,
326 editing: bool,
327 selected: bool,
328 cell_position: Option<CellPosition>,
329 languages: Arc<LanguageRegistry>,
330 _editor_subscription: gpui::Subscription,
331}
332
333impl EventEmitter<MarkdownCellEvent> for MarkdownCell {}
334
335impl MarkdownCell {
336 pub fn new(
337 id: CellId,
338 metadata: CellMetadata,
339 source: String,
340 languages: Arc<LanguageRegistry>,
341 window: &mut Window,
342 cx: &mut Context<Self>,
343 ) -> Self {
344 let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
345 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
346
347 let markdown_language = languages.language_for_name("Markdown");
348 cx.spawn_in(window, async move |_this, cx| {
349 if let Some(markdown) = markdown_language.await.log_err() {
350 buffer.update(cx, |buffer, cx| {
351 buffer.set_language(Some(markdown), cx);
352 });
353 }
354 })
355 .detach();
356
357 let editor = cx.new(|cx| {
358 let mut editor = Editor::new(
359 EditorMode::AutoHeight {
360 min_lines: 1,
361 max_lines: Some(1024),
362 },
363 multi_buffer,
364 None,
365 window,
366 cx,
367 );
368
369 let theme = ThemeSettings::get_global(cx);
370 let refinement = TextStyleRefinement {
371 font_family: Some(theme.buffer_font.family.clone()),
372 font_size: Some(theme.buffer_font_size(cx).into()),
373 color: Some(cx.theme().colors().editor_foreground),
374 background_color: Some(gpui::transparent_black()),
375 ..Default::default()
376 };
377
378 editor.set_show_gutter(false, cx);
379 editor.set_text_style_refinement(refinement);
380 editor
381 });
382
383 let markdown = cx.new(|cx| Markdown::new(source.clone().into(), None, None, cx));
384
385 let cell_id = id.clone();
386 let editor_subscription =
387 cx.subscribe(&editor, move |this, _editor, event, cx| match event {
388 editor::EditorEvent::Blurred => {
389 if this.editing {
390 this.editing = false;
391 cx.emit(MarkdownCellEvent::FinishedEditing);
392 cx.notify();
393 }
394 }
395 _ => {}
396 });
397
398 let start_editing = source.is_empty();
399 Self {
400 id,
401 metadata,
402 image_cache: RetainAllImageCache::new(cx),
403 source,
404 editor,
405 markdown,
406 editing: start_editing,
407 selected: false,
408 cell_position: None,
409 languages,
410 _editor_subscription: editor_subscription,
411 }
412 }
413
414 pub fn editor(&self) -> &Entity<Editor> {
415 &self.editor
416 }
417
418 pub fn current_source(&self, cx: &App) -> String {
419 let editor = self.editor.read(cx);
420 let buffer = editor.buffer().read(cx);
421 buffer
422 .as_singleton()
423 .map(|b| b.read(cx).text())
424 .unwrap_or_default()
425 }
426
427 pub fn is_dirty(&self, cx: &App) -> bool {
428 self.editor.read(cx).buffer().read(cx).is_dirty(cx)
429 }
430
431 pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
432 let source = self.current_source(cx);
433 let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
434
435 nbformat::v4::Cell::Markdown {
436 id: self.id.clone(),
437 metadata: self.metadata.clone(),
438 source: source_lines,
439 attachments: None,
440 }
441 }
442
443 pub fn is_editing(&self) -> bool {
444 self.editing
445 }
446
447 pub fn set_editing(&mut self, editing: bool) {
448 self.editing = editing;
449 }
450
451 pub fn reparse_markdown(&mut self, cx: &mut Context<Self>) {
452 let editor = self.editor.read(cx);
453 let buffer = editor.buffer().read(cx);
454 let source = buffer
455 .as_singleton()
456 .map(|b| b.read(cx).text())
457 .unwrap_or_default();
458
459 self.source = source.clone();
460 let languages = self.languages.clone();
461
462 self.markdown.update(cx, |markdown, cx| {
463 markdown.reset(source.into(), cx);
464 });
465 }
466
467 /// Called when user presses Shift+Enter or Ctrl+Enter while editing.
468 /// Finishes editing and signals to move to the next cell.
469 pub fn run(&mut self, cx: &mut Context<Self>) {
470 if self.editing {
471 self.editing = false;
472 cx.emit(MarkdownCellEvent::FinishedEditing);
473 cx.emit(MarkdownCellEvent::Run(self.id.clone()));
474 cx.notify();
475 }
476 }
477}
478
479impl RenderableCell for MarkdownCell {
480 const CELL_TYPE: CellType = CellType::Markdown;
481
482 fn id(&self) -> &CellId {
483 &self.id
484 }
485
486 fn cell_type(&self) -> CellType {
487 CellType::Markdown
488 }
489
490 fn metadata(&self) -> &CellMetadata {
491 &self.metadata
492 }
493
494 fn source(&self) -> &String {
495 &self.source
496 }
497
498 fn selected(&self) -> bool {
499 self.selected
500 }
501
502 fn set_selected(&mut self, selected: bool) -> &mut Self {
503 self.selected = selected;
504 self
505 }
506
507 fn control(&self, _window: &mut Window, _: &mut Context<Self>) -> Option<CellControl> {
508 None
509 }
510
511 fn cell_position(&self) -> Option<&CellPosition> {
512 self.cell_position.as_ref()
513 }
514
515 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
516 self.cell_position = Some(cell_position);
517 self
518 }
519}
520
521impl Render for MarkdownCell {
522 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
523 // If editing, show the editor
524 if self.editing {
525 return v_flex()
526 .size_full()
527 .children(self.cell_position_spacer(true, window, cx))
528 .child(
529 h_flex()
530 .w_full()
531 .pr_6()
532 .rounded_xs()
533 .items_start()
534 .gap(DynamicSpacing::Base08.rems(cx))
535 .bg(self.selected_bg_color(window, cx))
536 .child(self.gutter(window, cx))
537 .child(
538 div()
539 .flex_1()
540 .p_3()
541 .bg(cx.theme().colors().editor_background)
542 .rounded_sm()
543 .child(self.editor.clone())
544 .on_mouse_down(
545 gpui::MouseButton::Left,
546 cx.listener(|_this, _event, _window, _cx| {
547 // Prevent the click from propagating
548 }),
549 ),
550 ),
551 )
552 .children(self.cell_position_spacer(false, window, cx));
553 }
554
555 // Preview mode - show rendered markdown
556
557 let style = MarkdownStyle {
558 base_text_style: window.text_style(),
559 ..Default::default()
560 };
561
562 v_flex()
563 .size_full()
564 .children(self.cell_position_spacer(true, window, cx))
565 .child(
566 h_flex()
567 .w_full()
568 .pr_6()
569 .rounded_xs()
570 .items_start()
571 .gap(DynamicSpacing::Base08.rems(cx))
572 .bg(self.selected_bg_color(window, cx))
573 .child(self.gutter(window, cx))
574 .child(
575 v_flex()
576 .image_cache(self.image_cache.clone())
577 .id("markdown-content")
578 .size_full()
579 .flex_1()
580 .p_3()
581 .font_ui(cx)
582 .text_size(TextSize::Default.rems(cx))
583 .cursor_pointer()
584 .on_click(cx.listener(|this, _event, window, cx| {
585 this.editing = true;
586 window.focus(&this.editor.focus_handle(cx), cx);
587 cx.notify();
588 }))
589 .child(MarkdownElement::new(self.markdown.clone(), style)),
590 ),
591 )
592 .children(self.cell_position_spacer(false, window, cx))
593 }
594}
595
596pub struct CodeCell {
597 id: CellId,
598 metadata: CellMetadata,
599 execution_count: Option<i32>,
600 source: String,
601 editor: Entity<editor::Editor>,
602 outputs: Vec<Output>,
603 selected: bool,
604 cell_position: Option<CellPosition>,
605 language_task: Task<()>,
606 execution_start_time: Option<Instant>,
607 execution_duration: Option<Duration>,
608 is_executing: bool,
609}
610
611impl EventEmitter<CellEvent> for CodeCell {}
612
613impl CodeCell {
614 pub fn new(
615 id: CellId,
616 metadata: CellMetadata,
617 source: String,
618 notebook_language: Shared<Task<Option<Arc<Language>>>>,
619 window: &mut Window,
620 cx: &mut Context<Self>,
621 ) -> Self {
622 let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
623 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
624
625 let editor_view = cx.new(|cx| {
626 let mut editor = Editor::new(
627 EditorMode::AutoHeight {
628 min_lines: 1,
629 max_lines: Some(1024),
630 },
631 multi_buffer,
632 None,
633 window,
634 cx,
635 );
636
637 let theme = ThemeSettings::get_global(cx);
638 let refinement = TextStyleRefinement {
639 font_family: Some(theme.buffer_font.family.clone()),
640 font_size: Some(theme.buffer_font_size(cx).into()),
641 color: Some(cx.theme().colors().editor_foreground),
642 background_color: Some(gpui::transparent_black()),
643 ..Default::default()
644 };
645
646 editor.set_show_gutter(false, cx);
647 editor.set_text_style_refinement(refinement);
648 editor
649 });
650
651 let language_task = cx.spawn_in(window, async move |_this, cx| {
652 let language = notebook_language.await;
653 buffer.update(cx, |buffer, cx| {
654 buffer.set_language(language.clone(), cx);
655 });
656 });
657
658 Self {
659 id,
660 metadata,
661 execution_count: None,
662 source,
663 editor: editor_view,
664 outputs: Vec::new(),
665 selected: false,
666 cell_position: None,
667 language_task,
668 execution_start_time: None,
669 execution_duration: None,
670 is_executing: false,
671 }
672 }
673
674 /// Load a code cell from notebook file data, including existing outputs and execution count
675 pub fn load(
676 id: CellId,
677 metadata: CellMetadata,
678 execution_count: Option<i32>,
679 source: String,
680 outputs: Vec<Output>,
681 notebook_language: Shared<Task<Option<Arc<Language>>>>,
682 window: &mut Window,
683 cx: &mut Context<Self>,
684 ) -> Self {
685 let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
686 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
687
688 let editor_view = cx.new(|cx| {
689 let mut editor = Editor::new(
690 EditorMode::AutoHeight {
691 min_lines: 1,
692 max_lines: Some(1024),
693 },
694 multi_buffer,
695 None,
696 window,
697 cx,
698 );
699
700 let theme = ThemeSettings::get_global(cx);
701 let refinement = TextStyleRefinement {
702 font_family: Some(theme.buffer_font.family.clone()),
703 font_size: Some(theme.buffer_font_size(cx).into()),
704 color: Some(cx.theme().colors().editor_foreground),
705 background_color: Some(gpui::transparent_black()),
706 ..Default::default()
707 };
708
709 editor.set_text(source.clone(), window, cx);
710 editor.set_show_gutter(false, cx);
711 editor.set_text_style_refinement(refinement);
712 editor
713 });
714
715 let language_task = cx.spawn_in(window, async move |_this, cx| {
716 let language = notebook_language.await;
717 buffer.update(cx, |buffer, cx| {
718 buffer.set_language(language.clone(), cx);
719 });
720 });
721
722 Self {
723 id,
724 metadata,
725 execution_count,
726 source,
727 editor: editor_view,
728 outputs,
729 selected: false,
730 cell_position: None,
731 language_task,
732 execution_start_time: None,
733 execution_duration: None,
734 is_executing: false,
735 }
736 }
737
738 pub fn editor(&self) -> &Entity<editor::Editor> {
739 &self.editor
740 }
741
742 pub fn current_source(&self, cx: &App) -> String {
743 let editor = self.editor.read(cx);
744 let buffer = editor.buffer().read(cx);
745 buffer
746 .as_singleton()
747 .map(|b| b.read(cx).text())
748 .unwrap_or_default()
749 }
750
751 pub fn is_dirty(&self, cx: &App) -> bool {
752 self.editor.read(cx).buffer().read(cx).is_dirty(cx)
753 }
754
755 pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
756 let source = self.current_source(cx);
757 let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
758
759 let outputs = self.outputs_to_nbformat(cx);
760
761 nbformat::v4::Cell::Code {
762 id: self.id.clone(),
763 metadata: self.metadata.clone(),
764 execution_count: self.execution_count,
765 source: source_lines,
766 outputs,
767 }
768 }
769
770 fn outputs_to_nbformat(&self, cx: &App) -> Vec<nbformat::v4::Output> {
771 self.outputs
772 .iter()
773 .filter_map(|output| output.to_nbformat(cx))
774 .collect()
775 }
776
777 pub fn has_outputs(&self) -> bool {
778 !self.outputs.is_empty()
779 }
780
781 pub fn clear_outputs(&mut self) {
782 self.outputs.clear();
783 self.execution_duration = None;
784 }
785
786 pub fn start_execution(&mut self) {
787 self.execution_start_time = Some(Instant::now());
788 self.execution_duration = None;
789 self.is_executing = true;
790 }
791
792 pub fn finish_execution(&mut self) {
793 if let Some(start_time) = self.execution_start_time.take() {
794 self.execution_duration = Some(start_time.elapsed());
795 }
796 self.is_executing = false;
797 }
798
799 pub fn is_executing(&self) -> bool {
800 self.is_executing
801 }
802
803 pub fn execution_duration(&self) -> Option<Duration> {
804 self.execution_duration
805 }
806
807 fn format_duration(duration: Duration) -> String {
808 let total_secs = duration.as_secs_f64();
809 if total_secs < 1.0 {
810 format!("{:.0}ms", duration.as_millis())
811 } else if total_secs < 60.0 {
812 format!("{:.1}s", total_secs)
813 } else {
814 let minutes = (total_secs / 60.0).floor() as u64;
815 let secs = total_secs % 60.0;
816 format!("{}m {:.1}s", minutes, secs)
817 }
818 }
819
820 pub fn handle_message(
821 &mut self,
822 message: &JupyterMessage,
823 window: &mut Window,
824 cx: &mut Context<Self>,
825 ) {
826 match &message.content {
827 JupyterMessageContent::StreamContent(stream) => {
828 self.outputs.push(Output::Stream {
829 content: cx.new(|cx| TerminalOutput::from(&stream.text, window, cx)),
830 });
831 }
832 JupyterMessageContent::DisplayData(display_data) => {
833 self.outputs
834 .push(Output::new(&display_data.data, None, window, cx));
835 }
836 JupyterMessageContent::ExecuteResult(execute_result) => {
837 self.outputs
838 .push(Output::new(&execute_result.data, None, window, cx));
839 }
840 JupyterMessageContent::ExecuteInput(input) => {
841 self.execution_count = serde_json::to_value(&input.execution_count)
842 .ok()
843 .and_then(|v| v.as_i64())
844 .map(|v| v as i32);
845 }
846 JupyterMessageContent::ExecuteReply(_) => {
847 self.finish_execution();
848 }
849 JupyterMessageContent::ErrorOutput(error) => {
850 self.outputs.push(Output::ErrorOutput(ErrorView {
851 ename: error.ename.clone(),
852 evalue: error.evalue.clone(),
853 traceback: cx
854 .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
855 }));
856 }
857 _ => {}
858 }
859 cx.notify();
860 }
861
862 fn output_control(&self) -> Option<CellControlType> {
863 if self.has_outputs() {
864 Some(CellControlType::ClearCell)
865 } else {
866 None
867 }
868 }
869
870 pub fn gutter_output(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
871 let is_selected = self.selected();
872
873 div()
874 .relative()
875 .h_full()
876 .w(px(GUTTER_WIDTH))
877 .child(
878 div()
879 .w(px(GUTTER_WIDTH))
880 .flex()
881 .flex_none()
882 .justify_center()
883 .h_full()
884 .child(
885 div()
886 .flex_none()
887 .w(px(1.))
888 .h_full()
889 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
890 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
891 ),
892 )
893 .when(self.has_outputs(), |this| {
894 this.child(
895 div()
896 .absolute()
897 .top(px(CODE_BLOCK_INSET - 2.0))
898 .left_0()
899 .flex()
900 .flex_none()
901 .w(px(GUTTER_WIDTH))
902 .h(px(GUTTER_WIDTH + 12.0))
903 .items_center()
904 .justify_center()
905 .bg(cx.theme().colors().tab_bar_background)
906 .child(IconButton::new("control", IconName::Ellipsis)),
907 )
908 })
909 }
910}
911
912impl RenderableCell for CodeCell {
913 const CELL_TYPE: CellType = CellType::Code;
914
915 fn id(&self) -> &CellId {
916 &self.id
917 }
918
919 fn cell_type(&self) -> CellType {
920 CellType::Code
921 }
922
923 fn metadata(&self) -> &CellMetadata {
924 &self.metadata
925 }
926
927 fn source(&self) -> &String {
928 &self.source
929 }
930
931 fn control(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<CellControl> {
932 let control_type = if self.has_outputs() {
933 CellControlType::RerunCell
934 } else {
935 CellControlType::RunCell
936 };
937
938 let cell_control = CellControl::new(
939 if self.has_outputs() {
940 "rerun-cell"
941 } else {
942 "run-cell"
943 },
944 control_type,
945 )
946 .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx)));
947
948 Some(cell_control)
949 }
950
951 fn selected(&self) -> bool {
952 self.selected
953 }
954
955 fn set_selected(&mut self, selected: bool) -> &mut Self {
956 self.selected = selected;
957 self
958 }
959
960 fn cell_position(&self) -> Option<&CellPosition> {
961 self.cell_position.as_ref()
962 }
963
964 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
965 self.cell_position = Some(cell_position);
966 self
967 }
968
969 fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
970 let is_selected = self.selected();
971 let execution_count = self.execution_count;
972
973 div()
974 .relative()
975 .h_full()
976 .w(px(GUTTER_WIDTH))
977 .child(
978 div()
979 .w(px(GUTTER_WIDTH))
980 .flex()
981 .flex_none()
982 .justify_center()
983 .h_full()
984 .child(
985 div()
986 .flex_none()
987 .w(px(1.))
988 .h_full()
989 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
990 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
991 ),
992 )
993 .when_some(self.control(window, cx), |this, control| {
994 this.child(
995 div()
996 .absolute()
997 .top(px(CODE_BLOCK_INSET - 2.0))
998 .left_0()
999 .flex()
1000 .flex_col()
1001 .w(px(GUTTER_WIDTH))
1002 .items_center()
1003 .justify_center()
1004 .bg(cx.theme().colors().tab_bar_background)
1005 .child(control.button)
1006 .when_some(execution_count, |this, count| {
1007 this.child(
1008 div()
1009 .mt_1()
1010 .text_xs()
1011 .text_color(cx.theme().colors().text_muted)
1012 .child(format!("{}", count)),
1013 )
1014 }),
1015 )
1016 })
1017 }
1018}
1019
1020impl RunnableCell for CodeCell {
1021 fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1022 println!("Running code cell: {}", self.id);
1023 cx.emit(CellEvent::Run(self.id.clone()));
1024 }
1025
1026 fn execution_count(&self) -> Option<i32> {
1027 self.execution_count
1028 .and_then(|count| if count > 0 { Some(count) } else { None })
1029 }
1030
1031 fn set_execution_count(&mut self, count: i32) -> &mut Self {
1032 self.execution_count = Some(count);
1033 self
1034 }
1035}
1036
1037impl Render for CodeCell {
1038 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1039 // get the language from the editor's buffer
1040 let language_name = self
1041 .editor
1042 .read(cx)
1043 .buffer()
1044 .read(cx)
1045 .as_singleton()
1046 .and_then(|buffer| buffer.read(cx).language())
1047 .map(|lang| lang.name().to_string());
1048
1049 v_flex()
1050 .size_full()
1051 // TODO: Move base cell render into trait impl so we don't have to repeat this
1052 .children(self.cell_position_spacer(true, window, cx))
1053 // Editor portion
1054 .child(
1055 h_flex()
1056 .w_full()
1057 .pr_6()
1058 .rounded_xs()
1059 .items_start()
1060 .gap(DynamicSpacing::Base08.rems(cx))
1061 .bg(self.selected_bg_color(window, cx))
1062 .child(self.gutter(window, cx))
1063 .child(
1064 div().py_1p5().w_full().child(
1065 div()
1066 .relative()
1067 .flex()
1068 .size_full()
1069 .flex_1()
1070 .py_3()
1071 .px_5()
1072 .rounded_lg()
1073 .border_1()
1074 .border_color(cx.theme().colors().border)
1075 .bg(cx.theme().colors().editor_background)
1076 .child(div().w_full().child(self.editor.clone()))
1077 // lang badge in top-right corner
1078 .when_some(language_name, |this, name| {
1079 this.child(
1080 div()
1081 .absolute()
1082 .top_1()
1083 .right_2()
1084 .px_2()
1085 .py_0p5()
1086 .rounded_md()
1087 .bg(cx.theme().colors().element_background.opacity(0.7))
1088 .text_xs()
1089 .text_color(cx.theme().colors().text_muted)
1090 .child(name),
1091 )
1092 }),
1093 ),
1094 ),
1095 )
1096 // Output portion
1097 .when(
1098 self.has_outputs() || self.execution_duration.is_some() || self.is_executing,
1099 |this| {
1100 let execution_time_label = self.execution_duration.map(Self::format_duration);
1101 let is_executing = self.is_executing;
1102 this.child(
1103 h_flex()
1104 .w_full()
1105 .pr_6()
1106 .rounded_xs()
1107 .items_start()
1108 .gap(DynamicSpacing::Base08.rems(cx))
1109 .bg(self.selected_bg_color(window, cx))
1110 .child(self.gutter_output(window, cx))
1111 .child(
1112 div().py_1p5().w_full().child(
1113 v_flex()
1114 .size_full()
1115 .flex_1()
1116 .py_3()
1117 .px_5()
1118 .rounded_lg()
1119 .border_1()
1120 // execution status/time at the TOP
1121 .when(
1122 is_executing || execution_time_label.is_some(),
1123 |this| {
1124 let time_element = if is_executing {
1125 h_flex()
1126 .gap_1()
1127 .items_center()
1128 .child(
1129 Icon::new(IconName::ArrowCircle)
1130 .size(IconSize::XSmall)
1131 .color(Color::Warning)
1132 .with_rotate_animation(2)
1133 .into_any_element(),
1134 )
1135 .child(
1136 div()
1137 .text_xs()
1138 .text_color(
1139 cx.theme().colors().text_muted,
1140 )
1141 .child("Running..."),
1142 )
1143 .into_any_element()
1144 } else if let Some(duration_text) =
1145 execution_time_label.clone()
1146 {
1147 h_flex()
1148 .gap_1()
1149 .items_center()
1150 .child(
1151 Icon::new(IconName::Check)
1152 .size(IconSize::XSmall)
1153 .color(Color::Success),
1154 )
1155 .child(
1156 div()
1157 .text_xs()
1158 .text_color(
1159 cx.theme().colors().text_muted,
1160 )
1161 .child(duration_text),
1162 )
1163 .into_any_element()
1164 } else {
1165 div().into_any_element()
1166 };
1167 this.child(div().mb_2().child(time_element))
1168 },
1169 )
1170 // output at bottom
1171 .child(div().w_full().children(self.outputs.iter().map(
1172 |output| {
1173 let content = match output {
1174 Output::Plain { content, .. } => {
1175 Some(content.clone().into_any_element())
1176 }
1177 Output::Markdown { content, .. } => {
1178 Some(content.clone().into_any_element())
1179 }
1180 Output::Stream { content, .. } => {
1181 Some(content.clone().into_any_element())
1182 }
1183 Output::Image { content, .. } => {
1184 Some(content.clone().into_any_element())
1185 }
1186 Output::Message(message) => Some(
1187 div()
1188 .child(message.clone())
1189 .into_any_element(),
1190 ),
1191 Output::Table { content, .. } => {
1192 Some(content.clone().into_any_element())
1193 }
1194 Output::ErrorOutput(error_view) => {
1195 error_view.render(window, cx)
1196 }
1197 Output::ClearOutputWaitMarker => None,
1198 };
1199
1200 div().children(content)
1201 },
1202 ))),
1203 ),
1204 ),
1205 )
1206 },
1207 )
1208 // TODO: Move base cell render into trait impl so we don't have to repeat this
1209 .children(self.cell_position_spacer(false, window, cx))
1210 }
1211}
1212
1213pub struct RawCell {
1214 id: CellId,
1215 metadata: CellMetadata,
1216 source: String,
1217 selected: bool,
1218 cell_position: Option<CellPosition>,
1219}
1220
1221impl RawCell {
1222 pub fn to_nbformat_cell(&self) -> nbformat::v4::Cell {
1223 let source_lines: Vec<String> = self.source.lines().map(|l| format!("{}\n", l)).collect();
1224
1225 nbformat::v4::Cell::Raw {
1226 id: self.id.clone(),
1227 metadata: self.metadata.clone(),
1228 source: source_lines,
1229 }
1230 }
1231}
1232
1233impl RenderableCell for RawCell {
1234 const CELL_TYPE: CellType = CellType::Raw;
1235
1236 fn id(&self) -> &CellId {
1237 &self.id
1238 }
1239
1240 fn cell_type(&self) -> CellType {
1241 CellType::Raw
1242 }
1243
1244 fn metadata(&self) -> &CellMetadata {
1245 &self.metadata
1246 }
1247
1248 fn source(&self) -> &String {
1249 &self.source
1250 }
1251
1252 fn selected(&self) -> bool {
1253 self.selected
1254 }
1255
1256 fn set_selected(&mut self, selected: bool) -> &mut Self {
1257 self.selected = selected;
1258 self
1259 }
1260
1261 fn cell_position(&self) -> Option<&CellPosition> {
1262 self.cell_position.as_ref()
1263 }
1264
1265 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
1266 self.cell_position = Some(cell_position);
1267 self
1268 }
1269}
1270
1271impl Render for RawCell {
1272 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1273 v_flex()
1274 .size_full()
1275 // TODO: Move base cell render into trait impl so we don't have to repeat this
1276 .children(self.cell_position_spacer(true, window, cx))
1277 .child(
1278 h_flex()
1279 .w_full()
1280 .pr_2()
1281 .rounded_xs()
1282 .items_start()
1283 .gap(DynamicSpacing::Base08.rems(cx))
1284 .bg(self.selected_bg_color(window, cx))
1285 .child(self.gutter(window, cx))
1286 .child(
1287 div()
1288 .flex()
1289 .size_full()
1290 .flex_1()
1291 .p_3()
1292 .font_ui(cx)
1293 .text_size(TextSize::Default.rems(cx))
1294 .child(self.source.clone()),
1295 ),
1296 )
1297 // TODO: Move base cell render into trait impl so we don't have to repeat this
1298 .children(self.cell_position_spacer(false, window, cx))
1299 }
1300}