cell.rs

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