notebook_ui.rs

   1#![allow(unused, dead_code)]
   2use std::future::Future;
   3use std::{path::PathBuf, sync::Arc};
   4
   5use anyhow::{Context as _, Result};
   6use client::proto::ViewId;
   7use collections::HashMap;
   8use editor::DisplayPoint;
   9use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag};
  10use futures::FutureExt;
  11use futures::future::Shared;
  12use gpui::{
  13    AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, ListScrollEvent, ListState,
  14    Point, Task, actions, list, prelude::*,
  15};
  16use jupyter_protocol::JupyterKernelspec;
  17use language::{Language, LanguageRegistry};
  18use log;
  19use project::{Project, ProjectEntryId, ProjectPath};
  20use settings::Settings as _;
  21use ui::{CommonAnimationExt, Tooltip, prelude::*};
  22use workspace::item::{ItemEvent, SaveOptions, TabContentParams};
  23use workspace::searchable::SearchableItemHandle;
  24use workspace::{Item, ItemHandle, Pane, ProjectItem, ToolbarItemLocation};
  25
  26use super::{Cell, CellEvent, CellPosition, MarkdownCellEvent, RenderableCell};
  27
  28use nbformat::v4::CellId;
  29use nbformat::v4::Metadata as NotebookMetadata;
  30use serde_json;
  31use uuid::Uuid;
  32
  33use crate::components::{KernelPickerDelegate, KernelSelector};
  34use crate::kernels::{
  35    Kernel, KernelSession, KernelSpecification, KernelStatus, LocalKernelSpecification,
  36    NativeRunningKernel, RemoteRunningKernel, SshRunningKernel, WslRunningKernel,
  37};
  38use crate::repl_store::ReplStore;
  39
  40use picker::Picker;
  41use runtimelib::{ExecuteRequest, JupyterMessage, JupyterMessageContent};
  42use ui::PopoverMenuHandle;
  43use zed_actions::editor::{MoveDown, MoveUp};
  44use zed_actions::notebook::{NotebookMoveDown, NotebookMoveUp};
  45
  46actions!(
  47    notebook,
  48    [
  49        /// Opens a Jupyter notebook file.
  50        OpenNotebook,
  51        /// Runs all cells in the notebook.
  52        RunAll,
  53        /// Runs the current cell.
  54        Run,
  55        /// Clears all cell outputs.
  56        ClearOutputs,
  57        /// Moves the current cell up.
  58        MoveCellUp,
  59        /// Moves the current cell down.
  60        MoveCellDown,
  61        /// Adds a new markdown cell.
  62        AddMarkdownBlock,
  63        /// Adds a new code cell.
  64        AddCodeBlock,
  65        /// Restarts the kernel.
  66        RestartKernel,
  67        /// Interrupts the current execution.
  68        InterruptKernel,
  69    ]
  70);
  71
  72pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0;
  73pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0;
  74pub(crate) const MEDIUM_SPACING_SIZE: f32 = 12.0;
  75pub(crate) const LARGE_SPACING_SIZE: f32 = 16.0;
  76pub(crate) const GUTTER_WIDTH: f32 = 19.0;
  77pub(crate) const CODE_BLOCK_INSET: f32 = MEDIUM_SPACING_SIZE;
  78pub(crate) const CONTROL_SIZE: f32 = 20.0;
  79
  80pub fn init(cx: &mut App) {
  81    if cx.has_flag::<NotebookFeatureFlag>() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() {
  82        workspace::register_project_item::<NotebookEditor>(cx);
  83    }
  84
  85    cx.observe_flag::<NotebookFeatureFlag, _>({
  86        move |is_enabled, cx| {
  87            if is_enabled {
  88                workspace::register_project_item::<NotebookEditor>(cx);
  89            } else {
  90                // todo: there is no way to unregister a project item, so if the feature flag
  91                // gets turned off they need to restart Zed.
  92            }
  93        }
  94    })
  95    .detach();
  96}
  97
  98pub struct NotebookEditor {
  99    languages: Arc<LanguageRegistry>,
 100    project: Entity<Project>,
 101    worktree_id: project::WorktreeId,
 102
 103    focus_handle: FocusHandle,
 104    notebook_item: Entity<NotebookItem>,
 105    notebook_language: Shared<Task<Option<Arc<Language>>>>,
 106
 107    remote_id: Option<ViewId>,
 108    cell_list: ListState,
 109
 110    selected_cell_index: usize,
 111    cell_order: Vec<CellId>,
 112    original_cell_order: Vec<CellId>,
 113    cell_map: HashMap<CellId, Cell>,
 114    kernel: Kernel,
 115    kernel_specification: Option<KernelSpecification>,
 116    execution_requests: HashMap<String, CellId>,
 117    kernel_picker_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>,
 118}
 119
 120impl NotebookEditor {
 121    pub fn new(
 122        project: Entity<Project>,
 123        notebook_item: Entity<NotebookItem>,
 124        window: &mut Window,
 125        cx: &mut Context<Self>,
 126    ) -> Self {
 127        let focus_handle = cx.focus_handle();
 128
 129        let languages = project.read(cx).languages().clone();
 130        let language_name = notebook_item.read(cx).language_name();
 131        let worktree_id = notebook_item.read(cx).project_path.worktree_id;
 132
 133        let notebook_language = notebook_item.read(cx).notebook_language();
 134        let notebook_language = cx
 135            .spawn_in(window, async move |_, _| notebook_language.await)
 136            .shared();
 137
 138        let mut cell_order = vec![]; // Vec<CellId>
 139        let mut cell_map = HashMap::default(); // HashMap<CellId, Cell>
 140
 141        let cell_count = notebook_item.read(cx).notebook.cells.len();
 142        for index in 0..cell_count {
 143            let cell = notebook_item.read(cx).notebook.cells[index].clone();
 144            let cell_id = cell.id();
 145            cell_order.push(cell_id.clone());
 146            let cell_entity = Cell::load(&cell, &languages, notebook_language.clone(), window, cx);
 147
 148            match &cell_entity {
 149                Cell::Code(code_cell) => {
 150                    let cell_id_for_focus = cell_id.clone();
 151                    cx.subscribe(code_cell, move |this, cell, event, cx| match event {
 152                        CellEvent::Run(cell_id) => this.execute_cell(cell_id.clone(), cx),
 153                        CellEvent::FocusedIn(_) => {
 154                            if let Some(index) = this
 155                                .cell_order
 156                                .iter()
 157                                .position(|id| id == &cell_id_for_focus)
 158                            {
 159                                this.selected_cell_index = index;
 160                                cx.notify();
 161                            }
 162                        }
 163                    })
 164                    .detach();
 165
 166                    let cell_id_for_editor = cell_id.clone();
 167                    let editor = code_cell.read(cx).editor().clone();
 168                    cx.subscribe(&editor, move |this, _editor, event, cx| {
 169                        if let editor::EditorEvent::Focused = event {
 170                            if let Some(index) = this
 171                                .cell_order
 172                                .iter()
 173                                .position(|id| id == &cell_id_for_editor)
 174                            {
 175                                this.selected_cell_index = index;
 176                                cx.notify();
 177                            }
 178                        }
 179                    })
 180                    .detach();
 181                }
 182                Cell::Markdown(markdown_cell) => {
 183                    let cell_id_for_focus = cell_id.clone();
 184                    cx.subscribe(
 185                        markdown_cell,
 186                        move |_this, cell, event: &MarkdownCellEvent, cx| {
 187                            match event {
 188                                MarkdownCellEvent::FinishedEditing => {
 189                                    cell.update(cx, |cell, cx| {
 190                                        cell.reparse_markdown(cx);
 191                                    });
 192                                }
 193                                MarkdownCellEvent::Run(_cell_id) => {
 194                                    // run is handled separately by move_to_next_cell
 195                                    // Just reparse here
 196                                    cell.update(cx, |cell, cx| {
 197                                        cell.reparse_markdown(cx);
 198                                    });
 199                                }
 200                            }
 201                        },
 202                    )
 203                    .detach();
 204
 205                    let cell_id_for_editor = cell_id.clone();
 206                    let editor = markdown_cell.read(cx).editor().clone();
 207                    cx.subscribe(&editor, move |this, _editor, event, cx| {
 208                        if let editor::EditorEvent::Focused = event {
 209                            if let Some(index) = this
 210                                .cell_order
 211                                .iter()
 212                                .position(|id| id == &cell_id_for_editor)
 213                            {
 214                                this.selected_cell_index = index;
 215                                cx.notify();
 216                            }
 217                        }
 218                    })
 219                    .detach();
 220                }
 221                Cell::Raw(_) => {}
 222            }
 223
 224            cell_map.insert(cell_id.clone(), cell_entity);
 225        }
 226
 227        let notebook_handle = cx.entity().downgrade();
 228        let cell_count = cell_order.len();
 229
 230        let this = cx.entity();
 231        let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.));
 232
 233        let mut editor = Self {
 234            project,
 235            languages: languages.clone(),
 236            worktree_id,
 237            focus_handle,
 238            notebook_item: notebook_item.clone(),
 239            notebook_language,
 240            remote_id: None,
 241            cell_list,
 242            selected_cell_index: 0,
 243            cell_order: cell_order.clone(),
 244            original_cell_order: cell_order.clone(),
 245            cell_map: cell_map.clone(),
 246            kernel: Kernel::Shutdown, // TODO: use recommended kernel after the implementation is done in repl
 247            kernel_specification: None,
 248            execution_requests: HashMap::default(),
 249            kernel_picker_handle: PopoverMenuHandle::default(),
 250        };
 251        editor.launch_kernel(window, cx);
 252        editor.refresh_language(cx);
 253
 254        cx.subscribe(&notebook_item, |this, _item, _event, cx| {
 255            this.refresh_language(cx);
 256        })
 257        .detach();
 258
 259        editor
 260    }
 261
 262    fn refresh_language(&mut self, cx: &mut Context<Self>) {
 263        let notebook_language = self.notebook_item.read(cx).notebook_language();
 264        let task = cx.spawn(async move |this, cx| {
 265            let language = notebook_language.await;
 266            if let Some(this) = this.upgrade() {
 267                this.update(cx, |this, cx| {
 268                    for cell in this.cell_map.values() {
 269                        if let Cell::Code(code_cell) = cell {
 270                            code_cell.update(cx, |cell, cx| {
 271                                cell.set_language(language.clone(), cx);
 272                            });
 273                        }
 274                    }
 275                });
 276            }
 277            language
 278        });
 279        self.notebook_language = task.shared();
 280    }
 281
 282    fn has_structural_changes(&self) -> bool {
 283        self.cell_order != self.original_cell_order
 284    }
 285
 286    fn has_content_changes(&self, cx: &App) -> bool {
 287        self.cell_map.values().any(|cell| cell.is_dirty(cx))
 288    }
 289
 290    pub fn to_notebook(&self, cx: &App) -> nbformat::v4::Notebook {
 291        let cells: Vec<nbformat::v4::Cell> = self
 292            .cell_order
 293            .iter()
 294            .filter_map(|cell_id| {
 295                self.cell_map
 296                    .get(cell_id)
 297                    .map(|cell| cell.to_nbformat_cell(cx))
 298            })
 299            .collect();
 300
 301        let metadata = self.notebook_item.read(cx).notebook.metadata.clone();
 302
 303        nbformat::v4::Notebook {
 304            metadata,
 305            nbformat: 4,
 306            nbformat_minor: 5,
 307            cells,
 308        }
 309    }
 310
 311    pub fn mark_as_saved(&mut self, cx: &mut Context<Self>) {
 312        self.original_cell_order = self.cell_order.clone();
 313
 314        for cell in self.cell_map.values() {
 315            match cell {
 316                Cell::Code(code_cell) => {
 317                    code_cell.update(cx, |code_cell, cx| {
 318                        let editor = code_cell.editor();
 319                        editor.update(cx, |editor, cx| {
 320                            editor.buffer().update(cx, |buffer, cx| {
 321                                if let Some(buf) = buffer.as_singleton() {
 322                                    buf.update(cx, |b, cx| {
 323                                        let version = b.version();
 324                                        b.did_save(version, None, cx);
 325                                    });
 326                                }
 327                            });
 328                        });
 329                    });
 330                }
 331                Cell::Markdown(markdown_cell) => {
 332                    markdown_cell.update(cx, |markdown_cell, cx| {
 333                        let editor = markdown_cell.editor();
 334                        editor.update(cx, |editor, cx| {
 335                            editor.buffer().update(cx, |buffer, cx| {
 336                                if let Some(buf) = buffer.as_singleton() {
 337                                    buf.update(cx, |b, cx| {
 338                                        let version = b.version();
 339                                        b.did_save(version, None, cx);
 340                                    });
 341                                }
 342                            });
 343                        });
 344                    });
 345                }
 346                Cell::Raw(_) => {}
 347            }
 348        }
 349        cx.notify();
 350    }
 351
 352    fn launch_kernel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 353        // use default Python kernel if no specification is set
 354        let spec = self.kernel_specification.clone().unwrap_or_else(|| {
 355            KernelSpecification::Jupyter(LocalKernelSpecification {
 356                name: "python3".to_string(),
 357                path: PathBuf::from("python3"),
 358                kernelspec: JupyterKernelspec {
 359                    argv: vec![
 360                        "python3".to_string(),
 361                        "-m".to_string(),
 362                        "ipykernel_launcher".to_string(),
 363                        "-f".to_string(),
 364                        "{connection_file}".to_string(),
 365                    ],
 366                    display_name: "Python 3".to_string(),
 367                    language: "python".to_string(),
 368                    interrupt_mode: None,
 369                    metadata: None,
 370                    env: None,
 371                },
 372            })
 373        });
 374
 375        self.launch_kernel_with_spec(spec, window, cx);
 376    }
 377
 378    fn launch_kernel_with_spec(
 379        &mut self,
 380        spec: KernelSpecification,
 381        window: &mut Window,
 382        cx: &mut Context<Self>,
 383    ) {
 384        let entity_id = cx.entity_id();
 385        let working_directory = self
 386            .project
 387            .read(cx)
 388            .worktrees(cx)
 389            .next()
 390            .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
 391            .unwrap_or_else(std::env::temp_dir);
 392        let fs = self.project.read(cx).fs().clone();
 393        let view = cx.entity();
 394
 395        self.kernel_specification = Some(spec.clone());
 396
 397        self.notebook_item.update(cx, |item, cx| {
 398            let kernel_name = spec.name().to_string();
 399            let language = spec.language().to_string();
 400
 401            let display_name = match &spec {
 402                KernelSpecification::Jupyter(s) => s.kernelspec.display_name.clone(),
 403                KernelSpecification::PythonEnv(s) => s.kernelspec.display_name.clone(),
 404                KernelSpecification::JupyterServer(s) => s.kernelspec.display_name.clone(),
 405                KernelSpecification::SshRemote(s) => s.kernelspec.display_name.clone(),
 406                KernelSpecification::WslRemote(s) => s.kernelspec.display_name.clone(),
 407            };
 408
 409            let kernelspec_json = serde_json::json!({
 410                "display_name": display_name,
 411                "name": kernel_name,
 412                "language": language
 413            });
 414
 415            if let Ok(k) = serde_json::from_value(kernelspec_json) {
 416                item.notebook.metadata.kernelspec = Some(k);
 417                cx.emit(());
 418            }
 419        });
 420
 421        let kernel_task = match spec {
 422            KernelSpecification::Jupyter(local_spec) => NativeRunningKernel::new(
 423                local_spec,
 424                entity_id,
 425                working_directory,
 426                fs,
 427                view,
 428                window,
 429                cx,
 430            ),
 431            KernelSpecification::PythonEnv(env_spec) => NativeRunningKernel::new(
 432                env_spec.as_local_spec(),
 433                entity_id,
 434                working_directory,
 435                fs,
 436                view,
 437                window,
 438                cx,
 439            ),
 440            KernelSpecification::JupyterServer(remote_spec) => {
 441                RemoteRunningKernel::new(remote_spec, working_directory, view, window, cx)
 442            }
 443
 444            KernelSpecification::SshRemote(spec) => {
 445                let project = self.project.clone();
 446                SshRunningKernel::new(spec, working_directory, project, view, window, cx)
 447            }
 448            KernelSpecification::WslRemote(spec) => {
 449                WslRunningKernel::new(spec, entity_id, working_directory, fs, view, window, cx)
 450            }
 451        };
 452
 453        let pending_kernel = cx
 454            .spawn(async move |this, cx| {
 455                let kernel = kernel_task.await;
 456
 457                match kernel {
 458                    Ok(kernel) => {
 459                        this.update(cx, |editor, cx| {
 460                            editor.kernel = Kernel::RunningKernel(kernel);
 461                            cx.notify();
 462                        })
 463                        .ok();
 464                    }
 465                    Err(err) => {
 466                        log::error!("Kernel failed to start: {:?}", err);
 467                        this.update(cx, |editor, cx| {
 468                            editor.kernel = Kernel::ErroredLaunch(err.to_string());
 469                            cx.notify();
 470                        })
 471                        .ok();
 472                    }
 473                }
 474            })
 475            .shared();
 476
 477        self.kernel = Kernel::StartingKernel(pending_kernel);
 478        cx.notify();
 479    }
 480
 481    // Note: Python environments are only detected as kernels if ipykernel is installed.
 482    // Users need to run `pip install ipykernel` (or `uv pip install ipykernel`) in their
 483    // virtual environment for it to appear in the kernel selector.
 484    // This happens because we have an ipykernel check inside the function python_env_kernel_specification in mod.rs L:121
 485
 486    fn change_kernel(
 487        &mut self,
 488        spec: KernelSpecification,
 489        window: &mut Window,
 490        cx: &mut Context<Self>,
 491    ) {
 492        if let Kernel::RunningKernel(kernel) = &mut self.kernel {
 493            kernel.force_shutdown(window, cx).detach();
 494        }
 495
 496        self.execution_requests.clear();
 497
 498        self.launch_kernel_with_spec(spec, window, cx);
 499    }
 500
 501    fn restart_kernel(&mut self, _: &RestartKernel, window: &mut Window, cx: &mut Context<Self>) {
 502        if let Some(spec) = self.kernel_specification.clone() {
 503            if let Kernel::RunningKernel(kernel) = &mut self.kernel {
 504                kernel.force_shutdown(window, cx).detach();
 505            }
 506
 507            self.kernel = Kernel::Restarting;
 508            cx.notify();
 509
 510            self.launch_kernel_with_spec(spec, window, cx);
 511        }
 512    }
 513
 514    fn interrupt_kernel(
 515        &mut self,
 516        _: &InterruptKernel,
 517        _window: &mut Window,
 518        cx: &mut Context<Self>,
 519    ) {
 520        if let Kernel::RunningKernel(kernel) = &self.kernel {
 521            let interrupt_request = runtimelib::InterruptRequest {};
 522            let message: JupyterMessage = interrupt_request.into();
 523            kernel.request_tx().try_send(message).ok();
 524            cx.notify();
 525        }
 526    }
 527
 528    fn execute_cell(&mut self, cell_id: CellId, cx: &mut Context<Self>) {
 529        let code = if let Some(Cell::Code(cell)) = self.cell_map.get(&cell_id) {
 530            let editor = cell.read(cx).editor().clone();
 531            let buffer = editor.read(cx).buffer().read(cx);
 532            buffer
 533                .as_singleton()
 534                .map(|b| b.read(cx).text())
 535                .unwrap_or_default()
 536        } else {
 537            return;
 538        };
 539
 540        if let Some(Cell::Code(cell)) = self.cell_map.get(&cell_id) {
 541            cell.update(cx, |cell, cx| {
 542                if cell.has_outputs() {
 543                    cell.clear_outputs();
 544                }
 545                cell.start_execution();
 546                cx.notify();
 547            });
 548        }
 549
 550        let request = ExecuteRequest {
 551            code,
 552            ..Default::default()
 553        };
 554        let message: JupyterMessage = request.into();
 555        let msg_id = message.header.msg_id.clone();
 556
 557        self.execution_requests.insert(msg_id, cell_id.clone());
 558
 559        if let Kernel::RunningKernel(kernel) = &mut self.kernel {
 560            kernel.request_tx().try_send(message).ok();
 561        }
 562    }
 563
 564    fn has_outputs(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 565        self.cell_map.values().any(|cell| {
 566            if let Cell::Code(code_cell) = cell {
 567                code_cell.read(cx).has_outputs()
 568            } else {
 569                false
 570            }
 571        })
 572    }
 573
 574    fn clear_outputs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 575        for cell in self.cell_map.values() {
 576            if let Cell::Code(code_cell) = cell {
 577                code_cell.update(cx, |cell, cx| {
 578                    cell.clear_outputs();
 579                    cx.notify();
 580                });
 581            }
 582        }
 583        cx.notify();
 584    }
 585
 586    fn run_cells(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 587        for cell_id in self.cell_order.clone() {
 588            self.execute_cell(cell_id, cx);
 589        }
 590    }
 591
 592    fn run_current_cell(&mut self, _: &Run, window: &mut Window, cx: &mut Context<Self>) {
 593        if let Some(cell_id) = self.cell_order.get(self.selected_cell_index).cloned() {
 594            if let Some(cell) = self.cell_map.get(&cell_id) {
 595                match cell {
 596                    Cell::Code(_) => {
 597                        self.execute_cell(cell_id, cx);
 598                    }
 599                    Cell::Markdown(markdown_cell) => {
 600                        // for markdown, finish editing and move to next cell
 601                        let is_editing = markdown_cell.read(cx).is_editing();
 602                        if is_editing {
 603                            markdown_cell.update(cx, |cell, cx| {
 604                                cell.run(cx);
 605                            });
 606                            // move to the next cell
 607                            // Discussion can be done on this default implementation
 608                            self.move_to_next_cell(window, cx);
 609                        }
 610                    }
 611                    Cell::Raw(_) => {}
 612                }
 613            }
 614        }
 615    }
 616
 617    // Discussion can be done on this default implementation
 618    /// Moves focus to the next cell, or creates a new code cell if at the end
 619    fn move_to_next_cell(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 620        if !self.cell_order.is_empty() && self.selected_cell_index < self.cell_order.len() - 1 {
 621            self.selected_cell_index += 1;
 622            // focus the new cell's editor
 623            if let Some(cell_id) = self.cell_order.get(self.selected_cell_index) {
 624                if let Some(cell) = self.cell_map.get(cell_id) {
 625                    match cell {
 626                        Cell::Code(code_cell) => {
 627                            let editor = code_cell.read(cx).editor();
 628                            window.focus(&editor.focus_handle(cx), cx);
 629                        }
 630                        Cell::Markdown(markdown_cell) => {
 631                            // Don't auto-enter edit mode for next markdown cell
 632                            // Just select it
 633                        }
 634                        Cell::Raw(_) => {}
 635                    }
 636                }
 637            }
 638            cx.notify();
 639        } else {
 640            // in the end, could optionally create a new cell
 641            // For now, just stay on the current cell
 642        }
 643    }
 644
 645    fn open_notebook(&mut self, _: &OpenNotebook, _window: &mut Window, _cx: &mut Context<Self>) {
 646        println!("Open notebook triggered");
 647    }
 648
 649    fn move_cell_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 650        println!("Move cell up triggered");
 651        if self.selected_cell_index > 0 {
 652            self.cell_order
 653                .swap(self.selected_cell_index, self.selected_cell_index - 1);
 654            self.selected_cell_index -= 1;
 655            cx.notify();
 656        }
 657    }
 658
 659    fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 660        println!("Move cell down triggered");
 661        if !self.cell_order.is_empty() && self.selected_cell_index < self.cell_order.len() - 1 {
 662            self.cell_order
 663                .swap(self.selected_cell_index, self.selected_cell_index + 1);
 664            self.selected_cell_index += 1;
 665            cx.notify();
 666        }
 667    }
 668
 669    fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 670        let new_cell_id: CellId = Uuid::new_v4().into();
 671        let languages = self.languages.clone();
 672        let metadata: nbformat::v4::CellMetadata =
 673            serde_json::from_str("{}").expect("empty object should parse");
 674
 675        let markdown_cell = cx.new(|cx| {
 676            super::MarkdownCell::new(
 677                new_cell_id.clone(),
 678                metadata,
 679                String::new(),
 680                languages,
 681                window,
 682                cx,
 683            )
 684        });
 685
 686        let insert_index = if self.cell_order.is_empty() {
 687            0
 688        } else {
 689            self.selected_cell_index + 1
 690        };
 691        self.cell_order.insert(insert_index, new_cell_id.clone());
 692        self.cell_map
 693            .insert(new_cell_id.clone(), Cell::Markdown(markdown_cell.clone()));
 694        self.selected_cell_index = insert_index;
 695
 696        cx.subscribe(
 697            &markdown_cell,
 698            move |_this, cell, event: &MarkdownCellEvent, cx| match event {
 699                MarkdownCellEvent::FinishedEditing | MarkdownCellEvent::Run(_) => {
 700                    cell.update(cx, |cell, cx| {
 701                        cell.reparse_markdown(cx);
 702                    });
 703                }
 704            },
 705        )
 706        .detach();
 707
 708        let cell_id_for_editor = new_cell_id.clone();
 709        let editor = markdown_cell.read(cx).editor().clone();
 710        cx.subscribe(&editor, move |this, _editor, event, cx| {
 711            if let editor::EditorEvent::Focused = event {
 712                if let Some(index) = this
 713                    .cell_order
 714                    .iter()
 715                    .position(|id| id == &cell_id_for_editor)
 716                {
 717                    this.selected_cell_index = index;
 718                    cx.notify();
 719                }
 720            }
 721        })
 722        .detach();
 723
 724        self.cell_list.reset(self.cell_order.len());
 725        cx.notify();
 726    }
 727
 728    fn add_code_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 729        let new_cell_id: CellId = Uuid::new_v4().into();
 730        let notebook_language = self.notebook_language.clone();
 731        let metadata: nbformat::v4::CellMetadata =
 732            serde_json::from_str("{}").expect("empty object should parse");
 733
 734        let code_cell = cx.new(|cx| {
 735            super::CodeCell::new(
 736                new_cell_id.clone(),
 737                metadata,
 738                String::new(),
 739                notebook_language,
 740                window,
 741                cx,
 742            )
 743        });
 744
 745        let insert_index = if self.cell_order.is_empty() {
 746            0
 747        } else {
 748            self.selected_cell_index + 1
 749        };
 750        self.cell_order.insert(insert_index, new_cell_id.clone());
 751        self.cell_map
 752            .insert(new_cell_id.clone(), Cell::Code(code_cell.clone()));
 753        self.selected_cell_index = insert_index;
 754
 755        let cell_id_for_run = new_cell_id.clone();
 756        cx.subscribe(&code_cell, move |this, _cell, event, cx| match event {
 757            CellEvent::Run(cell_id) => this.execute_cell(cell_id.clone(), cx),
 758            CellEvent::FocusedIn(_) => {
 759                if let Some(index) = this.cell_order.iter().position(|id| id == &cell_id_for_run) {
 760                    this.selected_cell_index = index;
 761                    cx.notify();
 762                }
 763            }
 764        })
 765        .detach();
 766
 767        let cell_id_for_editor = new_cell_id.clone();
 768        let editor = code_cell.read(cx).editor().clone();
 769        cx.subscribe(&editor, move |this, _editor, event, cx| {
 770            if let editor::EditorEvent::Focused = event {
 771                if let Some(index) = this
 772                    .cell_order
 773                    .iter()
 774                    .position(|id| id == &cell_id_for_editor)
 775                {
 776                    this.selected_cell_index = index;
 777                    cx.notify();
 778                }
 779            }
 780        })
 781        .detach();
 782
 783        self.cell_list.reset(self.cell_order.len());
 784        cx.notify();
 785    }
 786
 787    fn cell_count(&self) -> usize {
 788        self.cell_map.len()
 789    }
 790
 791    fn selected_index(&self) -> usize {
 792        self.selected_cell_index
 793    }
 794
 795    pub fn set_selected_index(
 796        &mut self,
 797        index: usize,
 798        jump_to_index: bool,
 799        window: &mut Window,
 800        cx: &mut Context<Self>,
 801    ) {
 802        // let previous_index = self.selected_cell_index;
 803        self.selected_cell_index = index;
 804        let current_index = self.selected_cell_index;
 805
 806        // in the future we may have some `on_cell_change` event that we want to fire here
 807
 808        if jump_to_index {
 809            self.jump_to_cell(current_index, window, cx);
 810        }
 811    }
 812
 813    pub fn select_next(
 814        &mut self,
 815        _: &menu::SelectNext,
 816        window: &mut Window,
 817        cx: &mut Context<Self>,
 818    ) {
 819        let count = self.cell_count();
 820        if count > 0 {
 821            let index = self.selected_index();
 822            let ix = if index == count - 1 {
 823                count - 1
 824            } else {
 825                index + 1
 826            };
 827            self.set_selected_index(ix, true, window, cx);
 828            cx.notify();
 829        }
 830    }
 831
 832    pub fn select_previous(
 833        &mut self,
 834        _: &menu::SelectPrevious,
 835        window: &mut Window,
 836        cx: &mut Context<Self>,
 837    ) {
 838        let count = self.cell_count();
 839        if count > 0 {
 840            let index = self.selected_index();
 841            let ix = if index == 0 { 0 } else { index - 1 };
 842            self.set_selected_index(ix, true, window, cx);
 843            cx.notify();
 844        }
 845    }
 846
 847    pub fn select_first(
 848        &mut self,
 849        _: &menu::SelectFirst,
 850        window: &mut Window,
 851        cx: &mut Context<Self>,
 852    ) {
 853        let count = self.cell_count();
 854        if count > 0 {
 855            self.set_selected_index(0, true, window, cx);
 856            cx.notify();
 857        }
 858    }
 859
 860    pub fn select_last(
 861        &mut self,
 862        _: &menu::SelectLast,
 863        window: &mut Window,
 864        cx: &mut Context<Self>,
 865    ) {
 866        let count = self.cell_count();
 867        if count > 0 {
 868            self.set_selected_index(count - 1, true, window, cx);
 869            cx.notify();
 870        }
 871    }
 872
 873    fn jump_to_cell(&mut self, index: usize, _window: &mut Window, _cx: &mut Context<Self>) {
 874        self.cell_list.scroll_to_reveal_item(index);
 875    }
 876
 877    fn button_group(window: &mut Window, cx: &mut Context<Self>) -> Div {
 878        v_flex()
 879            .gap(DynamicSpacing::Base04.rems(cx))
 880            .items_center()
 881            .w(px(CONTROL_SIZE + 4.0))
 882            .overflow_hidden()
 883            .rounded(px(5.))
 884            .bg(cx.theme().colors().title_bar_background)
 885            .p_px()
 886            .border_1()
 887            .border_color(cx.theme().colors().border)
 888    }
 889
 890    fn render_notebook_control(
 891        id: impl Into<SharedString>,
 892        icon: IconName,
 893        _window: &mut Window,
 894        _cx: &mut Context<Self>,
 895    ) -> IconButton {
 896        let id: ElementId = ElementId::Name(id.into());
 897        IconButton::new(id, icon).width(px(CONTROL_SIZE))
 898    }
 899
 900    fn render_notebook_controls(
 901        &self,
 902        window: &mut Window,
 903        cx: &mut Context<Self>,
 904    ) -> impl IntoElement {
 905        let has_outputs = self.has_outputs(window, cx);
 906
 907        v_flex()
 908            .max_w(px(CONTROL_SIZE + 4.0))
 909            .items_center()
 910            .gap(DynamicSpacing::Base16.rems(cx))
 911            .justify_between()
 912            .flex_none()
 913            .h_full()
 914            .py(DynamicSpacing::Base12.px(cx))
 915            .child(
 916                v_flex()
 917                    .gap(DynamicSpacing::Base08.rems(cx))
 918                    .child(
 919                        Self::button_group(window, cx)
 920                            .child(
 921                                Self::render_notebook_control(
 922                                    "run-all-cells",
 923                                    IconName::PlayFilled,
 924                                    window,
 925                                    cx,
 926                                )
 927                                .tooltip(move |window, cx| {
 928                                    Tooltip::for_action("Execute all cells", &RunAll, cx)
 929                                })
 930                                .on_click(|_, window, cx| {
 931                                    window.dispatch_action(Box::new(RunAll), cx);
 932                                }),
 933                            )
 934                            .child(
 935                                Self::render_notebook_control(
 936                                    "clear-all-outputs",
 937                                    IconName::ListX,
 938                                    window,
 939                                    cx,
 940                                )
 941                                .disabled(!has_outputs)
 942                                .tooltip(move |window, cx| {
 943                                    Tooltip::for_action("Clear all outputs", &ClearOutputs, cx)
 944                                })
 945                                .on_click(|_, window, cx| {
 946                                    window.dispatch_action(Box::new(ClearOutputs), cx);
 947                                }),
 948                            ),
 949                    )
 950                    .child(
 951                        Self::button_group(window, cx)
 952                            .child(
 953                                Self::render_notebook_control(
 954                                    "move-cell-up",
 955                                    IconName::ArrowUp,
 956                                    window,
 957                                    cx,
 958                                )
 959                                .tooltip(move |window, cx| {
 960                                    Tooltip::for_action("Move cell up", &MoveCellUp, cx)
 961                                })
 962                                .on_click(|_, window, cx| {
 963                                    window.dispatch_action(Box::new(MoveCellUp), cx);
 964                                }),
 965                            )
 966                            .child(
 967                                Self::render_notebook_control(
 968                                    "move-cell-down",
 969                                    IconName::ArrowDown,
 970                                    window,
 971                                    cx,
 972                                )
 973                                .tooltip(move |window, cx| {
 974                                    Tooltip::for_action("Move cell down", &MoveCellDown, cx)
 975                                })
 976                                .on_click(|_, window, cx| {
 977                                    window.dispatch_action(Box::new(MoveCellDown), cx);
 978                                }),
 979                            ),
 980                    )
 981                    .child(
 982                        Self::button_group(window, cx)
 983                            .child(
 984                                Self::render_notebook_control(
 985                                    "new-markdown-cell",
 986                                    IconName::Plus,
 987                                    window,
 988                                    cx,
 989                                )
 990                                .tooltip(move |window, cx| {
 991                                    Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx)
 992                                })
 993                                .on_click(|_, window, cx| {
 994                                    window.dispatch_action(Box::new(AddMarkdownBlock), cx);
 995                                }),
 996                            )
 997                            .child(
 998                                Self::render_notebook_control(
 999                                    "new-code-cell",
1000                                    IconName::Code,
1001                                    window,
1002                                    cx,
1003                                )
1004                                .tooltip(move |window, cx| {
1005                                    Tooltip::for_action("Add code block", &AddCodeBlock, cx)
1006                                })
1007                                .on_click(|_, window, cx| {
1008                                    window.dispatch_action(Box::new(AddCodeBlock), cx);
1009                                }),
1010                            ),
1011                    ),
1012            )
1013            .child(
1014                v_flex()
1015                    .gap(DynamicSpacing::Base08.rems(cx))
1016                    .items_center()
1017                    .child(
1018                        Self::render_notebook_control("more-menu", IconName::Ellipsis, window, cx)
1019                            .tooltip(move |window, cx| (Tooltip::text("More options"))(window, cx)),
1020                    )
1021                    .child(Self::button_group(window, cx).child({
1022                        let kernel_status = self.kernel.status();
1023                        let (icon, icon_color) = match &kernel_status {
1024                            KernelStatus::Idle => (IconName::ReplNeutral, Color::Success),
1025                            KernelStatus::Busy => (IconName::ReplNeutral, Color::Warning),
1026                            KernelStatus::Starting => (IconName::ReplNeutral, Color::Muted),
1027                            KernelStatus::Error => (IconName::ReplNeutral, Color::Error),
1028                            KernelStatus::ShuttingDown => (IconName::ReplNeutral, Color::Muted),
1029                            KernelStatus::Shutdown => (IconName::ReplNeutral, Color::Disabled),
1030                            KernelStatus::Restarting => (IconName::ReplNeutral, Color::Warning),
1031                        };
1032                        let kernel_name = self
1033                            .kernel_specification
1034                            .as_ref()
1035                            .map(|spec| spec.name().to_string())
1036                            .unwrap_or_else(|| "Select Kernel".to_string());
1037                        IconButton::new("repl", icon)
1038                            .icon_color(icon_color)
1039                            .tooltip(move |window, cx| {
1040                                Tooltip::text(format!(
1041                                    "{} ({}). Click to change kernel.",
1042                                    kernel_name,
1043                                    kernel_status.to_string()
1044                                ))(window, cx)
1045                            })
1046                            .on_click(cx.listener(|this, _, window, cx| {
1047                                this.kernel_picker_handle.toggle(window, cx);
1048                            }))
1049                    })),
1050            )
1051    }
1052
1053    fn render_kernel_status_bar(
1054        &self,
1055        _window: &mut Window,
1056        cx: &mut Context<Self>,
1057    ) -> impl IntoElement {
1058        let kernel_status = self.kernel.status();
1059        let kernel_name = self
1060            .kernel_specification
1061            .as_ref()
1062            .map(|spec| spec.name().to_string())
1063            .unwrap_or_else(|| "Select Kernel".to_string());
1064
1065        let (status_icon, status_color) = match &kernel_status {
1066            KernelStatus::Idle => (IconName::Circle, Color::Success),
1067            KernelStatus::Busy => (IconName::ArrowCircle, Color::Warning),
1068            KernelStatus::Starting => (IconName::ArrowCircle, Color::Muted),
1069            KernelStatus::Error => (IconName::XCircle, Color::Error),
1070            KernelStatus::ShuttingDown => (IconName::ArrowCircle, Color::Muted),
1071            KernelStatus::Shutdown => (IconName::Circle, Color::Muted),
1072            KernelStatus::Restarting => (IconName::ArrowCircle, Color::Warning),
1073        };
1074
1075        let is_spinning = matches!(
1076            kernel_status,
1077            KernelStatus::Busy
1078                | KernelStatus::Starting
1079                | KernelStatus::ShuttingDown
1080                | KernelStatus::Restarting
1081        );
1082
1083        let status_icon_element = if is_spinning {
1084            Icon::new(status_icon)
1085                .size(IconSize::Small)
1086                .color(status_color)
1087                .with_rotate_animation(2)
1088                .into_any_element()
1089        } else {
1090            Icon::new(status_icon)
1091                .size(IconSize::Small)
1092                .color(status_color)
1093                .into_any_element()
1094        };
1095
1096        let worktree_id = self.worktree_id;
1097        let kernel_picker_handle = self.kernel_picker_handle.clone();
1098        let view = cx.entity().downgrade();
1099
1100        h_flex()
1101            .w_full()
1102            .px_3()
1103            .py_1()
1104            .gap_2()
1105            .items_center()
1106            .justify_between()
1107            .bg(cx.theme().colors().status_bar_background)
1108            .child(
1109                KernelSelector::new(
1110                    Box::new(move |spec: KernelSpecification, window, cx| {
1111                        if let Some(view) = view.upgrade() {
1112                            view.update(cx, |this, cx| {
1113                                this.change_kernel(spec, window, cx);
1114                            });
1115                        }
1116                    }),
1117                    worktree_id,
1118                    Button::new("kernel-selector", kernel_name.clone())
1119                        .label_size(LabelSize::Small)
1120                        .icon(status_icon)
1121                        .icon_size(IconSize::Small)
1122                        .icon_color(status_color)
1123                        .icon_position(IconPosition::Start),
1124                    Tooltip::text(format!(
1125                        "Kernel: {} ({}). Click to change.",
1126                        kernel_name,
1127                        kernel_status.to_string()
1128                    )),
1129                )
1130                .with_handle(kernel_picker_handle),
1131            )
1132            .child(
1133                h_flex()
1134                    .gap_1()
1135                    .child(
1136                        IconButton::new("restart-kernel", IconName::RotateCw)
1137                            .icon_size(IconSize::Small)
1138                            .tooltip(|window, cx| {
1139                                Tooltip::for_action("Restart Kernel", &RestartKernel, cx)
1140                            })
1141                            .on_click(cx.listener(|this, _, window, cx| {
1142                                this.restart_kernel(&RestartKernel, window, cx);
1143                            })),
1144                    )
1145                    .child(
1146                        IconButton::new("interrupt-kernel", IconName::Stop)
1147                            .icon_size(IconSize::Small)
1148                            .disabled(!matches!(kernel_status, KernelStatus::Busy))
1149                            .tooltip(|window, cx| {
1150                                Tooltip::for_action("Interrupt Kernel", &InterruptKernel, cx)
1151                            })
1152                            .on_click(cx.listener(|this, _, window, cx| {
1153                                this.interrupt_kernel(&InterruptKernel, window, cx);
1154                            })),
1155                    ),
1156            )
1157    }
1158
1159    fn cell_list(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1160        let view = cx.entity();
1161        list(self.cell_list.clone(), move |index, window, cx| {
1162            view.update(cx, |this, cx| {
1163                let cell_id = &this.cell_order[index];
1164                let cell = this.cell_map.get(cell_id).unwrap();
1165                this.render_cell(index, cell, window, cx).into_any_element()
1166            })
1167        })
1168        .size_full()
1169    }
1170
1171    fn cell_position(&self, index: usize) -> CellPosition {
1172        match index {
1173            0 => CellPosition::First,
1174            index if index == self.cell_count() - 1 => CellPosition::Last,
1175            _ => CellPosition::Middle,
1176        }
1177    }
1178
1179    fn render_cell(
1180        &self,
1181        index: usize,
1182        cell: &Cell,
1183        window: &mut Window,
1184        cx: &mut Context<Self>,
1185    ) -> impl IntoElement {
1186        let cell_position = self.cell_position(index);
1187
1188        let is_selected = index == self.selected_cell_index;
1189
1190        match cell {
1191            Cell::Code(cell) => {
1192                cell.update(cx, |cell, _cx| {
1193                    cell.set_selected(is_selected)
1194                        .set_cell_position(cell_position);
1195                });
1196                cell.clone().into_any_element()
1197            }
1198            Cell::Markdown(cell) => {
1199                cell.update(cx, |cell, _cx| {
1200                    cell.set_selected(is_selected)
1201                        .set_cell_position(cell_position);
1202                });
1203                cell.clone().into_any_element()
1204            }
1205            Cell::Raw(cell) => {
1206                cell.update(cx, |cell, _cx| {
1207                    cell.set_selected(is_selected)
1208                        .set_cell_position(cell_position);
1209                });
1210                cell.clone().into_any_element()
1211            }
1212        }
1213    }
1214}
1215
1216impl Render for NotebookEditor {
1217    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1218        v_flex()
1219            .size_full()
1220            .key_context("NotebookEditor")
1221            .track_focus(&self.focus_handle)
1222            .on_action(cx.listener(|this, _: &OpenNotebook, window, cx| {
1223                this.open_notebook(&OpenNotebook, window, cx)
1224            }))
1225            .on_action(
1226                cx.listener(|this, _: &ClearOutputs, window, cx| this.clear_outputs(window, cx)),
1227            )
1228            .on_action(
1229                cx.listener(|this, _: &Run, window, cx| this.run_current_cell(&Run, window, cx)),
1230            )
1231            .on_action(cx.listener(|this, _: &RunAll, window, cx| this.run_cells(window, cx)))
1232            .on_action(
1233                cx.listener(|this, _: &MoveCellUp, window, cx| this.move_cell_up(window, cx)),
1234            )
1235            .on_action(
1236                cx.listener(|this, _: &MoveCellDown, window, cx| this.move_cell_down(window, cx)),
1237            )
1238            .on_action(cx.listener(|this, _: &AddMarkdownBlock, window, cx| {
1239                this.add_markdown_block(window, cx)
1240            }))
1241            .on_action(
1242                cx.listener(|this, _: &AddCodeBlock, window, cx| this.add_code_block(window, cx)),
1243            )
1244            .on_action(cx.listener(|this, _: &MoveUp, window, cx| {
1245                this.select_previous(&menu::SelectPrevious, window, cx);
1246                if let Some(cell_id) = this.cell_order.get(this.selected_cell_index) {
1247                    if let Some(cell) = this.cell_map.get(cell_id) {
1248                        match cell {
1249                            Cell::Code(cell) => {
1250                                let editor = cell.read(cx).editor().clone();
1251                                editor.update(cx, |editor, cx| {
1252                                    editor.move_to_end(&Default::default(), window, cx);
1253                                });
1254                                editor.focus_handle(cx).focus(window, cx);
1255                            }
1256                            Cell::Markdown(cell) => {
1257                                cell.update(cx, |cell, cx| {
1258                                    cell.set_editing(true);
1259                                    cx.notify();
1260                                });
1261                                let editor = cell.read(cx).editor().clone();
1262                                editor.update(cx, |editor, cx| {
1263                                    editor.move_to_end(&Default::default(), window, cx);
1264                                });
1265                                editor.focus_handle(cx).focus(window, cx);
1266                            }
1267                            _ => {}
1268                        }
1269                    }
1270                }
1271            }))
1272            .on_action(cx.listener(|this, _: &MoveDown, window, cx| {
1273                this.select_next(&menu::SelectNext, window, cx);
1274                if let Some(cell_id) = this.cell_order.get(this.selected_cell_index) {
1275                    if let Some(cell) = this.cell_map.get(cell_id) {
1276                        match cell {
1277                            Cell::Code(cell) => {
1278                                let editor = cell.read(cx).editor().clone();
1279                                editor.update(cx, |editor, cx| {
1280                                    editor.move_to_beginning(&Default::default(), window, cx);
1281                                });
1282                                editor.focus_handle(cx).focus(window, cx);
1283                            }
1284                            Cell::Markdown(cell) => {
1285                                cell.update(cx, |cell, cx| {
1286                                    cell.set_editing(true);
1287                                    cx.notify();
1288                                });
1289                                let editor = cell.read(cx).editor().clone();
1290                                editor.update(cx, |editor, cx| {
1291                                    editor.move_to_beginning(&Default::default(), window, cx);
1292                                });
1293                                editor.focus_handle(cx).focus(window, cx);
1294                            }
1295                            _ => {}
1296                        }
1297                    }
1298                }
1299            }))
1300            .on_action(cx.listener(|this, _: &NotebookMoveDown, window, cx| {
1301                let Some(cell_id) = this.cell_order.get(this.selected_cell_index) else {
1302                    return;
1303                };
1304                let Some(cell) = this.cell_map.get(cell_id) else {
1305                    return;
1306                };
1307
1308                let editor = match cell {
1309                    Cell::Code(cell) => cell.read(cx).editor().clone(),
1310                    Cell::Markdown(cell) => cell.read(cx).editor().clone(),
1311                    _ => return,
1312                };
1313
1314                let is_at_last_line = editor.update(cx, |editor, cx| {
1315                    let display_snapshot = editor.display_snapshot(cx);
1316                    let selections = editor.selections.all_display(&display_snapshot);
1317                    if let Some(selection) = selections.last() {
1318                        let head = selection.head();
1319                        let cursor_row = head.row();
1320                        let max_row = display_snapshot.max_point().row();
1321
1322                        cursor_row >= max_row
1323                    } else {
1324                        false
1325                    }
1326                });
1327
1328                if is_at_last_line {
1329                    this.select_next(&menu::SelectNext, window, cx);
1330                    if let Some(cell_id) = this.cell_order.get(this.selected_cell_index) {
1331                        if let Some(cell) = this.cell_map.get(cell_id) {
1332                            match cell {
1333                                Cell::Code(cell) => {
1334                                    let editor = cell.read(cx).editor().clone();
1335                                    editor.update(cx, |editor, cx| {
1336                                        editor.move_to_beginning(&Default::default(), window, cx);
1337                                    });
1338                                    editor.focus_handle(cx).focus(window, cx);
1339                                }
1340                                Cell::Markdown(cell) => {
1341                                    cell.update(cx, |cell, cx| {
1342                                        cell.set_editing(true);
1343                                        cx.notify();
1344                                    });
1345                                    let editor = cell.read(cx).editor().clone();
1346                                    editor.update(cx, |editor, cx| {
1347                                        editor.move_to_beginning(&Default::default(), window, cx);
1348                                    });
1349                                    editor.focus_handle(cx).focus(window, cx);
1350                                }
1351                                _ => {}
1352                            }
1353                        }
1354                    }
1355                } else {
1356                    editor.update(cx, |editor, cx| {
1357                        editor.move_down(&Default::default(), window, cx);
1358                    });
1359                }
1360            }))
1361            .on_action(cx.listener(|this, _: &NotebookMoveUp, window, cx| {
1362                let Some(cell_id) = this.cell_order.get(this.selected_cell_index) else {
1363                    return;
1364                };
1365                let Some(cell) = this.cell_map.get(cell_id) else {
1366                    return;
1367                };
1368
1369                let editor = match cell {
1370                    Cell::Code(cell) => cell.read(cx).editor().clone(),
1371                    Cell::Markdown(cell) => cell.read(cx).editor().clone(),
1372                    _ => return,
1373                };
1374
1375                let is_at_first_line = editor.update(cx, |editor, cx| {
1376                    let display_snapshot = editor.display_snapshot(cx);
1377                    let selections = editor.selections.all_display(&display_snapshot);
1378                    if let Some(selection) = selections.first() {
1379                        let head = selection.head();
1380                        let cursor_row = head.row();
1381
1382                        cursor_row.0 == 0
1383                    } else {
1384                        false
1385                    }
1386                });
1387
1388                if is_at_first_line {
1389                    this.select_previous(&menu::SelectPrevious, window, cx);
1390                    if let Some(cell_id) = this.cell_order.get(this.selected_cell_index) {
1391                        if let Some(cell) = this.cell_map.get(cell_id) {
1392                            match cell {
1393                                Cell::Code(cell) => {
1394                                    let editor = cell.read(cx).editor().clone();
1395                                    editor.update(cx, |editor, cx| {
1396                                        editor.move_to_end(&Default::default(), window, cx);
1397                                    });
1398                                    editor.focus_handle(cx).focus(window, cx);
1399                                }
1400                                Cell::Markdown(cell) => {
1401                                    cell.update(cx, |cell, cx| {
1402                                        cell.set_editing(true);
1403                                        cx.notify();
1404                                    });
1405                                    let editor = cell.read(cx).editor().clone();
1406                                    editor.update(cx, |editor, cx| {
1407                                        editor.move_to_end(&Default::default(), window, cx);
1408                                    });
1409                                    editor.focus_handle(cx).focus(window, cx);
1410                                }
1411                                _ => {}
1412                            }
1413                        }
1414                    }
1415                } else {
1416                    editor.update(cx, |editor, cx| {
1417                        editor.move_up(&Default::default(), window, cx);
1418                    });
1419                }
1420            }))
1421            .on_action(
1422                cx.listener(|this, action, window, cx| this.restart_kernel(action, window, cx)),
1423            )
1424            .on_action(
1425                cx.listener(|this, action, window, cx| this.interrupt_kernel(action, window, cx)),
1426            )
1427            .child(
1428                h_flex()
1429                    .flex_1()
1430                    .w_full()
1431                    .h_full()
1432                    .gap_2()
1433                    .child(div().flex_1().h_full().child(self.cell_list(window, cx)))
1434                    .child(self.render_notebook_controls(window, cx)),
1435            )
1436            .child(self.render_kernel_status_bar(window, cx))
1437    }
1438}
1439
1440impl Focusable for NotebookEditor {
1441    fn focus_handle(&self, _: &App) -> FocusHandle {
1442        self.focus_handle.clone()
1443    }
1444}
1445
1446// Intended to be a NotebookBuffer
1447pub struct NotebookItem {
1448    path: PathBuf,
1449    project_path: ProjectPath,
1450    languages: Arc<LanguageRegistry>,
1451    // Raw notebook data
1452    notebook: nbformat::v4::Notebook,
1453    // Store our version of the notebook in memory (cell_order, cell_map)
1454    id: ProjectEntryId,
1455}
1456
1457impl project::ProjectItem for NotebookItem {
1458    fn try_open(
1459        project: &Entity<Project>,
1460        path: &ProjectPath,
1461        cx: &mut App,
1462    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
1463        let path = path.clone();
1464        let project = project.clone();
1465        let fs = project.read(cx).fs().clone();
1466        let languages = project.read(cx).languages().clone();
1467
1468        if path.path.extension().unwrap_or_default() == "ipynb" {
1469            Some(cx.spawn(async move |cx| {
1470                let abs_path = project
1471                    .read_with(cx, |project, cx| project.absolute_path(&path, cx))
1472                    .with_context(|| format!("finding the absolute path of {path:?}"))?;
1473
1474                // todo: watch for changes to the file
1475                let buffer = project
1476                    .update(cx, |project, cx| project.open_buffer(path.clone(), cx))
1477                    .await?;
1478                let file_content = buffer.read_with(cx, |buffer, _| buffer.text());
1479
1480                let notebook = if file_content.trim().is_empty() {
1481                    nbformat::v4::Notebook {
1482                        nbformat: 4,
1483                        nbformat_minor: 5,
1484                        cells: vec![],
1485                        metadata: serde_json::from_str("{}").unwrap(),
1486                    }
1487                } else {
1488                    let notebook = match nbformat::parse_notebook(&file_content) {
1489                        Ok(nb) => nb,
1490                        Err(_) => {
1491                            // Pre-process to ensure IDs exist
1492                            let mut json: serde_json::Value = serde_json::from_str(&file_content)?;
1493                            if let Some(cells) =
1494                                json.get_mut("cells").and_then(|c| c.as_array_mut())
1495                            {
1496                                for cell in cells {
1497                                    if cell.get("id").is_none() {
1498                                        cell["id"] =
1499                                            serde_json::Value::String(Uuid::new_v4().to_string());
1500                                    }
1501                                }
1502                            }
1503                            let file_content = serde_json::to_string(&json)?;
1504                            nbformat::parse_notebook(&file_content)?
1505                        }
1506                    };
1507
1508                    match notebook {
1509                        nbformat::Notebook::V4(notebook) => notebook,
1510                        // 4.1 - 4.4 are converted to 4.5
1511                        nbformat::Notebook::Legacy(legacy_notebook) => {
1512                            // TODO: Decide if we want to mutate the notebook by including Cell IDs
1513                            // and any other conversions
1514
1515                            nbformat::upgrade_legacy_notebook(legacy_notebook)?
1516                        }
1517                    }
1518                };
1519
1520                let id = project
1521                    .update(cx, |project, cx| {
1522                        project.entry_for_path(&path, cx).map(|entry| entry.id)
1523                    })
1524                    .context("Entry not found")?;
1525
1526                Ok(cx.new(|_| NotebookItem {
1527                    path: abs_path,
1528                    project_path: path,
1529                    languages,
1530                    notebook,
1531                    id,
1532                }))
1533            }))
1534        } else {
1535            None
1536        }
1537    }
1538
1539    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
1540        Some(self.id)
1541    }
1542
1543    fn project_path(&self, _: &App) -> Option<ProjectPath> {
1544        Some(self.project_path.clone())
1545    }
1546
1547    fn is_dirty(&self) -> bool {
1548        // TODO: Track if notebook metadata or structure has changed
1549        false
1550    }
1551}
1552
1553impl NotebookItem {
1554    pub fn language_name(&self) -> Option<String> {
1555        self.notebook
1556            .metadata
1557            .language_info
1558            .as_ref()
1559            .map(|l| l.name.clone())
1560            .or(self
1561                .notebook
1562                .metadata
1563                .kernelspec
1564                .as_ref()
1565                .and_then(|spec| spec.language.clone()))
1566    }
1567
1568    pub fn notebook_language(&self) -> impl Future<Output = Option<Arc<Language>>> + use<> {
1569        let language_name = self.language_name();
1570        let languages = self.languages.clone();
1571
1572        async move {
1573            if let Some(language_name) = language_name {
1574                languages.language_for_name(&language_name).await.ok()
1575            } else {
1576                None
1577            }
1578        }
1579    }
1580}
1581
1582impl EventEmitter<()> for NotebookItem {}
1583
1584impl EventEmitter<()> for NotebookEditor {}
1585
1586// pub struct NotebookControls {
1587//     pane_focused: bool,
1588//     active_item: Option<Box<dyn ItemHandle>>,
1589//     // subscription: Option<Subscription>,
1590// }
1591
1592// impl NotebookControls {
1593//     pub fn new() -> Self {
1594//         Self {
1595//             pane_focused: false,
1596//             active_item: Default::default(),
1597//             // subscription: Default::default(),
1598//         }
1599//     }
1600// }
1601
1602// impl EventEmitter<ToolbarItemEvent> for NotebookControls {}
1603
1604// impl Render for NotebookControls {
1605//     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1606//         div().child("notebook controls")
1607//     }
1608// }
1609
1610// impl ToolbarItemView for NotebookControls {
1611//     fn set_active_pane_item(
1612//         &mut self,
1613//         active_pane_item: Option<&dyn workspace::ItemHandle>,
1614//         window: &mut Window, cx: &mut Context<Self>,
1615//     ) -> workspace::ToolbarItemLocation {
1616//         cx.notify();
1617//         self.active_item = None;
1618
1619//         let Some(item) = active_pane_item else {
1620//             return ToolbarItemLocation::Hidden;
1621//         };
1622
1623//         ToolbarItemLocation::PrimaryLeft
1624//     }
1625
1626//     fn pane_focus_update(&mut self, pane_focused: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1627//         self.pane_focused = pane_focused;
1628//     }
1629// }
1630
1631impl Item for NotebookEditor {
1632    type Event = ();
1633
1634    fn can_split(&self) -> bool {
1635        true
1636    }
1637
1638    fn clone_on_split(
1639        &self,
1640        _workspace_id: Option<workspace::WorkspaceId>,
1641        window: &mut Window,
1642        cx: &mut Context<Self>,
1643    ) -> Task<Option<Entity<Self>>>
1644    where
1645        Self: Sized,
1646    {
1647        Task::ready(Some(cx.new(|cx| {
1648            Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)
1649        })))
1650    }
1651
1652    fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {
1653        workspace::item::ItemBufferKind::Singleton
1654    }
1655
1656    fn for_each_project_item(
1657        &self,
1658        cx: &App,
1659        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1660    ) {
1661        f(self.notebook_item.entity_id(), self.notebook_item.read(cx))
1662    }
1663
1664    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
1665        self.notebook_item
1666            .read(cx)
1667            .project_path
1668            .path
1669            .file_name()
1670            .map(|s| s.to_string())
1671            .unwrap_or_default()
1672            .into()
1673    }
1674
1675    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
1676        Label::new(self.tab_content_text(params.detail.unwrap_or(0), cx))
1677            .single_line()
1678            .color(params.text_color())
1679            .when(params.preview, |this| this.italic())
1680            .into_any_element()
1681    }
1682
1683    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
1684        Some(IconName::Book.into())
1685    }
1686
1687    fn show_toolbar(&self) -> bool {
1688        false
1689    }
1690
1691    // TODO
1692    fn pixel_position_of_cursor(&self, _: &App) -> Option<Point<Pixels>> {
1693        None
1694    }
1695
1696    // TODO
1697    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
1698        None
1699    }
1700
1701    fn set_nav_history(
1702        &mut self,
1703        _: workspace::ItemNavHistory,
1704        _window: &mut Window,
1705        _: &mut Context<Self>,
1706    ) {
1707        // TODO
1708    }
1709
1710    fn can_save(&self, _cx: &App) -> bool {
1711        true
1712    }
1713
1714    fn save(
1715        &mut self,
1716        _options: SaveOptions,
1717        project: Entity<Project>,
1718        _window: &mut Window,
1719        cx: &mut Context<Self>,
1720    ) -> Task<Result<()>> {
1721        let notebook = self.to_notebook(cx);
1722        let path = self.notebook_item.read(cx).path.clone();
1723        let fs = project.read(cx).fs().clone();
1724
1725        self.mark_as_saved(cx);
1726
1727        cx.spawn(async move |_this, _cx| {
1728            let json =
1729                serde_json::to_string_pretty(&notebook).context("Failed to serialize notebook")?;
1730            fs.atomic_write(path, json).await?;
1731            Ok(())
1732        })
1733    }
1734
1735    fn save_as(
1736        &mut self,
1737        project: Entity<Project>,
1738        path: ProjectPath,
1739        _window: &mut Window,
1740        cx: &mut Context<Self>,
1741    ) -> Task<Result<()>> {
1742        let notebook = self.to_notebook(cx);
1743        let fs = project.read(cx).fs().clone();
1744
1745        let abs_path = project.read(cx).absolute_path(&path, cx);
1746
1747        self.mark_as_saved(cx);
1748
1749        cx.spawn(async move |_this, _cx| {
1750            let abs_path = abs_path.context("Failed to get absolute path")?;
1751            let json =
1752                serde_json::to_string_pretty(&notebook).context("Failed to serialize notebook")?;
1753            fs.atomic_write(abs_path, json).await?;
1754            Ok(())
1755        })
1756    }
1757
1758    fn reload(
1759        &mut self,
1760        project: Entity<Project>,
1761        window: &mut Window,
1762        cx: &mut Context<Self>,
1763    ) -> Task<Result<()>> {
1764        let project_path = self.notebook_item.read(cx).project_path.clone();
1765        let languages = self.languages.clone();
1766        let notebook_language = self.notebook_language.clone();
1767
1768        cx.spawn_in(window, async move |this, cx| {
1769            let buffer = this
1770                .update(cx, |this, cx| {
1771                    this.project
1772                        .update(cx, |project, cx| project.open_buffer(project_path, cx))
1773                })?
1774                .await?;
1775
1776            let file_content = buffer.read_with(cx, |buffer, _| buffer.text());
1777
1778            let mut json: serde_json::Value = serde_json::from_str(&file_content)?;
1779            if let Some(cells) = json.get_mut("cells").and_then(|c| c.as_array_mut()) {
1780                for cell in cells {
1781                    if cell.get("id").is_none() {
1782                        cell["id"] = serde_json::Value::String(Uuid::new_v4().to_string());
1783                    }
1784                }
1785            }
1786            let file_content = serde_json::to_string(&json)?;
1787
1788            let notebook = nbformat::parse_notebook(&file_content);
1789            let notebook = match notebook {
1790                Ok(nbformat::Notebook::V4(notebook)) => notebook,
1791                Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
1792                    nbformat::upgrade_legacy_notebook(legacy_notebook)?
1793                }
1794                Err(e) => {
1795                    anyhow::bail!("Failed to parse notebook: {:?}", e);
1796                }
1797            };
1798
1799            this.update_in(cx, |this, window, cx| {
1800                let mut cell_order = vec![];
1801                let mut cell_map = HashMap::default();
1802
1803                for cell in notebook.cells.iter() {
1804                    let cell_id = cell.id();
1805                    cell_order.push(cell_id.clone());
1806                    let cell_entity =
1807                        Cell::load(cell, &languages, notebook_language.clone(), window, cx);
1808                    cell_map.insert(cell_id.clone(), cell_entity);
1809                }
1810
1811                this.cell_order = cell_order.clone();
1812                this.original_cell_order = cell_order;
1813                this.cell_map = cell_map;
1814                this.cell_list =
1815                    ListState::new(this.cell_order.len(), gpui::ListAlignment::Top, px(1000.));
1816                cx.notify();
1817            })?;
1818
1819            Ok(())
1820        })
1821    }
1822
1823    fn is_dirty(&self, cx: &App) -> bool {
1824        self.has_structural_changes() || self.has_content_changes(cx)
1825    }
1826}
1827
1828impl ProjectItem for NotebookEditor {
1829    type Item = NotebookItem;
1830
1831    fn for_project_item(
1832        project: Entity<Project>,
1833        _pane: Option<&Pane>,
1834        item: Entity<Self::Item>,
1835        window: &mut Window,
1836        cx: &mut Context<Self>,
1837    ) -> Self {
1838        Self::new(project, item, window, cx)
1839    }
1840}
1841
1842impl KernelSession for NotebookEditor {
1843    fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context<Self>) {
1844        // Handle kernel status updates (these are broadcast to all)
1845        if let JupyterMessageContent::Status(status) = &message.content {
1846            self.kernel.set_execution_state(&status.execution_state);
1847            cx.notify();
1848        }
1849
1850        if let JupyterMessageContent::KernelInfoReply(reply) = &message.content {
1851            self.kernel.set_kernel_info(reply);
1852
1853            if let Ok(language_info) = serde_json::from_value::<nbformat::v4::LanguageInfo>(
1854                serde_json::to_value(&reply.language_info).unwrap(),
1855            ) {
1856                self.notebook_item.update(cx, |item, cx| {
1857                    item.notebook.metadata.language_info = Some(language_info);
1858                    cx.emit(());
1859                });
1860            }
1861            cx.notify();
1862        }
1863
1864        // Handle cell-specific messages
1865        if let Some(parent_header) = &message.parent_header {
1866            if let Some(cell_id) = self.execution_requests.get(&parent_header.msg_id) {
1867                if let Some(Cell::Code(cell)) = self.cell_map.get(cell_id) {
1868                    cell.update(cx, |cell, cx| {
1869                        cell.handle_message(message, window, cx);
1870                    });
1871                }
1872            }
1873        }
1874    }
1875
1876    fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
1877        self.kernel = Kernel::ErroredLaunch(error_message);
1878        cx.notify();
1879    }
1880}