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