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 /// Load a code cell from notebook file data, including existing outputs and execution count
676 pub fn load(
677 id: CellId,
678 metadata: CellMetadata,
679 execution_count: Option<i32>,
680 source: String,
681 outputs: Vec<Output>,
682 notebook_language: Shared<Task<Option<Arc<Language>>>>,
683 window: &mut Window,
684 cx: &mut Context<Self>,
685 ) -> Self {
686 let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
687 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
688
689 let editor_view = cx.new(|cx| {
690 let mut editor = Editor::new(
691 EditorMode::AutoHeight {
692 min_lines: 1,
693 max_lines: Some(1024),
694 },
695 multi_buffer,
696 None,
697 window,
698 cx,
699 );
700
701 let theme = ThemeSettings::get_global(cx);
702 let refinement = TextStyleRefinement {
703 font_family: Some(theme.buffer_font.family.clone()),
704 font_size: Some(theme.buffer_font_size(cx).into()),
705 color: Some(cx.theme().colors().editor_foreground),
706 background_color: Some(gpui::transparent_black()),
707 ..Default::default()
708 };
709
710 editor.set_text(source.clone(), window, cx);
711 editor.set_show_gutter(false, cx);
712 editor.set_text_style_refinement(refinement);
713 editor
714 });
715
716 let language_task = cx.spawn_in(window, async move |_this, cx| {
717 let language = notebook_language.await;
718 buffer.update(cx, |buffer, cx| {
719 buffer.set_language(language.clone(), cx);
720 });
721 });
722
723 Self {
724 id,
725 metadata,
726 execution_count,
727 source,
728 editor: editor_view,
729 outputs,
730 selected: false,
731 cell_position: None,
732 language_task,
733 execution_start_time: None,
734 execution_duration: None,
735 is_executing: false,
736 }
737 }
738
739 pub fn editor(&self) -> &Entity<editor::Editor> {
740 &self.editor
741 }
742
743 pub fn current_source(&self, cx: &App) -> String {
744 let editor = self.editor.read(cx);
745 let buffer = editor.buffer().read(cx);
746 buffer
747 .as_singleton()
748 .map(|b| b.read(cx).text())
749 .unwrap_or_default()
750 }
751
752 pub fn is_dirty(&self, cx: &App) -> bool {
753 self.editor.read(cx).buffer().read(cx).is_dirty(cx)
754 }
755
756 pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
757 let source = self.current_source(cx);
758 let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
759
760 let outputs = self.outputs_to_nbformat(cx);
761
762 nbformat::v4::Cell::Code {
763 id: self.id.clone(),
764 metadata: self.metadata.clone(),
765 execution_count: self.execution_count,
766 source: source_lines,
767 outputs,
768 }
769 }
770
771 fn outputs_to_nbformat(&self, cx: &App) -> Vec<nbformat::v4::Output> {
772 self.outputs
773 .iter()
774 .filter_map(|output| output.to_nbformat(cx))
775 .collect()
776 }
777
778 pub fn has_outputs(&self) -> bool {
779 !self.outputs.is_empty()
780 }
781
782 pub fn clear_outputs(&mut self) {
783 self.outputs.clear();
784 self.execution_duration = None;
785 }
786
787 pub fn start_execution(&mut self) {
788 self.execution_start_time = Some(Instant::now());
789 self.execution_duration = None;
790 self.is_executing = true;
791 }
792
793 pub fn finish_execution(&mut self) {
794 if let Some(start_time) = self.execution_start_time.take() {
795 self.execution_duration = Some(start_time.elapsed());
796 }
797 self.is_executing = false;
798 }
799
800 pub fn is_executing(&self) -> bool {
801 self.is_executing
802 }
803
804 pub fn execution_duration(&self) -> Option<Duration> {
805 self.execution_duration
806 }
807
808 fn format_duration(duration: Duration) -> String {
809 let total_secs = duration.as_secs_f64();
810 if total_secs < 1.0 {
811 format!("{:.0}ms", duration.as_millis())
812 } else if total_secs < 60.0 {
813 format!("{:.1}s", total_secs)
814 } else {
815 let minutes = (total_secs / 60.0).floor() as u64;
816 let secs = total_secs % 60.0;
817 format!("{}m {:.1}s", minutes, secs)
818 }
819 }
820
821 pub fn handle_message(
822 &mut self,
823 message: &JupyterMessage,
824 window: &mut Window,
825 cx: &mut Context<Self>,
826 ) {
827 match &message.content {
828 JupyterMessageContent::StreamContent(stream) => {
829 self.outputs.push(Output::Stream {
830 content: cx.new(|cx| TerminalOutput::from(&stream.text, window, cx)),
831 });
832 }
833 JupyterMessageContent::DisplayData(display_data) => {
834 self.outputs
835 .push(Output::new(&display_data.data, None, window, cx));
836 }
837 JupyterMessageContent::ExecuteResult(execute_result) => {
838 self.outputs
839 .push(Output::new(&execute_result.data, None, window, cx));
840 }
841 JupyterMessageContent::ExecuteInput(input) => {
842 self.execution_count = serde_json::to_value(&input.execution_count)
843 .ok()
844 .and_then(|v| v.as_i64())
845 .map(|v| v as i32);
846 }
847 JupyterMessageContent::ExecuteReply(_) => {
848 self.finish_execution();
849 }
850 JupyterMessageContent::ErrorOutput(error) => {
851 self.outputs.push(Output::ErrorOutput(ErrorView {
852 ename: error.ename.clone(),
853 evalue: error.evalue.clone(),
854 traceback: cx
855 .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
856 }));
857 }
858 _ => {}
859 }
860 cx.notify();
861 }
862
863 fn output_control(&self) -> Option<CellControlType> {
864 if self.has_outputs() {
865 Some(CellControlType::ClearCell)
866 } else {
867 None
868 }
869 }
870
871 pub fn gutter_output(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
872 let is_selected = self.selected();
873
874 div()
875 .relative()
876 .h_full()
877 .w(px(GUTTER_WIDTH))
878 .child(
879 div()
880 .w(px(GUTTER_WIDTH))
881 .flex()
882 .flex_none()
883 .justify_center()
884 .h_full()
885 .child(
886 div()
887 .flex_none()
888 .w(px(1.))
889 .h_full()
890 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
891 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
892 ),
893 )
894 .when(self.has_outputs(), |this| {
895 this.child(
896 div()
897 .absolute()
898 .top(px(CODE_BLOCK_INSET - 2.0))
899 .left_0()
900 .flex()
901 .flex_none()
902 .w(px(GUTTER_WIDTH))
903 .h(px(GUTTER_WIDTH + 12.0))
904 .items_center()
905 .justify_center()
906 .bg(cx.theme().colors().tab_bar_background)
907 .child(IconButton::new("control", IconName::Ellipsis)),
908 )
909 })
910 }
911}
912
913impl RenderableCell for CodeCell {
914 const CELL_TYPE: CellType = CellType::Code;
915
916 fn id(&self) -> &CellId {
917 &self.id
918 }
919
920 fn cell_type(&self) -> CellType {
921 CellType::Code
922 }
923
924 fn metadata(&self) -> &CellMetadata {
925 &self.metadata
926 }
927
928 fn source(&self) -> &String {
929 &self.source
930 }
931
932 fn control(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<CellControl> {
933 let control_type = if self.has_outputs() {
934 CellControlType::RerunCell
935 } else {
936 CellControlType::RunCell
937 };
938
939 let cell_control = CellControl::new(
940 if self.has_outputs() {
941 "rerun-cell"
942 } else {
943 "run-cell"
944 },
945 control_type,
946 )
947 .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx)));
948
949 Some(cell_control)
950 }
951
952 fn selected(&self) -> bool {
953 self.selected
954 }
955
956 fn set_selected(&mut self, selected: bool) -> &mut Self {
957 self.selected = selected;
958 self
959 }
960
961 fn cell_position(&self) -> Option<&CellPosition> {
962 self.cell_position.as_ref()
963 }
964
965 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
966 self.cell_position = Some(cell_position);
967 self
968 }
969
970 fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
971 let is_selected = self.selected();
972 let execution_count = self.execution_count;
973
974 div()
975 .relative()
976 .h_full()
977 .w(px(GUTTER_WIDTH))
978 .child(
979 div()
980 .w(px(GUTTER_WIDTH))
981 .flex()
982 .flex_none()
983 .justify_center()
984 .h_full()
985 .child(
986 div()
987 .flex_none()
988 .w(px(1.))
989 .h_full()
990 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
991 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
992 ),
993 )
994 .when_some(self.control(window, cx), |this, control| {
995 this.child(
996 div()
997 .absolute()
998 .top(px(CODE_BLOCK_INSET - 2.0))
999 .left_0()
1000 .flex()
1001 .flex_col()
1002 .w(px(GUTTER_WIDTH))
1003 .items_center()
1004 .justify_center()
1005 .bg(cx.theme().colors().tab_bar_background)
1006 .child(control.button)
1007 .when_some(execution_count, |this, count| {
1008 this.child(
1009 div()
1010 .mt_1()
1011 .text_xs()
1012 .text_color(cx.theme().colors().text_muted)
1013 .child(format!("{}", count)),
1014 )
1015 }),
1016 )
1017 })
1018 }
1019}
1020
1021impl RunnableCell for CodeCell {
1022 fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1023 println!("Running code cell: {}", self.id);
1024 cx.emit(CellEvent::Run(self.id.clone()));
1025 }
1026
1027 fn execution_count(&self) -> Option<i32> {
1028 self.execution_count
1029 .and_then(|count| if count > 0 { Some(count) } else { None })
1030 }
1031
1032 fn set_execution_count(&mut self, count: i32) -> &mut Self {
1033 self.execution_count = Some(count);
1034 self
1035 }
1036}
1037
1038impl Render for CodeCell {
1039 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1040 let output_max_height = ReplSettings::get_global(cx).output_max_height_lines;
1041 let output_max_height = if output_max_height > 0 {
1042 Some(window.line_height() * output_max_height as f32)
1043 } else {
1044 None
1045 };
1046 let output_max_width = plain::max_width_for_columns(
1047 ReplSettings::get_global(cx).output_max_width_columns,
1048 window,
1049 cx,
1050 );
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 // Output portion
1109 .child(
1110 h_flex()
1111 .w_full()
1112 .pr_6()
1113 .rounded_xs()
1114 .items_start()
1115 .gap(DynamicSpacing::Base08.rems(cx))
1116 .bg(self.selected_bg_color(window, cx))
1117 .child(self.gutter_output(window, cx))
1118 .child(
1119 div().py_1p5().w_full().child(
1120 div()
1121 .flex()
1122 .size_full()
1123 .flex_1()
1124 .py_3()
1125 .px_5()
1126 .rounded_lg()
1127 .border_1()
1128 .child(
1129 div()
1130 .id((ElementId::from(self.id.to_string()), "output-scroll"))
1131 .w_full()
1132 .when_some(output_max_width, |div, max_w| {
1133 div.max_w(max_w).overflow_x_scroll()
1134 })
1135 .when_some(output_max_height, |div, max_h| {
1136 div.max_h(max_h).overflow_y_scroll()
1137 })
1138 .children(self.outputs.iter().map(|output| {
1139 let content = match output {
1140 Output::Plain { content, .. } => {
1141 Some(content.clone().into_any_element())
1142 }
1143 Output::Markdown { content, .. } => {
1144 Some(content.clone().into_any_element())
1145 }
1146 Output::Stream { content, .. } => {
1147 Some(content.clone().into_any_element())
1148 }
1149 Output::Image { content, .. } => {
1150 Some(content.clone().into_any_element())
1151 }
1152 Output::Message(message) => Some(
1153 div().child(message.clone()).into_any_element(),
1154 ),
1155 Output::Table { content, .. } => {
1156 Some(content.clone().into_any_element())
1157 }
1158 Output::Json { content, .. } => {
1159 Some(content.clone().into_any_element())
1160 }
1161 Output::ErrorOutput(error_view) => {
1162 error_view.render(window, cx)
1163 }
1164 Output::ClearOutputWaitMarker => None,
1165 };
1166
1167 div().children(content)
1168 })),
1169 ),
1170 ),
1171 ),
1172 )
1173 .when(
1174 self.has_outputs() || self.execution_duration.is_some() || self.is_executing,
1175 |this| {
1176 let execution_time_label = self.execution_duration.map(Self::format_duration);
1177 let is_executing = self.is_executing;
1178 this.child(
1179 h_flex()
1180 .w_full()
1181 .pr_6()
1182 .rounded_xs()
1183 .items_start()
1184 .gap(DynamicSpacing::Base08.rems(cx))
1185 .bg(self.selected_bg_color(window, cx))
1186 .child(self.gutter_output(window, cx))
1187 .child(
1188 div().py_1p5().w_full().child(
1189 v_flex()
1190 .size_full()
1191 .flex_1()
1192 .py_3()
1193 .px_5()
1194 .rounded_lg()
1195 .border_1()
1196 // execution status/time at the TOP
1197 .when(
1198 is_executing || execution_time_label.is_some(),
1199 |this| {
1200 let time_element = if is_executing {
1201 h_flex()
1202 .gap_1()
1203 .items_center()
1204 .child(
1205 Icon::new(IconName::ArrowCircle)
1206 .size(IconSize::XSmall)
1207 .color(Color::Warning)
1208 .with_rotate_animation(2)
1209 .into_any_element(),
1210 )
1211 .child(
1212 div()
1213 .text_xs()
1214 .text_color(
1215 cx.theme().colors().text_muted,
1216 )
1217 .child("Running..."),
1218 )
1219 .into_any_element()
1220 } else if let Some(duration_text) =
1221 execution_time_label.clone()
1222 {
1223 h_flex()
1224 .gap_1()
1225 .items_center()
1226 .child(
1227 Icon::new(IconName::Check)
1228 .size(IconSize::XSmall)
1229 .color(Color::Success),
1230 )
1231 .child(
1232 div()
1233 .text_xs()
1234 .text_color(
1235 cx.theme().colors().text_muted,
1236 )
1237 .child(duration_text),
1238 )
1239 .into_any_element()
1240 } else {
1241 div().into_any_element()
1242 };
1243 this.child(div().mb_2().child(time_element))
1244 },
1245 )
1246 // output at bottom
1247 .child(div().w_full().children(self.outputs.iter().map(
1248 |output| {
1249 let content = match output {
1250 Output::Plain { content, .. } => {
1251 Some(content.clone().into_any_element())
1252 }
1253 Output::Markdown { content, .. } => {
1254 Some(content.clone().into_any_element())
1255 }
1256 Output::Stream { content, .. } => {
1257 Some(content.clone().into_any_element())
1258 }
1259 Output::Image { content, .. } => {
1260 Some(content.clone().into_any_element())
1261 }
1262 Output::Message(message) => Some(
1263 div()
1264 .child(message.clone())
1265 .into_any_element(),
1266 ),
1267 Output::Table { content, .. } => {
1268 Some(content.clone().into_any_element())
1269 }
1270 Output::Json { content, .. } => {
1271 Some(content.clone().into_any_element())
1272 }
1273 Output::ErrorOutput(error_view) => {
1274 error_view.render(window, cx)
1275 }
1276 Output::ClearOutputWaitMarker => None,
1277 };
1278
1279 div().children(content)
1280 },
1281 ))),
1282 ),
1283 ),
1284 )
1285 },
1286 )
1287 // TODO: Move base cell render into trait impl so we don't have to repeat this
1288 .children(self.cell_position_spacer(false, window, cx))
1289 }
1290}
1291
1292pub struct RawCell {
1293 id: CellId,
1294 metadata: CellMetadata,
1295 source: String,
1296 selected: bool,
1297 cell_position: Option<CellPosition>,
1298}
1299
1300impl RawCell {
1301 pub fn to_nbformat_cell(&self) -> nbformat::v4::Cell {
1302 let source_lines: Vec<String> = self.source.lines().map(|l| format!("{}\n", l)).collect();
1303
1304 nbformat::v4::Cell::Raw {
1305 id: self.id.clone(),
1306 metadata: self.metadata.clone(),
1307 source: source_lines,
1308 }
1309 }
1310}
1311
1312impl RenderableCell for RawCell {
1313 const CELL_TYPE: CellType = CellType::Raw;
1314
1315 fn id(&self) -> &CellId {
1316 &self.id
1317 }
1318
1319 fn cell_type(&self) -> CellType {
1320 CellType::Raw
1321 }
1322
1323 fn metadata(&self) -> &CellMetadata {
1324 &self.metadata
1325 }
1326
1327 fn source(&self) -> &String {
1328 &self.source
1329 }
1330
1331 fn selected(&self) -> bool {
1332 self.selected
1333 }
1334
1335 fn set_selected(&mut self, selected: bool) -> &mut Self {
1336 self.selected = selected;
1337 self
1338 }
1339
1340 fn cell_position(&self) -> Option<&CellPosition> {
1341 self.cell_position.as_ref()
1342 }
1343
1344 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
1345 self.cell_position = Some(cell_position);
1346 self
1347 }
1348}
1349
1350impl Render for RawCell {
1351 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1352 v_flex()
1353 .size_full()
1354 // TODO: Move base cell render into trait impl so we don't have to repeat this
1355 .children(self.cell_position_spacer(true, window, cx))
1356 .child(
1357 h_flex()
1358 .w_full()
1359 .pr_2()
1360 .rounded_xs()
1361 .items_start()
1362 .gap(DynamicSpacing::Base08.rems(cx))
1363 .bg(self.selected_bg_color(window, cx))
1364 .child(self.gutter(window, cx))
1365 .child(
1366 div()
1367 .flex()
1368 .size_full()
1369 .flex_1()
1370 .p_3()
1371 .font_ui(cx)
1372 .text_size(TextSize::Default.rems(cx))
1373 .child(self.source.clone()),
1374 ),
1375 )
1376 // TODO: Move base cell render into trait impl so we don't have to repeat this
1377 .children(self.cell_position_spacer(false, window, cx))
1378 }
1379}