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