cell.rs

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