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_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block};
  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::TerminalOutput, user_error::ErrorView},
  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    parsed_markdown: Option<markdown_preview::markdown_elements::ParsedMarkdown>,
 326    markdown_parsing_task: Task<()>,
 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_parsing_task = {
 385            let languages = languages.clone();
 386            let source = source.clone();
 387
 388            cx.spawn_in(window, async move |this, cx| {
 389                let parsed_markdown = cx
 390                    .background_spawn(async move {
 391                        parse_markdown(&source, None, Some(languages)).await
 392                    })
 393                    .await;
 394
 395                this.update(cx, |cell: &mut MarkdownCell, _| {
 396                    cell.parsed_markdown = Some(parsed_markdown);
 397                })
 398                .log_err();
 399            })
 400        };
 401
 402        let cell_id = id.clone();
 403        let editor_subscription =
 404            cx.subscribe(&editor, move |this, _editor, event, cx| match event {
 405                editor::EditorEvent::Blurred => {
 406                    if this.editing {
 407                        this.editing = false;
 408                        cx.emit(MarkdownCellEvent::FinishedEditing);
 409                        cx.notify();
 410                    }
 411                }
 412                _ => {}
 413            });
 414
 415        let start_editing = source.is_empty();
 416        Self {
 417            id,
 418            metadata,
 419            image_cache: RetainAllImageCache::new(cx),
 420            source,
 421            editor,
 422            parsed_markdown: None,
 423            markdown_parsing_task,
 424            editing: start_editing, // Start in edit mode if empty
 425            selected: false,
 426            cell_position: None,
 427            languages,
 428            _editor_subscription: editor_subscription,
 429        }
 430    }
 431
 432    pub fn editor(&self) -> &Entity<Editor> {
 433        &self.editor
 434    }
 435
 436    pub fn current_source(&self, cx: &App) -> String {
 437        let editor = self.editor.read(cx);
 438        let buffer = editor.buffer().read(cx);
 439        buffer
 440            .as_singleton()
 441            .map(|b| b.read(cx).text())
 442            .unwrap_or_default()
 443    }
 444
 445    pub fn is_dirty(&self, cx: &App) -> bool {
 446        self.editor.read(cx).buffer().read(cx).is_dirty(cx)
 447    }
 448
 449    pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
 450        let source = self.current_source(cx);
 451        let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
 452
 453        nbformat::v4::Cell::Markdown {
 454            id: self.id.clone(),
 455            metadata: self.metadata.clone(),
 456            source: source_lines,
 457            attachments: None,
 458        }
 459    }
 460
 461    pub fn is_editing(&self) -> bool {
 462        self.editing
 463    }
 464
 465    pub fn set_editing(&mut self, editing: bool) {
 466        self.editing = editing;
 467    }
 468
 469    pub fn reparse_markdown(&mut self, cx: &mut Context<Self>) {
 470        let editor = self.editor.read(cx);
 471        let buffer = editor.buffer().read(cx);
 472        let source = buffer
 473            .as_singleton()
 474            .map(|b| b.read(cx).text())
 475            .unwrap_or_default();
 476
 477        self.source = source.clone();
 478        let languages = self.languages.clone();
 479
 480        self.markdown_parsing_task = cx.spawn(async move |this, cx| {
 481            let parsed_markdown = cx
 482                .background_spawn(
 483                    async move { parse_markdown(&source, None, Some(languages)).await },
 484                )
 485                .await;
 486
 487            this.update(cx, |cell: &mut MarkdownCell, cx| {
 488                cell.parsed_markdown = Some(parsed_markdown);
 489                cx.notify();
 490            })
 491            .log_err();
 492        });
 493    }
 494
 495    /// Called when user presses Shift+Enter or Ctrl+Enter while editing.
 496    /// Finishes editing and signals to move to the next cell.
 497    pub fn run(&mut self, cx: &mut Context<Self>) {
 498        if self.editing {
 499            self.editing = false;
 500            cx.emit(MarkdownCellEvent::FinishedEditing);
 501            cx.emit(MarkdownCellEvent::Run(self.id.clone()));
 502            cx.notify();
 503        }
 504    }
 505}
 506
 507impl RenderableCell for MarkdownCell {
 508    const CELL_TYPE: CellType = CellType::Markdown;
 509
 510    fn id(&self) -> &CellId {
 511        &self.id
 512    }
 513
 514    fn cell_type(&self) -> CellType {
 515        CellType::Markdown
 516    }
 517
 518    fn metadata(&self) -> &CellMetadata {
 519        &self.metadata
 520    }
 521
 522    fn source(&self) -> &String {
 523        &self.source
 524    }
 525
 526    fn selected(&self) -> bool {
 527        self.selected
 528    }
 529
 530    fn set_selected(&mut self, selected: bool) -> &mut Self {
 531        self.selected = selected;
 532        self
 533    }
 534
 535    fn control(&self, _window: &mut Window, _: &mut Context<Self>) -> Option<CellControl> {
 536        None
 537    }
 538
 539    fn cell_position(&self) -> Option<&CellPosition> {
 540        self.cell_position.as_ref()
 541    }
 542
 543    fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
 544        self.cell_position = Some(cell_position);
 545        self
 546    }
 547}
 548
 549impl Render for MarkdownCell {
 550    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 551        // If editing, show the editor
 552        if self.editing {
 553            return v_flex()
 554                .size_full()
 555                .children(self.cell_position_spacer(true, window, cx))
 556                .child(
 557                    h_flex()
 558                        .w_full()
 559                        .pr_6()
 560                        .rounded_xs()
 561                        .items_start()
 562                        .gap(DynamicSpacing::Base08.rems(cx))
 563                        .bg(self.selected_bg_color(window, cx))
 564                        .child(self.gutter(window, cx))
 565                        .child(
 566                            div()
 567                                .flex_1()
 568                                .p_3()
 569                                .bg(cx.theme().colors().editor_background)
 570                                .rounded_sm()
 571                                .child(self.editor.clone())
 572                                .on_mouse_down(
 573                                    gpui::MouseButton::Left,
 574                                    cx.listener(|_this, _event, _window, _cx| {
 575                                        // Prevent the click from propagating
 576                                    }),
 577                                ),
 578                        ),
 579                )
 580                .children(self.cell_position_spacer(false, window, cx));
 581        }
 582
 583        // Preview mode - show rendered markdown
 584        let Some(parsed) = self.parsed_markdown.as_ref() else {
 585            // No parsed content yet, show placeholder that can be clicked to edit
 586            let focus_handle = self.editor.focus_handle(cx);
 587            return v_flex()
 588                .size_full()
 589                .children(self.cell_position_spacer(true, window, cx))
 590                .child(
 591                    h_flex()
 592                        .w_full()
 593                        .pr_6()
 594                        .rounded_xs()
 595                        .items_start()
 596                        .gap(DynamicSpacing::Base08.rems(cx))
 597                        .bg(self.selected_bg_color(window, cx))
 598                        .child(self.gutter(window, cx))
 599                        .child(
 600                            div()
 601                                .id("markdown-placeholder")
 602                                .flex_1()
 603                                .p_3()
 604                                .italic()
 605                                .text_color(cx.theme().colors().text_muted)
 606                                .child("Click to edit markdown...")
 607                                .cursor_pointer()
 608                                .on_click(cx.listener(move |this, _event, window, cx| {
 609                                    this.editing = true;
 610                                    window.focus(&this.editor.focus_handle(cx), cx);
 611                                    cx.notify();
 612                                })),
 613                        ),
 614                )
 615                .children(self.cell_position_spacer(false, window, cx));
 616        };
 617
 618        let mut markdown_render_context =
 619            markdown_preview::markdown_renderer::RenderContext::new(None, window, cx);
 620
 621        v_flex()
 622            .size_full()
 623            .children(self.cell_position_spacer(true, window, cx))
 624            .child(
 625                h_flex()
 626                    .w_full()
 627                    .pr_6()
 628                    .rounded_xs()
 629                    .items_start()
 630                    .gap(DynamicSpacing::Base08.rems(cx))
 631                    .bg(self.selected_bg_color(window, cx))
 632                    .child(self.gutter(window, cx))
 633                    .child(
 634                        v_flex()
 635                            .image_cache(self.image_cache.clone())
 636                            .id("markdown-content")
 637                            .size_full()
 638                            .flex_1()
 639                            .p_3()
 640                            .font_ui(cx)
 641                            .text_size(TextSize::Default.rems(cx))
 642                            .cursor_pointer()
 643                            .on_click(cx.listener(|this, _event, window, cx| {
 644                                this.editing = true;
 645                                window.focus(&this.editor.focus_handle(cx), cx);
 646                                cx.notify();
 647                            }))
 648                            .children(parsed.children.iter().map(|child| {
 649                                div().relative().child(div().relative().child(
 650                                    render_markdown_block(child, &mut markdown_render_context),
 651                                ))
 652                            })),
 653                    ),
 654            )
 655            .children(self.cell_position_spacer(false, window, cx))
 656    }
 657}
 658
 659pub struct CodeCell {
 660    id: CellId,
 661    metadata: CellMetadata,
 662    execution_count: Option<i32>,
 663    source: String,
 664    editor: Entity<editor::Editor>,
 665    outputs: Vec<Output>,
 666    selected: bool,
 667    cell_position: Option<CellPosition>,
 668    language_task: Task<()>,
 669    execution_start_time: Option<Instant>,
 670    execution_duration: Option<Duration>,
 671    is_executing: bool,
 672}
 673
 674impl EventEmitter<CellEvent> for CodeCell {}
 675
 676impl CodeCell {
 677    pub fn new(
 678        id: CellId,
 679        metadata: CellMetadata,
 680        source: String,
 681        notebook_language: Shared<Task<Option<Arc<Language>>>>,
 682        window: &mut Window,
 683        cx: &mut Context<Self>,
 684    ) -> Self {
 685        let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
 686        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 687
 688        let editor_view = cx.new(|cx| {
 689            let mut editor = Editor::new(
 690                EditorMode::AutoHeight {
 691                    min_lines: 1,
 692                    max_lines: Some(1024),
 693                },
 694                multi_buffer,
 695                None,
 696                window,
 697                cx,
 698            );
 699
 700            let theme = ThemeSettings::get_global(cx);
 701            let refinement = TextStyleRefinement {
 702                font_family: Some(theme.buffer_font.family.clone()),
 703                font_size: Some(theme.buffer_font_size(cx).into()),
 704                color: Some(cx.theme().colors().editor_foreground),
 705                background_color: Some(gpui::transparent_black()),
 706                ..Default::default()
 707            };
 708
 709            editor.set_show_gutter(false, cx);
 710            editor.set_text_style_refinement(refinement);
 711            editor
 712        });
 713
 714        let language_task = cx.spawn_in(window, async move |_this, cx| {
 715            let language = notebook_language.await;
 716            buffer.update(cx, |buffer, cx| {
 717                buffer.set_language(language.clone(), cx);
 718            });
 719        });
 720
 721        Self {
 722            id,
 723            metadata,
 724            execution_count: None,
 725            source,
 726            editor: editor_view,
 727            outputs: Vec::new(),
 728            selected: false,
 729            cell_position: None,
 730            language_task,
 731            execution_start_time: None,
 732            execution_duration: None,
 733            is_executing: false,
 734        }
 735    }
 736
 737    /// Load a code cell from notebook file data, including existing outputs and execution count
 738    pub fn load(
 739        id: CellId,
 740        metadata: CellMetadata,
 741        execution_count: Option<i32>,
 742        source: String,
 743        outputs: Vec<Output>,
 744        notebook_language: Shared<Task<Option<Arc<Language>>>>,
 745        window: &mut Window,
 746        cx: &mut Context<Self>,
 747    ) -> Self {
 748        let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
 749        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 750
 751        let editor_view = cx.new(|cx| {
 752            let mut editor = Editor::new(
 753                EditorMode::AutoHeight {
 754                    min_lines: 1,
 755                    max_lines: Some(1024),
 756                },
 757                multi_buffer,
 758                None,
 759                window,
 760                cx,
 761            );
 762
 763            let theme = ThemeSettings::get_global(cx);
 764            let refinement = TextStyleRefinement {
 765                font_family: Some(theme.buffer_font.family.clone()),
 766                font_size: Some(theme.buffer_font_size(cx).into()),
 767                color: Some(cx.theme().colors().editor_foreground),
 768                background_color: Some(gpui::transparent_black()),
 769                ..Default::default()
 770            };
 771
 772            editor.set_text(source.clone(), window, cx);
 773            editor.set_show_gutter(false, cx);
 774            editor.set_text_style_refinement(refinement);
 775            editor
 776        });
 777
 778        let language_task = cx.spawn_in(window, async move |_this, cx| {
 779            let language = notebook_language.await;
 780            buffer.update(cx, |buffer, cx| {
 781                buffer.set_language(language.clone(), cx);
 782            });
 783        });
 784
 785        Self {
 786            id,
 787            metadata,
 788            execution_count,
 789            source,
 790            editor: editor_view,
 791            outputs,
 792            selected: false,
 793            cell_position: None,
 794            language_task,
 795            execution_start_time: None,
 796            execution_duration: None,
 797            is_executing: false,
 798        }
 799    }
 800
 801    pub fn editor(&self) -> &Entity<editor::Editor> {
 802        &self.editor
 803    }
 804
 805    pub fn current_source(&self, cx: &App) -> String {
 806        let editor = self.editor.read(cx);
 807        let buffer = editor.buffer().read(cx);
 808        buffer
 809            .as_singleton()
 810            .map(|b| b.read(cx).text())
 811            .unwrap_or_default()
 812    }
 813
 814    pub fn is_dirty(&self, cx: &App) -> bool {
 815        self.editor.read(cx).buffer().read(cx).is_dirty(cx)
 816    }
 817
 818    pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
 819        let source = self.current_source(cx);
 820        let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
 821
 822        let outputs = self.outputs_to_nbformat(cx);
 823
 824        nbformat::v4::Cell::Code {
 825            id: self.id.clone(),
 826            metadata: self.metadata.clone(),
 827            execution_count: self.execution_count,
 828            source: source_lines,
 829            outputs,
 830        }
 831    }
 832
 833    fn outputs_to_nbformat(&self, cx: &App) -> Vec<nbformat::v4::Output> {
 834        self.outputs
 835            .iter()
 836            .filter_map(|output| output.to_nbformat(cx))
 837            .collect()
 838    }
 839
 840    pub fn has_outputs(&self) -> bool {
 841        !self.outputs.is_empty()
 842    }
 843
 844    pub fn clear_outputs(&mut self) {
 845        self.outputs.clear();
 846        self.execution_duration = None;
 847    }
 848
 849    pub fn start_execution(&mut self) {
 850        self.execution_start_time = Some(Instant::now());
 851        self.execution_duration = None;
 852        self.is_executing = true;
 853    }
 854
 855    pub fn finish_execution(&mut self) {
 856        if let Some(start_time) = self.execution_start_time.take() {
 857            self.execution_duration = Some(start_time.elapsed());
 858        }
 859        self.is_executing = false;
 860    }
 861
 862    pub fn is_executing(&self) -> bool {
 863        self.is_executing
 864    }
 865
 866    pub fn execution_duration(&self) -> Option<Duration> {
 867        self.execution_duration
 868    }
 869
 870    fn format_duration(duration: Duration) -> String {
 871        let total_secs = duration.as_secs_f64();
 872        if total_secs < 1.0 {
 873            format!("{:.0}ms", duration.as_millis())
 874        } else if total_secs < 60.0 {
 875            format!("{:.1}s", total_secs)
 876        } else {
 877            let minutes = (total_secs / 60.0).floor() as u64;
 878            let secs = total_secs % 60.0;
 879            format!("{}m {:.1}s", minutes, secs)
 880        }
 881    }
 882
 883    pub fn handle_message(
 884        &mut self,
 885        message: &JupyterMessage,
 886        window: &mut Window,
 887        cx: &mut Context<Self>,
 888    ) {
 889        match &message.content {
 890            JupyterMessageContent::StreamContent(stream) => {
 891                self.outputs.push(Output::Stream {
 892                    content: cx.new(|cx| TerminalOutput::from(&stream.text, window, cx)),
 893                });
 894            }
 895            JupyterMessageContent::DisplayData(display_data) => {
 896                self.outputs
 897                    .push(Output::new(&display_data.data, None, window, cx));
 898            }
 899            JupyterMessageContent::ExecuteResult(execute_result) => {
 900                self.outputs
 901                    .push(Output::new(&execute_result.data, None, window, cx));
 902            }
 903            JupyterMessageContent::ExecuteInput(input) => {
 904                self.execution_count = serde_json::to_value(&input.execution_count)
 905                    .ok()
 906                    .and_then(|v| v.as_i64())
 907                    .map(|v| v as i32);
 908            }
 909            JupyterMessageContent::ExecuteReply(_) => {
 910                self.finish_execution();
 911            }
 912            JupyterMessageContent::ErrorOutput(error) => {
 913                self.outputs.push(Output::ErrorOutput(ErrorView {
 914                    ename: error.ename.clone(),
 915                    evalue: error.evalue.clone(),
 916                    traceback: cx
 917                        .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
 918                }));
 919            }
 920            _ => {}
 921        }
 922        cx.notify();
 923    }
 924
 925    fn output_control(&self) -> Option<CellControlType> {
 926        if self.has_outputs() {
 927            Some(CellControlType::ClearCell)
 928        } else {
 929            None
 930        }
 931    }
 932
 933    pub fn gutter_output(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 934        let is_selected = self.selected();
 935
 936        div()
 937            .relative()
 938            .h_full()
 939            .w(px(GUTTER_WIDTH))
 940            .child(
 941                div()
 942                    .w(px(GUTTER_WIDTH))
 943                    .flex()
 944                    .flex_none()
 945                    .justify_center()
 946                    .h_full()
 947                    .child(
 948                        div()
 949                            .flex_none()
 950                            .w(px(1.))
 951                            .h_full()
 952                            .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
 953                            .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
 954                    ),
 955            )
 956            .when(self.has_outputs(), |this| {
 957                this.child(
 958                    div()
 959                        .absolute()
 960                        .top(px(CODE_BLOCK_INSET - 2.0))
 961                        .left_0()
 962                        .flex()
 963                        .flex_none()
 964                        .w(px(GUTTER_WIDTH))
 965                        .h(px(GUTTER_WIDTH + 12.0))
 966                        .items_center()
 967                        .justify_center()
 968                        .bg(cx.theme().colors().tab_bar_background)
 969                        .child(IconButton::new("control", IconName::Ellipsis)),
 970                )
 971            })
 972    }
 973}
 974
 975impl RenderableCell for CodeCell {
 976    const CELL_TYPE: CellType = CellType::Code;
 977
 978    fn id(&self) -> &CellId {
 979        &self.id
 980    }
 981
 982    fn cell_type(&self) -> CellType {
 983        CellType::Code
 984    }
 985
 986    fn metadata(&self) -> &CellMetadata {
 987        &self.metadata
 988    }
 989
 990    fn source(&self) -> &String {
 991        &self.source
 992    }
 993
 994    fn control(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<CellControl> {
 995        let control_type = if self.has_outputs() {
 996            CellControlType::RerunCell
 997        } else {
 998            CellControlType::RunCell
 999        };
1000
1001        let cell_control = CellControl::new(
1002            if self.has_outputs() {
1003                "rerun-cell"
1004            } else {
1005                "run-cell"
1006            },
1007            control_type,
1008        )
1009        .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx)));
1010
1011        Some(cell_control)
1012    }
1013
1014    fn selected(&self) -> bool {
1015        self.selected
1016    }
1017
1018    fn set_selected(&mut self, selected: bool) -> &mut Self {
1019        self.selected = selected;
1020        self
1021    }
1022
1023    fn cell_position(&self) -> Option<&CellPosition> {
1024        self.cell_position.as_ref()
1025    }
1026
1027    fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
1028        self.cell_position = Some(cell_position);
1029        self
1030    }
1031
1032    fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1033        let is_selected = self.selected();
1034        let execution_count = self.execution_count;
1035
1036        div()
1037            .relative()
1038            .h_full()
1039            .w(px(GUTTER_WIDTH))
1040            .child(
1041                div()
1042                    .w(px(GUTTER_WIDTH))
1043                    .flex()
1044                    .flex_none()
1045                    .justify_center()
1046                    .h_full()
1047                    .child(
1048                        div()
1049                            .flex_none()
1050                            .w(px(1.))
1051                            .h_full()
1052                            .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
1053                            .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
1054                    ),
1055            )
1056            .when_some(self.control(window, cx), |this, control| {
1057                this.child(
1058                    div()
1059                        .absolute()
1060                        .top(px(CODE_BLOCK_INSET - 2.0))
1061                        .left_0()
1062                        .flex()
1063                        .flex_col()
1064                        .w(px(GUTTER_WIDTH))
1065                        .items_center()
1066                        .justify_center()
1067                        .bg(cx.theme().colors().tab_bar_background)
1068                        .child(control.button)
1069                        .when_some(execution_count, |this, count| {
1070                            this.child(
1071                                div()
1072                                    .mt_1()
1073                                    .text_xs()
1074                                    .text_color(cx.theme().colors().text_muted)
1075                                    .child(format!("{}", count)),
1076                            )
1077                        }),
1078                )
1079            })
1080    }
1081}
1082
1083impl RunnableCell for CodeCell {
1084    fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1085        println!("Running code cell: {}", self.id);
1086        cx.emit(CellEvent::Run(self.id.clone()));
1087    }
1088
1089    fn execution_count(&self) -> Option<i32> {
1090        self.execution_count
1091            .and_then(|count| if count > 0 { Some(count) } else { None })
1092    }
1093
1094    fn set_execution_count(&mut self, count: i32) -> &mut Self {
1095        self.execution_count = Some(count);
1096        self
1097    }
1098}
1099
1100impl Render for CodeCell {
1101    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1102        // get the language from the editor's buffer
1103        let language_name = self
1104            .editor
1105            .read(cx)
1106            .buffer()
1107            .read(cx)
1108            .as_singleton()
1109            .and_then(|buffer| buffer.read(cx).language())
1110            .map(|lang| lang.name().to_string());
1111
1112        v_flex()
1113            .size_full()
1114            // TODO: Move base cell render into trait impl so we don't have to repeat this
1115            .children(self.cell_position_spacer(true, window, cx))
1116            // Editor portion
1117            .child(
1118                h_flex()
1119                    .w_full()
1120                    .pr_6()
1121                    .rounded_xs()
1122                    .items_start()
1123                    .gap(DynamicSpacing::Base08.rems(cx))
1124                    .bg(self.selected_bg_color(window, cx))
1125                    .child(self.gutter(window, cx))
1126                    .child(
1127                        div().py_1p5().w_full().child(
1128                            div()
1129                                .relative()
1130                                .flex()
1131                                .size_full()
1132                                .flex_1()
1133                                .py_3()
1134                                .px_5()
1135                                .rounded_lg()
1136                                .border_1()
1137                                .border_color(cx.theme().colors().border)
1138                                .bg(cx.theme().colors().editor_background)
1139                                .child(div().w_full().child(self.editor.clone()))
1140                                // lang badge in top-right corner
1141                                .when_some(language_name, |this, name| {
1142                                    this.child(
1143                                        div()
1144                                            .absolute()
1145                                            .top_1()
1146                                            .right_2()
1147                                            .px_2()
1148                                            .py_0p5()
1149                                            .rounded_md()
1150                                            .bg(cx.theme().colors().element_background.opacity(0.7))
1151                                            .text_xs()
1152                                            .text_color(cx.theme().colors().text_muted)
1153                                            .child(name),
1154                                    )
1155                                }),
1156                        ),
1157                    ),
1158            )
1159            // Output portion
1160            .when(
1161                self.has_outputs() || self.execution_duration.is_some() || self.is_executing,
1162                |this| {
1163                    let execution_time_label = self.execution_duration.map(Self::format_duration);
1164                    let is_executing = self.is_executing;
1165                    this.child(
1166                        h_flex()
1167                            .w_full()
1168                            .pr_6()
1169                            .rounded_xs()
1170                            .items_start()
1171                            .gap(DynamicSpacing::Base08.rems(cx))
1172                            .bg(self.selected_bg_color(window, cx))
1173                            .child(self.gutter_output(window, cx))
1174                            .child(
1175                                div().py_1p5().w_full().child(
1176                                    v_flex()
1177                                        .size_full()
1178                                        .flex_1()
1179                                        .py_3()
1180                                        .px_5()
1181                                        .rounded_lg()
1182                                        .border_1()
1183                                        // execution status/time at the TOP
1184                                        .when(
1185                                            is_executing || execution_time_label.is_some(),
1186                                            |this| {
1187                                                let time_element = if is_executing {
1188                                                    h_flex()
1189                                                        .gap_1()
1190                                                        .items_center()
1191                                                        .child(
1192                                                            Icon::new(IconName::ArrowCircle)
1193                                                                .size(IconSize::XSmall)
1194                                                                .color(Color::Warning)
1195                                                                .with_rotate_animation(2)
1196                                                                .into_any_element(),
1197                                                        )
1198                                                        .child(
1199                                                            div()
1200                                                                .text_xs()
1201                                                                .text_color(
1202                                                                    cx.theme().colors().text_muted,
1203                                                                )
1204                                                                .child("Running..."),
1205                                                        )
1206                                                        .into_any_element()
1207                                                } else if let Some(duration_text) =
1208                                                    execution_time_label.clone()
1209                                                {
1210                                                    h_flex()
1211                                                        .gap_1()
1212                                                        .items_center()
1213                                                        .child(
1214                                                            Icon::new(IconName::Check)
1215                                                                .size(IconSize::XSmall)
1216                                                                .color(Color::Success),
1217                                                        )
1218                                                        .child(
1219                                                            div()
1220                                                                .text_xs()
1221                                                                .text_color(
1222                                                                    cx.theme().colors().text_muted,
1223                                                                )
1224                                                                .child(duration_text),
1225                                                        )
1226                                                        .into_any_element()
1227                                                } else {
1228                                                    div().into_any_element()
1229                                                };
1230                                                this.child(div().mb_2().child(time_element))
1231                                            },
1232                                        )
1233                                        // output at bottom
1234                                        .child(div().w_full().children(self.outputs.iter().map(
1235                                            |output| {
1236                                                let content = match output {
1237                                                    Output::Plain { content, .. } => {
1238                                                        Some(content.clone().into_any_element())
1239                                                    }
1240                                                    Output::Markdown { content, .. } => {
1241                                                        Some(content.clone().into_any_element())
1242                                                    }
1243                                                    Output::Stream { content, .. } => {
1244                                                        Some(content.clone().into_any_element())
1245                                                    }
1246                                                    Output::Image { content, .. } => {
1247                                                        Some(content.clone().into_any_element())
1248                                                    }
1249                                                    Output::Message(message) => Some(
1250                                                        div()
1251                                                            .child(message.clone())
1252                                                            .into_any_element(),
1253                                                    ),
1254                                                    Output::Table { content, .. } => {
1255                                                        Some(content.clone().into_any_element())
1256                                                    }
1257                                                    Output::ErrorOutput(error_view) => {
1258                                                        error_view.render(window, cx)
1259                                                    }
1260                                                    Output::ClearOutputWaitMarker => None,
1261                                                };
1262
1263                                                div().children(content)
1264                                            },
1265                                        ))),
1266                                ),
1267                            ),
1268                    )
1269                },
1270            )
1271            // TODO: Move base cell render into trait impl so we don't have to repeat this
1272            .children(self.cell_position_spacer(false, window, cx))
1273    }
1274}
1275
1276pub struct RawCell {
1277    id: CellId,
1278    metadata: CellMetadata,
1279    source: String,
1280    selected: bool,
1281    cell_position: Option<CellPosition>,
1282}
1283
1284impl RawCell {
1285    pub fn to_nbformat_cell(&self) -> nbformat::v4::Cell {
1286        let source_lines: Vec<String> = self.source.lines().map(|l| format!("{}\n", l)).collect();
1287
1288        nbformat::v4::Cell::Raw {
1289            id: self.id.clone(),
1290            metadata: self.metadata.clone(),
1291            source: source_lines,
1292        }
1293    }
1294}
1295
1296impl RenderableCell for RawCell {
1297    const CELL_TYPE: CellType = CellType::Raw;
1298
1299    fn id(&self) -> &CellId {
1300        &self.id
1301    }
1302
1303    fn cell_type(&self) -> CellType {
1304        CellType::Raw
1305    }
1306
1307    fn metadata(&self) -> &CellMetadata {
1308        &self.metadata
1309    }
1310
1311    fn source(&self) -> &String {
1312        &self.source
1313    }
1314
1315    fn selected(&self) -> bool {
1316        self.selected
1317    }
1318
1319    fn set_selected(&mut self, selected: bool) -> &mut Self {
1320        self.selected = selected;
1321        self
1322    }
1323
1324    fn cell_position(&self) -> Option<&CellPosition> {
1325        self.cell_position.as_ref()
1326    }
1327
1328    fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
1329        self.cell_position = Some(cell_position);
1330        self
1331    }
1332}
1333
1334impl Render for RawCell {
1335    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1336        v_flex()
1337            .size_full()
1338            // TODO: Move base cell render into trait impl so we don't have to repeat this
1339            .children(self.cell_position_spacer(true, window, cx))
1340            .child(
1341                h_flex()
1342                    .w_full()
1343                    .pr_2()
1344                    .rounded_xs()
1345                    .items_start()
1346                    .gap(DynamicSpacing::Base08.rems(cx))
1347                    .bg(self.selected_bg_color(window, cx))
1348                    .child(self.gutter(window, cx))
1349                    .child(
1350                        div()
1351                            .flex()
1352                            .size_full()
1353                            .flex_1()
1354                            .p_3()
1355                            .font_ui(cx)
1356                            .text_size(TextSize::Default.rems(cx))
1357                            .child(self.source.clone()),
1358                    ),
1359            )
1360            // TODO: Move base cell render into trait impl so we don't have to repeat this
1361            .children(self.cell_position_spacer(false, window, cx))
1362    }
1363}