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