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