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_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block};
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 parsed_markdown: Option<markdown_preview::markdown_elements::ParsedMarkdown>,
326 markdown_parsing_task: Task<()>,
327 editing: bool,
328 selected: bool,
329 cell_position: Option<CellPosition>,
330 languages: Arc<LanguageRegistry>,
331 _editor_subscription: gpui::Subscription,
332}
333
334impl EventEmitter<MarkdownCellEvent> for MarkdownCell {}
335
336impl MarkdownCell {
337 pub fn new(
338 id: CellId,
339 metadata: CellMetadata,
340 source: String,
341 languages: Arc<LanguageRegistry>,
342 window: &mut Window,
343 cx: &mut Context<Self>,
344 ) -> Self {
345 let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
346 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
347
348 let markdown_language = languages.language_for_name("Markdown");
349 cx.spawn_in(window, async move |_this, cx| {
350 if let Some(markdown) = markdown_language.await.log_err() {
351 buffer.update(cx, |buffer, cx| {
352 buffer.set_language(Some(markdown), cx);
353 });
354 }
355 })
356 .detach();
357
358 let editor = cx.new(|cx| {
359 let mut editor = Editor::new(
360 EditorMode::AutoHeight {
361 min_lines: 1,
362 max_lines: Some(1024),
363 },
364 multi_buffer,
365 None,
366 window,
367 cx,
368 );
369
370 let theme = ThemeSettings::get_global(cx);
371 let refinement = TextStyleRefinement {
372 font_family: Some(theme.buffer_font.family.clone()),
373 font_size: Some(theme.buffer_font_size(cx).into()),
374 color: Some(cx.theme().colors().editor_foreground),
375 background_color: Some(gpui::transparent_black()),
376 ..Default::default()
377 };
378
379 editor.set_show_gutter(false, cx);
380 editor.set_text_style_refinement(refinement);
381 editor
382 });
383
384 let markdown_parsing_task = {
385 let languages = languages.clone();
386 let source = source.clone();
387
388 cx.spawn_in(window, async move |this, cx| {
389 let parsed_markdown = cx
390 .background_spawn(async move {
391 parse_markdown(&source, None, Some(languages)).await
392 })
393 .await;
394
395 this.update(cx, |cell: &mut MarkdownCell, _| {
396 cell.parsed_markdown = Some(parsed_markdown);
397 })
398 .log_err();
399 })
400 };
401
402 let cell_id = id.clone();
403 let editor_subscription =
404 cx.subscribe(&editor, move |this, _editor, event, cx| match event {
405 editor::EditorEvent::Blurred => {
406 if this.editing {
407 this.editing = false;
408 cx.emit(MarkdownCellEvent::FinishedEditing);
409 cx.notify();
410 }
411 }
412 _ => {}
413 });
414
415 let start_editing = source.is_empty();
416 Self {
417 id,
418 metadata,
419 image_cache: RetainAllImageCache::new(cx),
420 source,
421 editor,
422 parsed_markdown: None,
423 markdown_parsing_task,
424 editing: start_editing, // Start in edit mode if empty
425 selected: false,
426 cell_position: None,
427 languages,
428 _editor_subscription: editor_subscription,
429 }
430 }
431
432 pub fn editor(&self) -> &Entity<Editor> {
433 &self.editor
434 }
435
436 pub fn current_source(&self, cx: &App) -> String {
437 let editor = self.editor.read(cx);
438 let buffer = editor.buffer().read(cx);
439 buffer
440 .as_singleton()
441 .map(|b| b.read(cx).text())
442 .unwrap_or_default()
443 }
444
445 pub fn is_dirty(&self, cx: &App) -> bool {
446 self.editor.read(cx).buffer().read(cx).is_dirty(cx)
447 }
448
449 pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
450 let source = self.current_source(cx);
451 let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
452
453 nbformat::v4::Cell::Markdown {
454 id: self.id.clone(),
455 metadata: self.metadata.clone(),
456 source: source_lines,
457 attachments: None,
458 }
459 }
460
461 pub fn is_editing(&self) -> bool {
462 self.editing
463 }
464
465 pub fn set_editing(&mut self, editing: bool) {
466 self.editing = editing;
467 }
468
469 pub fn reparse_markdown(&mut self, cx: &mut Context<Self>) {
470 let editor = self.editor.read(cx);
471 let buffer = editor.buffer().read(cx);
472 let source = buffer
473 .as_singleton()
474 .map(|b| b.read(cx).text())
475 .unwrap_or_default();
476
477 self.source = source.clone();
478 let languages = self.languages.clone();
479
480 self.markdown_parsing_task = cx.spawn(async move |this, cx| {
481 let parsed_markdown = cx
482 .background_spawn(
483 async move { parse_markdown(&source, None, Some(languages)).await },
484 )
485 .await;
486
487 this.update(cx, |cell: &mut MarkdownCell, cx| {
488 cell.parsed_markdown = Some(parsed_markdown);
489 cx.notify();
490 })
491 .log_err();
492 });
493 }
494
495 /// Called when user presses Shift+Enter or Ctrl+Enter while editing.
496 /// Finishes editing and signals to move to the next cell.
497 pub fn run(&mut self, cx: &mut Context<Self>) {
498 if self.editing {
499 self.editing = false;
500 cx.emit(MarkdownCellEvent::FinishedEditing);
501 cx.emit(MarkdownCellEvent::Run(self.id.clone()));
502 cx.notify();
503 }
504 }
505}
506
507impl RenderableCell for MarkdownCell {
508 const CELL_TYPE: CellType = CellType::Markdown;
509
510 fn id(&self) -> &CellId {
511 &self.id
512 }
513
514 fn cell_type(&self) -> CellType {
515 CellType::Markdown
516 }
517
518 fn metadata(&self) -> &CellMetadata {
519 &self.metadata
520 }
521
522 fn source(&self) -> &String {
523 &self.source
524 }
525
526 fn selected(&self) -> bool {
527 self.selected
528 }
529
530 fn set_selected(&mut self, selected: bool) -> &mut Self {
531 self.selected = selected;
532 self
533 }
534
535 fn control(&self, _window: &mut Window, _: &mut Context<Self>) -> Option<CellControl> {
536 None
537 }
538
539 fn cell_position(&self) -> Option<&CellPosition> {
540 self.cell_position.as_ref()
541 }
542
543 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
544 self.cell_position = Some(cell_position);
545 self
546 }
547}
548
549impl Render for MarkdownCell {
550 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
551 // If editing, show the editor
552 if self.editing {
553 return v_flex()
554 .size_full()
555 .children(self.cell_position_spacer(true, window, cx))
556 .child(
557 h_flex()
558 .w_full()
559 .pr_6()
560 .rounded_xs()
561 .items_start()
562 .gap(DynamicSpacing::Base08.rems(cx))
563 .bg(self.selected_bg_color(window, cx))
564 .child(self.gutter(window, cx))
565 .child(
566 div()
567 .flex_1()
568 .p_3()
569 .bg(cx.theme().colors().editor_background)
570 .rounded_sm()
571 .child(self.editor.clone())
572 .on_mouse_down(
573 gpui::MouseButton::Left,
574 cx.listener(|_this, _event, _window, _cx| {
575 // Prevent the click from propagating
576 }),
577 ),
578 ),
579 )
580 .children(self.cell_position_spacer(false, window, cx));
581 }
582
583 // Preview mode - show rendered markdown
584 let Some(parsed) = self.parsed_markdown.as_ref() else {
585 // No parsed content yet, show placeholder that can be clicked to edit
586 let focus_handle = self.editor.focus_handle(cx);
587 return v_flex()
588 .size_full()
589 .children(self.cell_position_spacer(true, window, cx))
590 .child(
591 h_flex()
592 .w_full()
593 .pr_6()
594 .rounded_xs()
595 .items_start()
596 .gap(DynamicSpacing::Base08.rems(cx))
597 .bg(self.selected_bg_color(window, cx))
598 .child(self.gutter(window, cx))
599 .child(
600 div()
601 .id("markdown-placeholder")
602 .flex_1()
603 .p_3()
604 .italic()
605 .text_color(cx.theme().colors().text_muted)
606 .child("Click to edit markdown...")
607 .cursor_pointer()
608 .on_click(cx.listener(move |this, _event, window, cx| {
609 this.editing = true;
610 window.focus(&this.editor.focus_handle(cx), cx);
611 cx.notify();
612 })),
613 ),
614 )
615 .children(self.cell_position_spacer(false, window, cx));
616 };
617
618 let mut markdown_render_context =
619 markdown_preview::markdown_renderer::RenderContext::new(None, window, cx);
620
621 v_flex()
622 .size_full()
623 .children(self.cell_position_spacer(true, window, cx))
624 .child(
625 h_flex()
626 .w_full()
627 .pr_6()
628 .rounded_xs()
629 .items_start()
630 .gap(DynamicSpacing::Base08.rems(cx))
631 .bg(self.selected_bg_color(window, cx))
632 .child(self.gutter(window, cx))
633 .child(
634 v_flex()
635 .image_cache(self.image_cache.clone())
636 .id("markdown-content")
637 .size_full()
638 .flex_1()
639 .p_3()
640 .font_ui(cx)
641 .text_size(TextSize::Default.rems(cx))
642 .cursor_pointer()
643 .on_click(cx.listener(|this, _event, window, cx| {
644 this.editing = true;
645 window.focus(&this.editor.focus_handle(cx), cx);
646 cx.notify();
647 }))
648 .children(parsed.children.iter().map(|child| {
649 div().relative().child(div().relative().child(
650 render_markdown_block(child, &mut markdown_render_context),
651 ))
652 })),
653 ),
654 )
655 .children(self.cell_position_spacer(false, window, cx))
656 }
657}
658
659pub struct CodeCell {
660 id: CellId,
661 metadata: CellMetadata,
662 execution_count: Option<i32>,
663 source: String,
664 editor: Entity<editor::Editor>,
665 outputs: Vec<Output>,
666 selected: bool,
667 cell_position: Option<CellPosition>,
668 language_task: Task<()>,
669 execution_start_time: Option<Instant>,
670 execution_duration: Option<Duration>,
671 is_executing: bool,
672}
673
674impl EventEmitter<CellEvent> for CodeCell {}
675
676impl CodeCell {
677 pub fn new(
678 id: CellId,
679 metadata: CellMetadata,
680 source: String,
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_show_gutter(false, cx);
710 editor.set_text_style_refinement(refinement);
711 editor
712 });
713
714 let language_task = cx.spawn_in(window, async move |_this, cx| {
715 let language = notebook_language.await;
716 buffer.update(cx, |buffer, cx| {
717 buffer.set_language(language.clone(), cx);
718 });
719 });
720
721 Self {
722 id,
723 metadata,
724 execution_count: None,
725 source,
726 editor: editor_view,
727 outputs: Vec::new(),
728 selected: false,
729 cell_position: None,
730 language_task,
731 execution_start_time: None,
732 execution_duration: None,
733 is_executing: false,
734 }
735 }
736
737 /// Load a code cell from notebook file data, including existing outputs and execution count
738 pub fn load(
739 id: CellId,
740 metadata: CellMetadata,
741 execution_count: Option<i32>,
742 source: String,
743 outputs: Vec<Output>,
744 notebook_language: Shared<Task<Option<Arc<Language>>>>,
745 window: &mut Window,
746 cx: &mut Context<Self>,
747 ) -> Self {
748 let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
749 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
750
751 let editor_view = cx.new(|cx| {
752 let mut editor = Editor::new(
753 EditorMode::AutoHeight {
754 min_lines: 1,
755 max_lines: Some(1024),
756 },
757 multi_buffer,
758 None,
759 window,
760 cx,
761 );
762
763 let theme = ThemeSettings::get_global(cx);
764 let refinement = TextStyleRefinement {
765 font_family: Some(theme.buffer_font.family.clone()),
766 font_size: Some(theme.buffer_font_size(cx).into()),
767 color: Some(cx.theme().colors().editor_foreground),
768 background_color: Some(gpui::transparent_black()),
769 ..Default::default()
770 };
771
772 editor.set_text(source.clone(), window, cx);
773 editor.set_show_gutter(false, cx);
774 editor.set_text_style_refinement(refinement);
775 editor
776 });
777
778 let language_task = cx.spawn_in(window, async move |_this, cx| {
779 let language = notebook_language.await;
780 buffer.update(cx, |buffer, cx| {
781 buffer.set_language(language.clone(), cx);
782 });
783 });
784
785 Self {
786 id,
787 metadata,
788 execution_count,
789 source,
790 editor: editor_view,
791 outputs,
792 selected: false,
793 cell_position: None,
794 language_task,
795 execution_start_time: None,
796 execution_duration: None,
797 is_executing: false,
798 }
799 }
800
801 pub fn editor(&self) -> &Entity<editor::Editor> {
802 &self.editor
803 }
804
805 pub fn current_source(&self, cx: &App) -> String {
806 let editor = self.editor.read(cx);
807 let buffer = editor.buffer().read(cx);
808 buffer
809 .as_singleton()
810 .map(|b| b.read(cx).text())
811 .unwrap_or_default()
812 }
813
814 pub fn is_dirty(&self, cx: &App) -> bool {
815 self.editor.read(cx).buffer().read(cx).is_dirty(cx)
816 }
817
818 pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
819 let source = self.current_source(cx);
820 let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
821
822 let outputs = self.outputs_to_nbformat(cx);
823
824 nbformat::v4::Cell::Code {
825 id: self.id.clone(),
826 metadata: self.metadata.clone(),
827 execution_count: self.execution_count,
828 source: source_lines,
829 outputs,
830 }
831 }
832
833 fn outputs_to_nbformat(&self, cx: &App) -> Vec<nbformat::v4::Output> {
834 self.outputs
835 .iter()
836 .filter_map(|output| output.to_nbformat(cx))
837 .collect()
838 }
839
840 pub fn has_outputs(&self) -> bool {
841 !self.outputs.is_empty()
842 }
843
844 pub fn clear_outputs(&mut self) {
845 self.outputs.clear();
846 self.execution_duration = None;
847 }
848
849 pub fn start_execution(&mut self) {
850 self.execution_start_time = Some(Instant::now());
851 self.execution_duration = None;
852 self.is_executing = true;
853 }
854
855 pub fn finish_execution(&mut self) {
856 if let Some(start_time) = self.execution_start_time.take() {
857 self.execution_duration = Some(start_time.elapsed());
858 }
859 self.is_executing = false;
860 }
861
862 pub fn is_executing(&self) -> bool {
863 self.is_executing
864 }
865
866 pub fn execution_duration(&self) -> Option<Duration> {
867 self.execution_duration
868 }
869
870 fn format_duration(duration: Duration) -> String {
871 let total_secs = duration.as_secs_f64();
872 if total_secs < 1.0 {
873 format!("{:.0}ms", duration.as_millis())
874 } else if total_secs < 60.0 {
875 format!("{:.1}s", total_secs)
876 } else {
877 let minutes = (total_secs / 60.0).floor() as u64;
878 let secs = total_secs % 60.0;
879 format!("{}m {:.1}s", minutes, secs)
880 }
881 }
882
883 pub fn handle_message(
884 &mut self,
885 message: &JupyterMessage,
886 window: &mut Window,
887 cx: &mut Context<Self>,
888 ) {
889 match &message.content {
890 JupyterMessageContent::StreamContent(stream) => {
891 self.outputs.push(Output::Stream {
892 content: cx.new(|cx| TerminalOutput::from(&stream.text, window, cx)),
893 });
894 }
895 JupyterMessageContent::DisplayData(display_data) => {
896 self.outputs
897 .push(Output::new(&display_data.data, None, window, cx));
898 }
899 JupyterMessageContent::ExecuteResult(execute_result) => {
900 self.outputs
901 .push(Output::new(&execute_result.data, None, window, cx));
902 }
903 JupyterMessageContent::ExecuteInput(input) => {
904 self.execution_count = serde_json::to_value(&input.execution_count)
905 .ok()
906 .and_then(|v| v.as_i64())
907 .map(|v| v as i32);
908 }
909 JupyterMessageContent::ExecuteReply(_) => {
910 self.finish_execution();
911 }
912 JupyterMessageContent::ErrorOutput(error) => {
913 self.outputs.push(Output::ErrorOutput(ErrorView {
914 ename: error.ename.clone(),
915 evalue: error.evalue.clone(),
916 traceback: cx
917 .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
918 }));
919 }
920 _ => {}
921 }
922 cx.notify();
923 }
924
925 fn output_control(&self) -> Option<CellControlType> {
926 if self.has_outputs() {
927 Some(CellControlType::ClearCell)
928 } else {
929 None
930 }
931 }
932
933 pub fn gutter_output(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
934 let is_selected = self.selected();
935
936 div()
937 .relative()
938 .h_full()
939 .w(px(GUTTER_WIDTH))
940 .child(
941 div()
942 .w(px(GUTTER_WIDTH))
943 .flex()
944 .flex_none()
945 .justify_center()
946 .h_full()
947 .child(
948 div()
949 .flex_none()
950 .w(px(1.))
951 .h_full()
952 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
953 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
954 ),
955 )
956 .when(self.has_outputs(), |this| {
957 this.child(
958 div()
959 .absolute()
960 .top(px(CODE_BLOCK_INSET - 2.0))
961 .left_0()
962 .flex()
963 .flex_none()
964 .w(px(GUTTER_WIDTH))
965 .h(px(GUTTER_WIDTH + 12.0))
966 .items_center()
967 .justify_center()
968 .bg(cx.theme().colors().tab_bar_background)
969 .child(IconButton::new("control", IconName::Ellipsis)),
970 )
971 })
972 }
973}
974
975impl RenderableCell for CodeCell {
976 const CELL_TYPE: CellType = CellType::Code;
977
978 fn id(&self) -> &CellId {
979 &self.id
980 }
981
982 fn cell_type(&self) -> CellType {
983 CellType::Code
984 }
985
986 fn metadata(&self) -> &CellMetadata {
987 &self.metadata
988 }
989
990 fn source(&self) -> &String {
991 &self.source
992 }
993
994 fn control(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<CellControl> {
995 let control_type = if self.has_outputs() {
996 CellControlType::RerunCell
997 } else {
998 CellControlType::RunCell
999 };
1000
1001 let cell_control = CellControl::new(
1002 if self.has_outputs() {
1003 "rerun-cell"
1004 } else {
1005 "run-cell"
1006 },
1007 control_type,
1008 )
1009 .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx)));
1010
1011 Some(cell_control)
1012 }
1013
1014 fn selected(&self) -> bool {
1015 self.selected
1016 }
1017
1018 fn set_selected(&mut self, selected: bool) -> &mut Self {
1019 self.selected = selected;
1020 self
1021 }
1022
1023 fn cell_position(&self) -> Option<&CellPosition> {
1024 self.cell_position.as_ref()
1025 }
1026
1027 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
1028 self.cell_position = Some(cell_position);
1029 self
1030 }
1031
1032 fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1033 let is_selected = self.selected();
1034 let execution_count = self.execution_count;
1035
1036 div()
1037 .relative()
1038 .h_full()
1039 .w(px(GUTTER_WIDTH))
1040 .child(
1041 div()
1042 .w(px(GUTTER_WIDTH))
1043 .flex()
1044 .flex_none()
1045 .justify_center()
1046 .h_full()
1047 .child(
1048 div()
1049 .flex_none()
1050 .w(px(1.))
1051 .h_full()
1052 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
1053 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
1054 ),
1055 )
1056 .when_some(self.control(window, cx), |this, control| {
1057 this.child(
1058 div()
1059 .absolute()
1060 .top(px(CODE_BLOCK_INSET - 2.0))
1061 .left_0()
1062 .flex()
1063 .flex_col()
1064 .w(px(GUTTER_WIDTH))
1065 .items_center()
1066 .justify_center()
1067 .bg(cx.theme().colors().tab_bar_background)
1068 .child(control.button)
1069 .when_some(execution_count, |this, count| {
1070 this.child(
1071 div()
1072 .mt_1()
1073 .text_xs()
1074 .text_color(cx.theme().colors().text_muted)
1075 .child(format!("{}", count)),
1076 )
1077 }),
1078 )
1079 })
1080 }
1081}
1082
1083impl RunnableCell for CodeCell {
1084 fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1085 println!("Running code cell: {}", self.id);
1086 cx.emit(CellEvent::Run(self.id.clone()));
1087 }
1088
1089 fn execution_count(&self) -> Option<i32> {
1090 self.execution_count
1091 .and_then(|count| if count > 0 { Some(count) } else { None })
1092 }
1093
1094 fn set_execution_count(&mut self, count: i32) -> &mut Self {
1095 self.execution_count = Some(count);
1096 self
1097 }
1098}
1099
1100impl Render for CodeCell {
1101 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1102 // get the language from the editor's buffer
1103 let language_name = self
1104 .editor
1105 .read(cx)
1106 .buffer()
1107 .read(cx)
1108 .as_singleton()
1109 .and_then(|buffer| buffer.read(cx).language())
1110 .map(|lang| lang.name().to_string());
1111
1112 v_flex()
1113 .size_full()
1114 // TODO: Move base cell render into trait impl so we don't have to repeat this
1115 .children(self.cell_position_spacer(true, window, cx))
1116 // Editor portion
1117 .child(
1118 h_flex()
1119 .w_full()
1120 .pr_6()
1121 .rounded_xs()
1122 .items_start()
1123 .gap(DynamicSpacing::Base08.rems(cx))
1124 .bg(self.selected_bg_color(window, cx))
1125 .child(self.gutter(window, cx))
1126 .child(
1127 div().py_1p5().w_full().child(
1128 div()
1129 .relative()
1130 .flex()
1131 .size_full()
1132 .flex_1()
1133 .py_3()
1134 .px_5()
1135 .rounded_lg()
1136 .border_1()
1137 .border_color(cx.theme().colors().border)
1138 .bg(cx.theme().colors().editor_background)
1139 .child(div().w_full().child(self.editor.clone()))
1140 // lang badge in top-right corner
1141 .when_some(language_name, |this, name| {
1142 this.child(
1143 div()
1144 .absolute()
1145 .top_1()
1146 .right_2()
1147 .px_2()
1148 .py_0p5()
1149 .rounded_md()
1150 .bg(cx.theme().colors().element_background.opacity(0.7))
1151 .text_xs()
1152 .text_color(cx.theme().colors().text_muted)
1153 .child(name),
1154 )
1155 }),
1156 ),
1157 ),
1158 )
1159 // Output portion
1160 .when(
1161 self.has_outputs() || self.execution_duration.is_some() || self.is_executing,
1162 |this| {
1163 let execution_time_label = self.execution_duration.map(Self::format_duration);
1164 let is_executing = self.is_executing;
1165 this.child(
1166 h_flex()
1167 .w_full()
1168 .pr_6()
1169 .rounded_xs()
1170 .items_start()
1171 .gap(DynamicSpacing::Base08.rems(cx))
1172 .bg(self.selected_bg_color(window, cx))
1173 .child(self.gutter_output(window, cx))
1174 .child(
1175 div().py_1p5().w_full().child(
1176 v_flex()
1177 .size_full()
1178 .flex_1()
1179 .py_3()
1180 .px_5()
1181 .rounded_lg()
1182 .border_1()
1183 // execution status/time at the TOP
1184 .when(
1185 is_executing || execution_time_label.is_some(),
1186 |this| {
1187 let time_element = if is_executing {
1188 h_flex()
1189 .gap_1()
1190 .items_center()
1191 .child(
1192 Icon::new(IconName::ArrowCircle)
1193 .size(IconSize::XSmall)
1194 .color(Color::Warning)
1195 .with_rotate_animation(2)
1196 .into_any_element(),
1197 )
1198 .child(
1199 div()
1200 .text_xs()
1201 .text_color(
1202 cx.theme().colors().text_muted,
1203 )
1204 .child("Running..."),
1205 )
1206 .into_any_element()
1207 } else if let Some(duration_text) =
1208 execution_time_label.clone()
1209 {
1210 h_flex()
1211 .gap_1()
1212 .items_center()
1213 .child(
1214 Icon::new(IconName::Check)
1215 .size(IconSize::XSmall)
1216 .color(Color::Success),
1217 )
1218 .child(
1219 div()
1220 .text_xs()
1221 .text_color(
1222 cx.theme().colors().text_muted,
1223 )
1224 .child(duration_text),
1225 )
1226 .into_any_element()
1227 } else {
1228 div().into_any_element()
1229 };
1230 this.child(div().mb_2().child(time_element))
1231 },
1232 )
1233 // output at bottom
1234 .child(div().w_full().children(self.outputs.iter().map(
1235 |output| {
1236 let content = match output {
1237 Output::Plain { content, .. } => {
1238 Some(content.clone().into_any_element())
1239 }
1240 Output::Markdown { content, .. } => {
1241 Some(content.clone().into_any_element())
1242 }
1243 Output::Stream { content, .. } => {
1244 Some(content.clone().into_any_element())
1245 }
1246 Output::Image { content, .. } => {
1247 Some(content.clone().into_any_element())
1248 }
1249 Output::Message(message) => Some(
1250 div()
1251 .child(message.clone())
1252 .into_any_element(),
1253 ),
1254 Output::Table { content, .. } => {
1255 Some(content.clone().into_any_element())
1256 }
1257 Output::ErrorOutput(error_view) => {
1258 error_view.render(window, cx)
1259 }
1260 Output::ClearOutputWaitMarker => None,
1261 };
1262
1263 div().children(content)
1264 },
1265 ))),
1266 ),
1267 ),
1268 )
1269 },
1270 )
1271 // TODO: Move base cell render into trait impl so we don't have to repeat this
1272 .children(self.cell_position_spacer(false, window, cx))
1273 }
1274}
1275
1276pub struct RawCell {
1277 id: CellId,
1278 metadata: CellMetadata,
1279 source: String,
1280 selected: bool,
1281 cell_position: Option<CellPosition>,
1282}
1283
1284impl RawCell {
1285 pub fn to_nbformat_cell(&self) -> nbformat::v4::Cell {
1286 let source_lines: Vec<String> = self.source.lines().map(|l| format!("{}\n", l)).collect();
1287
1288 nbformat::v4::Cell::Raw {
1289 id: self.id.clone(),
1290 metadata: self.metadata.clone(),
1291 source: source_lines,
1292 }
1293 }
1294}
1295
1296impl RenderableCell for RawCell {
1297 const CELL_TYPE: CellType = CellType::Raw;
1298
1299 fn id(&self) -> &CellId {
1300 &self.id
1301 }
1302
1303 fn cell_type(&self) -> CellType {
1304 CellType::Raw
1305 }
1306
1307 fn metadata(&self) -> &CellMetadata {
1308 &self.metadata
1309 }
1310
1311 fn source(&self) -> &String {
1312 &self.source
1313 }
1314
1315 fn selected(&self) -> bool {
1316 self.selected
1317 }
1318
1319 fn set_selected(&mut self, selected: bool) -> &mut Self {
1320 self.selected = selected;
1321 self
1322 }
1323
1324 fn cell_position(&self) -> Option<&CellPosition> {
1325 self.cell_position.as_ref()
1326 }
1327
1328 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
1329 self.cell_position = Some(cell_position);
1330 self
1331 }
1332}
1333
1334impl Render for RawCell {
1335 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1336 v_flex()
1337 .size_full()
1338 // TODO: Move base cell render into trait impl so we don't have to repeat this
1339 .children(self.cell_position_spacer(true, window, cx))
1340 .child(
1341 h_flex()
1342 .w_full()
1343 .pr_2()
1344 .rounded_xs()
1345 .items_start()
1346 .gap(DynamicSpacing::Base08.rems(cx))
1347 .bg(self.selected_bg_color(window, cx))
1348 .child(self.gutter(window, cx))
1349 .child(
1350 div()
1351 .flex()
1352 .size_full()
1353 .flex_1()
1354 .p_3()
1355 .font_ui(cx)
1356 .text_size(TextSize::Default.rems(cx))
1357 .child(self.source.clone()),
1358 ),
1359 )
1360 // TODO: Move base cell render into trait impl so we don't have to repeat this
1361 .children(self.cell_position_spacer(false, window, cx))
1362 }
1363}