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