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    /// Load a code cell from notebook file data, including existing outputs and execution count
 676    pub fn load(
 677        id: CellId,
 678        metadata: CellMetadata,
 679        execution_count: Option<i32>,
 680        source: String,
 681        outputs: Vec<Output>,
 682        notebook_language: Shared<Task<Option<Arc<Language>>>>,
 683        window: &mut Window,
 684        cx: &mut Context<Self>,
 685    ) -> Self {
 686        let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
 687        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 688
 689        let editor_view = cx.new(|cx| {
 690            let mut editor = Editor::new(
 691                EditorMode::AutoHeight {
 692                    min_lines: 1,
 693                    max_lines: Some(1024),
 694                },
 695                multi_buffer,
 696                None,
 697                window,
 698                cx,
 699            );
 700
 701            let theme = ThemeSettings::get_global(cx);
 702            let refinement = TextStyleRefinement {
 703                font_family: Some(theme.buffer_font.family.clone()),
 704                font_size: Some(theme.buffer_font_size(cx).into()),
 705                color: Some(cx.theme().colors().editor_foreground),
 706                background_color: Some(gpui::transparent_black()),
 707                ..Default::default()
 708            };
 709
 710            editor.set_text(source.clone(), window, cx);
 711            editor.set_show_gutter(false, cx);
 712            editor.set_text_style_refinement(refinement);
 713            editor
 714        });
 715
 716        let language_task = cx.spawn_in(window, async move |_this, cx| {
 717            let language = notebook_language.await;
 718            buffer.update(cx, |buffer, cx| {
 719                buffer.set_language(language.clone(), cx);
 720            });
 721        });
 722
 723        Self {
 724            id,
 725            metadata,
 726            execution_count,
 727            source,
 728            editor: editor_view,
 729            outputs,
 730            selected: false,
 731            cell_position: None,
 732            language_task,
 733            execution_start_time: None,
 734            execution_duration: None,
 735            is_executing: false,
 736        }
 737    }
 738
 739    pub fn editor(&self) -> &Entity<editor::Editor> {
 740        &self.editor
 741    }
 742
 743    pub fn current_source(&self, cx: &App) -> String {
 744        let editor = self.editor.read(cx);
 745        let buffer = editor.buffer().read(cx);
 746        buffer
 747            .as_singleton()
 748            .map(|b| b.read(cx).text())
 749            .unwrap_or_default()
 750    }
 751
 752    pub fn is_dirty(&self, cx: &App) -> bool {
 753        self.editor.read(cx).buffer().read(cx).is_dirty(cx)
 754    }
 755
 756    pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
 757        let source = self.current_source(cx);
 758        let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
 759
 760        let outputs = self.outputs_to_nbformat(cx);
 761
 762        nbformat::v4::Cell::Code {
 763            id: self.id.clone(),
 764            metadata: self.metadata.clone(),
 765            execution_count: self.execution_count,
 766            source: source_lines,
 767            outputs,
 768        }
 769    }
 770
 771    fn outputs_to_nbformat(&self, cx: &App) -> Vec<nbformat::v4::Output> {
 772        self.outputs
 773            .iter()
 774            .filter_map(|output| output.to_nbformat(cx))
 775            .collect()
 776    }
 777
 778    pub fn has_outputs(&self) -> bool {
 779        !self.outputs.is_empty()
 780    }
 781
 782    pub fn clear_outputs(&mut self) {
 783        self.outputs.clear();
 784        self.execution_duration = None;
 785    }
 786
 787    pub fn start_execution(&mut self) {
 788        self.execution_start_time = Some(Instant::now());
 789        self.execution_duration = None;
 790        self.is_executing = true;
 791    }
 792
 793    pub fn finish_execution(&mut self) {
 794        if let Some(start_time) = self.execution_start_time.take() {
 795            self.execution_duration = Some(start_time.elapsed());
 796        }
 797        self.is_executing = false;
 798    }
 799
 800    pub fn is_executing(&self) -> bool {
 801        self.is_executing
 802    }
 803
 804    pub fn execution_duration(&self) -> Option<Duration> {
 805        self.execution_duration
 806    }
 807
 808    fn format_duration(duration: Duration) -> String {
 809        let total_secs = duration.as_secs_f64();
 810        if total_secs < 1.0 {
 811            format!("{:.0}ms", duration.as_millis())
 812        } else if total_secs < 60.0 {
 813            format!("{:.1}s", total_secs)
 814        } else {
 815            let minutes = (total_secs / 60.0).floor() as u64;
 816            let secs = total_secs % 60.0;
 817            format!("{}m {:.1}s", minutes, secs)
 818        }
 819    }
 820
 821    pub fn handle_message(
 822        &mut self,
 823        message: &JupyterMessage,
 824        window: &mut Window,
 825        cx: &mut Context<Self>,
 826    ) {
 827        match &message.content {
 828            JupyterMessageContent::StreamContent(stream) => {
 829                self.outputs.push(Output::Stream {
 830                    content: cx.new(|cx| TerminalOutput::from(&stream.text, window, cx)),
 831                });
 832            }
 833            JupyterMessageContent::DisplayData(display_data) => {
 834                self.outputs
 835                    .push(Output::new(&display_data.data, None, window, cx));
 836            }
 837            JupyterMessageContent::ExecuteResult(execute_result) => {
 838                self.outputs
 839                    .push(Output::new(&execute_result.data, None, window, cx));
 840            }
 841            JupyterMessageContent::ExecuteInput(input) => {
 842                self.execution_count = serde_json::to_value(&input.execution_count)
 843                    .ok()
 844                    .and_then(|v| v.as_i64())
 845                    .map(|v| v as i32);
 846            }
 847            JupyterMessageContent::ExecuteReply(_) => {
 848                self.finish_execution();
 849            }
 850            JupyterMessageContent::ErrorOutput(error) => {
 851                self.outputs.push(Output::ErrorOutput(ErrorView {
 852                    ename: error.ename.clone(),
 853                    evalue: error.evalue.clone(),
 854                    traceback: cx
 855                        .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
 856                }));
 857            }
 858            _ => {}
 859        }
 860        cx.notify();
 861    }
 862
 863    fn output_control(&self) -> Option<CellControlType> {
 864        if self.has_outputs() {
 865            Some(CellControlType::ClearCell)
 866        } else {
 867            None
 868        }
 869    }
 870
 871    pub fn gutter_output(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 872        let is_selected = self.selected();
 873
 874        div()
 875            .relative()
 876            .h_full()
 877            .w(px(GUTTER_WIDTH))
 878            .child(
 879                div()
 880                    .w(px(GUTTER_WIDTH))
 881                    .flex()
 882                    .flex_none()
 883                    .justify_center()
 884                    .h_full()
 885                    .child(
 886                        div()
 887                            .flex_none()
 888                            .w(px(1.))
 889                            .h_full()
 890                            .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
 891                            .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
 892                    ),
 893            )
 894            .when(self.has_outputs(), |this| {
 895                this.child(
 896                    div()
 897                        .absolute()
 898                        .top(px(CODE_BLOCK_INSET - 2.0))
 899                        .left_0()
 900                        .flex()
 901                        .flex_none()
 902                        .w(px(GUTTER_WIDTH))
 903                        .h(px(GUTTER_WIDTH + 12.0))
 904                        .items_center()
 905                        .justify_center()
 906                        .bg(cx.theme().colors().tab_bar_background)
 907                        .child(IconButton::new("control", IconName::Ellipsis)),
 908                )
 909            })
 910    }
 911}
 912
 913impl RenderableCell for CodeCell {
 914    const CELL_TYPE: CellType = CellType::Code;
 915
 916    fn id(&self) -> &CellId {
 917        &self.id
 918    }
 919
 920    fn cell_type(&self) -> CellType {
 921        CellType::Code
 922    }
 923
 924    fn metadata(&self) -> &CellMetadata {
 925        &self.metadata
 926    }
 927
 928    fn source(&self) -> &String {
 929        &self.source
 930    }
 931
 932    fn control(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<CellControl> {
 933        let control_type = if self.has_outputs() {
 934            CellControlType::RerunCell
 935        } else {
 936            CellControlType::RunCell
 937        };
 938
 939        let cell_control = CellControl::new(
 940            if self.has_outputs() {
 941                "rerun-cell"
 942            } else {
 943                "run-cell"
 944            },
 945            control_type,
 946        )
 947        .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx)));
 948
 949        Some(cell_control)
 950    }
 951
 952    fn selected(&self) -> bool {
 953        self.selected
 954    }
 955
 956    fn set_selected(&mut self, selected: bool) -> &mut Self {
 957        self.selected = selected;
 958        self
 959    }
 960
 961    fn cell_position(&self) -> Option<&CellPosition> {
 962        self.cell_position.as_ref()
 963    }
 964
 965    fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
 966        self.cell_position = Some(cell_position);
 967        self
 968    }
 969
 970    fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 971        let is_selected = self.selected();
 972        let execution_count = self.execution_count;
 973
 974        div()
 975            .relative()
 976            .h_full()
 977            .w(px(GUTTER_WIDTH))
 978            .child(
 979                div()
 980                    .w(px(GUTTER_WIDTH))
 981                    .flex()
 982                    .flex_none()
 983                    .justify_center()
 984                    .h_full()
 985                    .child(
 986                        div()
 987                            .flex_none()
 988                            .w(px(1.))
 989                            .h_full()
 990                            .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
 991                            .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
 992                    ),
 993            )
 994            .when_some(self.control(window, cx), |this, control| {
 995                this.child(
 996                    div()
 997                        .absolute()
 998                        .top(px(CODE_BLOCK_INSET - 2.0))
 999                        .left_0()
1000                        .flex()
1001                        .flex_col()
1002                        .w(px(GUTTER_WIDTH))
1003                        .items_center()
1004                        .justify_center()
1005                        .bg(cx.theme().colors().tab_bar_background)
1006                        .child(control.button)
1007                        .when_some(execution_count, |this, count| {
1008                            this.child(
1009                                div()
1010                                    .mt_1()
1011                                    .text_xs()
1012                                    .text_color(cx.theme().colors().text_muted)
1013                                    .child(format!("{}", count)),
1014                            )
1015                        }),
1016                )
1017            })
1018    }
1019}
1020
1021impl RunnableCell for CodeCell {
1022    fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1023        println!("Running code cell: {}", self.id);
1024        cx.emit(CellEvent::Run(self.id.clone()));
1025    }
1026
1027    fn execution_count(&self) -> Option<i32> {
1028        self.execution_count
1029            .and_then(|count| if count > 0 { Some(count) } else { None })
1030    }
1031
1032    fn set_execution_count(&mut self, count: i32) -> &mut Self {
1033        self.execution_count = Some(count);
1034        self
1035    }
1036}
1037
1038impl Render for CodeCell {
1039    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1040        let output_max_height = ReplSettings::get_global(cx).output_max_height_lines;
1041        let output_max_height = if output_max_height > 0 {
1042            Some(window.line_height() * output_max_height as f32)
1043        } else {
1044            None
1045        };
1046        let output_max_width = plain::max_width_for_columns(
1047            ReplSettings::get_global(cx).output_max_width_columns,
1048            window,
1049            cx,
1050        );
1051        // get the language from the editor's buffer
1052        let language_name = self
1053            .editor
1054            .read(cx)
1055            .buffer()
1056            .read(cx)
1057            .as_singleton()
1058            .and_then(|buffer| buffer.read(cx).language())
1059            .map(|lang| lang.name().to_string());
1060
1061        v_flex()
1062            .size_full()
1063            // TODO: Move base cell render into trait impl so we don't have to repeat this
1064            .children(self.cell_position_spacer(true, window, cx))
1065            // Editor portion
1066            .child(
1067                h_flex()
1068                    .w_full()
1069                    .pr_6()
1070                    .rounded_xs()
1071                    .items_start()
1072                    .gap(DynamicSpacing::Base08.rems(cx))
1073                    .bg(self.selected_bg_color(window, cx))
1074                    .child(self.gutter(window, cx))
1075                    .child(
1076                        div().py_1p5().w_full().child(
1077                            div()
1078                                .relative()
1079                                .flex()
1080                                .size_full()
1081                                .flex_1()
1082                                .py_3()
1083                                .px_5()
1084                                .rounded_lg()
1085                                .border_1()
1086                                .border_color(cx.theme().colors().border)
1087                                .bg(cx.theme().colors().editor_background)
1088                                .child(div().w_full().child(self.editor.clone()))
1089                                // lang badge in top-right corner
1090                                .when_some(language_name, |this, name| {
1091                                    this.child(
1092                                        div()
1093                                            .absolute()
1094                                            .top_1()
1095                                            .right_2()
1096                                            .px_2()
1097                                            .py_0p5()
1098                                            .rounded_md()
1099                                            .bg(cx.theme().colors().element_background.opacity(0.7))
1100                                            .text_xs()
1101                                            .text_color(cx.theme().colors().text_muted)
1102                                            .child(name),
1103                                    )
1104                                }),
1105                        ),
1106                    ),
1107            )
1108            // Output portion
1109            .child(
1110                h_flex()
1111                    .w_full()
1112                    .pr_6()
1113                    .rounded_xs()
1114                    .items_start()
1115                    .gap(DynamicSpacing::Base08.rems(cx))
1116                    .bg(self.selected_bg_color(window, cx))
1117                    .child(self.gutter_output(window, cx))
1118                    .child(
1119                        div().py_1p5().w_full().child(
1120                            div()
1121                                .flex()
1122                                .size_full()
1123                                .flex_1()
1124                                .py_3()
1125                                .px_5()
1126                                .rounded_lg()
1127                                .border_1()
1128                                .child(
1129                                    div()
1130                                        .id((ElementId::from(self.id.to_string()), "output-scroll"))
1131                                        .w_full()
1132                                        .when_some(output_max_width, |div, max_w| {
1133                                            div.max_w(max_w).overflow_x_scroll()
1134                                        })
1135                                        .when_some(output_max_height, |div, max_h| {
1136                                            div.max_h(max_h).overflow_y_scroll()
1137                                        })
1138                                        .children(self.outputs.iter().map(|output| {
1139                                            let content = match output {
1140                                                Output::Plain { content, .. } => {
1141                                                    Some(content.clone().into_any_element())
1142                                                }
1143                                                Output::Markdown { content, .. } => {
1144                                                    Some(content.clone().into_any_element())
1145                                                }
1146                                                Output::Stream { content, .. } => {
1147                                                    Some(content.clone().into_any_element())
1148                                                }
1149                                                Output::Image { content, .. } => {
1150                                                    Some(content.clone().into_any_element())
1151                                                }
1152                                                Output::Message(message) => Some(
1153                                                    div().child(message.clone()).into_any_element(),
1154                                                ),
1155                                                Output::Table { content, .. } => {
1156                                                    Some(content.clone().into_any_element())
1157                                                }
1158                                                Output::Json { content, .. } => {
1159                                                    Some(content.clone().into_any_element())
1160                                                }
1161                                                Output::ErrorOutput(error_view) => {
1162                                                    error_view.render(window, cx)
1163                                                }
1164                                                Output::ClearOutputWaitMarker => None,
1165                                            };
1166
1167                                            div().children(content)
1168                                        })),
1169                                ),
1170                        ),
1171                    ),
1172            )
1173            .when(
1174                self.has_outputs() || self.execution_duration.is_some() || self.is_executing,
1175                |this| {
1176                    let execution_time_label = self.execution_duration.map(Self::format_duration);
1177                    let is_executing = self.is_executing;
1178                    this.child(
1179                        h_flex()
1180                            .w_full()
1181                            .pr_6()
1182                            .rounded_xs()
1183                            .items_start()
1184                            .gap(DynamicSpacing::Base08.rems(cx))
1185                            .bg(self.selected_bg_color(window, cx))
1186                            .child(self.gutter_output(window, cx))
1187                            .child(
1188                                div().py_1p5().w_full().child(
1189                                    v_flex()
1190                                        .size_full()
1191                                        .flex_1()
1192                                        .py_3()
1193                                        .px_5()
1194                                        .rounded_lg()
1195                                        .border_1()
1196                                        // execution status/time at the TOP
1197                                        .when(
1198                                            is_executing || execution_time_label.is_some(),
1199                                            |this| {
1200                                                let time_element = if is_executing {
1201                                                    h_flex()
1202                                                        .gap_1()
1203                                                        .items_center()
1204                                                        .child(
1205                                                            Icon::new(IconName::ArrowCircle)
1206                                                                .size(IconSize::XSmall)
1207                                                                .color(Color::Warning)
1208                                                                .with_rotate_animation(2)
1209                                                                .into_any_element(),
1210                                                        )
1211                                                        .child(
1212                                                            div()
1213                                                                .text_xs()
1214                                                                .text_color(
1215                                                                    cx.theme().colors().text_muted,
1216                                                                )
1217                                                                .child("Running..."),
1218                                                        )
1219                                                        .into_any_element()
1220                                                } else if let Some(duration_text) =
1221                                                    execution_time_label.clone()
1222                                                {
1223                                                    h_flex()
1224                                                        .gap_1()
1225                                                        .items_center()
1226                                                        .child(
1227                                                            Icon::new(IconName::Check)
1228                                                                .size(IconSize::XSmall)
1229                                                                .color(Color::Success),
1230                                                        )
1231                                                        .child(
1232                                                            div()
1233                                                                .text_xs()
1234                                                                .text_color(
1235                                                                    cx.theme().colors().text_muted,
1236                                                                )
1237                                                                .child(duration_text),
1238                                                        )
1239                                                        .into_any_element()
1240                                                } else {
1241                                                    div().into_any_element()
1242                                                };
1243                                                this.child(div().mb_2().child(time_element))
1244                                            },
1245                                        )
1246                                        // output at bottom
1247                                        .child(div().w_full().children(self.outputs.iter().map(
1248                                            |output| {
1249                                                let content = match output {
1250                                                    Output::Plain { content, .. } => {
1251                                                        Some(content.clone().into_any_element())
1252                                                    }
1253                                                    Output::Markdown { content, .. } => {
1254                                                        Some(content.clone().into_any_element())
1255                                                    }
1256                                                    Output::Stream { content, .. } => {
1257                                                        Some(content.clone().into_any_element())
1258                                                    }
1259                                                    Output::Image { content, .. } => {
1260                                                        Some(content.clone().into_any_element())
1261                                                    }
1262                                                    Output::Message(message) => Some(
1263                                                        div()
1264                                                            .child(message.clone())
1265                                                            .into_any_element(),
1266                                                    ),
1267                                                    Output::Table { content, .. } => {
1268                                                        Some(content.clone().into_any_element())
1269                                                    }
1270                                                    Output::Json { content, .. } => {
1271                                                        Some(content.clone().into_any_element())
1272                                                    }
1273                                                    Output::ErrorOutput(error_view) => {
1274                                                        error_view.render(window, cx)
1275                                                    }
1276                                                    Output::ClearOutputWaitMarker => None,
1277                                                };
1278
1279                                                div().children(content)
1280                                            },
1281                                        ))),
1282                                ),
1283                            ),
1284                    )
1285                },
1286            )
1287            // TODO: Move base cell render into trait impl so we don't have to repeat this
1288            .children(self.cell_position_spacer(false, window, cx))
1289    }
1290}
1291
1292pub struct RawCell {
1293    id: CellId,
1294    metadata: CellMetadata,
1295    source: String,
1296    selected: bool,
1297    cell_position: Option<CellPosition>,
1298}
1299
1300impl RawCell {
1301    pub fn to_nbformat_cell(&self) -> nbformat::v4::Cell {
1302        let source_lines: Vec<String> = self.source.lines().map(|l| format!("{}\n", l)).collect();
1303
1304        nbformat::v4::Cell::Raw {
1305            id: self.id.clone(),
1306            metadata: self.metadata.clone(),
1307            source: source_lines,
1308        }
1309    }
1310}
1311
1312impl RenderableCell for RawCell {
1313    const CELL_TYPE: CellType = CellType::Raw;
1314
1315    fn id(&self) -> &CellId {
1316        &self.id
1317    }
1318
1319    fn cell_type(&self) -> CellType {
1320        CellType::Raw
1321    }
1322
1323    fn metadata(&self) -> &CellMetadata {
1324        &self.metadata
1325    }
1326
1327    fn source(&self) -> &String {
1328        &self.source
1329    }
1330
1331    fn selected(&self) -> bool {
1332        self.selected
1333    }
1334
1335    fn set_selected(&mut self, selected: bool) -> &mut Self {
1336        self.selected = selected;
1337        self
1338    }
1339
1340    fn cell_position(&self) -> Option<&CellPosition> {
1341        self.cell_position.as_ref()
1342    }
1343
1344    fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
1345        self.cell_position = Some(cell_position);
1346        self
1347    }
1348}
1349
1350impl Render for RawCell {
1351    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1352        v_flex()
1353            .size_full()
1354            // TODO: Move base cell render into trait impl so we don't have to repeat this
1355            .children(self.cell_position_spacer(true, window, cx))
1356            .child(
1357                h_flex()
1358                    .w_full()
1359                    .pr_2()
1360                    .rounded_xs()
1361                    .items_start()
1362                    .gap(DynamicSpacing::Base08.rems(cx))
1363                    .bg(self.selected_bg_color(window, cx))
1364                    .child(self.gutter(window, cx))
1365                    .child(
1366                        div()
1367                            .flex()
1368                            .size_full()
1369                            .flex_1()
1370                            .p_3()
1371                            .font_ui(cx)
1372                            .text_size(TextSize::Default.rems(cx))
1373                            .child(self.source.clone()),
1374                    ),
1375            )
1376            // TODO: Move base cell render into trait impl so we don't have to repeat this
1377            .children(self.cell_position_spacer(false, window, cx))
1378    }
1379}