cell.rs

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