cell.rs

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