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