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