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