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) => NativeRunningKernel::new(
 418                local_spec,
 419                entity_id,
 420                working_directory,
 421                fs,
 422                view,
 423                window,
 424                cx,
 425            ),
 426            KernelSpecification::PythonEnv(env_spec) => NativeRunningKernel::new(
 427                env_spec.as_local_spec(),
 428                entity_id,
 429                working_directory,
 430                fs,
 431                view,
 432                window,
 433                cx,
 434            ),
 435            KernelSpecification::Remote(remote_spec) => {
 436                RemoteRunningKernel::new(remote_spec, working_directory, view, window, cx)
 437            }
 438        };
 439
 440        let pending_kernel = cx
 441            .spawn(async move |this, cx| {
 442                let kernel = kernel_task.await;
 443
 444                match kernel {
 445                    Ok(kernel) => {
 446                        this.update(cx, |editor, cx| {
 447                            editor.kernel = Kernel::RunningKernel(kernel);
 448                            cx.notify();
 449                        })
 450                        .ok();
 451                    }
 452                    Err(err) => {
 453                        this.update(cx, |editor, cx| {
 454                            editor.kernel = Kernel::ErroredLaunch(err.to_string());
 455                            cx.notify();
 456                        })
 457                        .ok();
 458                    }
 459                }
 460            })
 461            .shared();
 462
 463        self.kernel = Kernel::StartingKernel(pending_kernel);
 464        cx.notify();
 465    }
 466
 467    // Note: Python environments are only detected as kernels if ipykernel is installed.
 468    // Users need to run `pip install ipykernel` (or `uv pip install ipykernel`) in their
 469    // virtual environment for it to appear in the kernel selector.
 470    // This happens because we have an ipykernel check inside the function python_env_kernel_specification in mod.rs L:121
 471
 472    fn change_kernel(
 473        &mut self,
 474        spec: KernelSpecification,
 475        window: &mut Window,
 476        cx: &mut Context<Self>,
 477    ) {
 478        if let Kernel::RunningKernel(kernel) = &mut self.kernel {
 479            kernel.force_shutdown(window, cx).detach();
 480        }
 481
 482        self.execution_requests.clear();
 483
 484        self.launch_kernel_with_spec(spec, window, cx);
 485    }
 486
 487    fn restart_kernel(&mut self, _: &RestartKernel, window: &mut Window, cx: &mut Context<Self>) {
 488        if let Some(spec) = self.kernel_specification.clone() {
 489            if let Kernel::RunningKernel(kernel) = &mut self.kernel {
 490                kernel.force_shutdown(window, cx).detach();
 491            }
 492
 493            self.kernel = Kernel::Restarting;
 494            cx.notify();
 495
 496            self.launch_kernel_with_spec(spec, window, cx);
 497        }
 498    }
 499
 500    fn interrupt_kernel(
 501        &mut self,
 502        _: &InterruptKernel,
 503        _window: &mut Window,
 504        cx: &mut Context<Self>,
 505    ) {
 506        if let Kernel::RunningKernel(kernel) = &self.kernel {
 507            let interrupt_request = runtimelib::InterruptRequest {};
 508            let message: JupyterMessage = interrupt_request.into();
 509            kernel.request_tx().try_send(message).ok();
 510            cx.notify();
 511        }
 512    }
 513
 514    fn execute_cell(&mut self, cell_id: CellId, cx: &mut Context<Self>) {
 515        let code = if let Some(Cell::Code(cell)) = self.cell_map.get(&cell_id) {
 516            let editor = cell.read(cx).editor().clone();
 517            let buffer = editor.read(cx).buffer().read(cx);
 518            buffer
 519                .as_singleton()
 520                .map(|b| b.read(cx).text())
 521                .unwrap_or_default()
 522        } else {
 523            return;
 524        };
 525
 526        if let Some(Cell::Code(cell)) = self.cell_map.get(&cell_id) {
 527            cell.update(cx, |cell, cx| {
 528                if cell.has_outputs() {
 529                    cell.clear_outputs();
 530                }
 531                cell.start_execution();
 532                cx.notify();
 533            });
 534        }
 535
 536        let request = ExecuteRequest {
 537            code,
 538            ..Default::default()
 539        };
 540        let message: JupyterMessage = request.into();
 541        let msg_id = message.header.msg_id.clone();
 542
 543        self.execution_requests.insert(msg_id, cell_id.clone());
 544
 545        if let Kernel::RunningKernel(kernel) = &mut self.kernel {
 546            kernel.request_tx().try_send(message).ok();
 547        }
 548    }
 549
 550    fn has_outputs(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 551        self.cell_map.values().any(|cell| {
 552            if let Cell::Code(code_cell) = cell {
 553                code_cell.read(cx).has_outputs()
 554            } else {
 555                false
 556            }
 557        })
 558    }
 559
 560    fn clear_outputs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 561        for cell in self.cell_map.values() {
 562            if let Cell::Code(code_cell) = cell {
 563                code_cell.update(cx, |cell, cx| {
 564                    cell.clear_outputs();
 565                    cx.notify();
 566                });
 567            }
 568        }
 569        cx.notify();
 570    }
 571
 572    fn run_cells(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 573        for cell_id in self.cell_order.clone() {
 574            self.execute_cell(cell_id, cx);
 575        }
 576    }
 577
 578    fn run_current_cell(&mut self, _: &Run, window: &mut Window, cx: &mut Context<Self>) {
 579        if let Some(cell_id) = self.cell_order.get(self.selected_cell_index).cloned() {
 580            if let Some(cell) = self.cell_map.get(&cell_id) {
 581                match cell {
 582                    Cell::Code(_) => {
 583                        self.execute_cell(cell_id, cx);
 584                    }
 585                    Cell::Markdown(markdown_cell) => {
 586                        // for markdown, finish editing and move to next cell
 587                        let is_editing = markdown_cell.read(cx).is_editing();
 588                        if is_editing {
 589                            markdown_cell.update(cx, |cell, cx| {
 590                                cell.run(cx);
 591                            });
 592                            // move to the next cell
 593                            // Discussion can be done on this default implementation
 594                            self.move_to_next_cell(window, cx);
 595                        }
 596                    }
 597                    Cell::Raw(_) => {}
 598                }
 599            }
 600        }
 601    }
 602
 603    // Discussion can be done on this default implementation
 604    /// Moves focus to the next cell, or creates a new code cell if at the end
 605    fn move_to_next_cell(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 606        if !self.cell_order.is_empty() && self.selected_cell_index < self.cell_order.len() - 1 {
 607            self.selected_cell_index += 1;
 608            // focus the new cell's editor
 609            if let Some(cell_id) = self.cell_order.get(self.selected_cell_index) {
 610                if let Some(cell) = self.cell_map.get(cell_id) {
 611                    match cell {
 612                        Cell::Code(code_cell) => {
 613                            let editor = code_cell.read(cx).editor();
 614                            window.focus(&editor.focus_handle(cx), cx);
 615                        }
 616                        Cell::Markdown(markdown_cell) => {
 617                            // Don't auto-enter edit mode for next markdown cell
 618                            // Just select it
 619                        }
 620                        Cell::Raw(_) => {}
 621                    }
 622                }
 623            }
 624            cx.notify();
 625        } else {
 626            // in the end, could optionally create a new cell
 627            // For now, just stay on the current cell
 628        }
 629    }
 630
 631    fn open_notebook(&mut self, _: &OpenNotebook, _window: &mut Window, _cx: &mut Context<Self>) {
 632        println!("Open notebook triggered");
 633    }
 634
 635    fn move_cell_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 636        println!("Move cell up triggered");
 637        if self.selected_cell_index > 0 {
 638            self.cell_order
 639                .swap(self.selected_cell_index, self.selected_cell_index - 1);
 640            self.selected_cell_index -= 1;
 641            cx.notify();
 642        }
 643    }
 644
 645    fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 646        println!("Move cell down triggered");
 647        if !self.cell_order.is_empty() && self.selected_cell_index < self.cell_order.len() - 1 {
 648            self.cell_order
 649                .swap(self.selected_cell_index, self.selected_cell_index + 1);
 650            self.selected_cell_index += 1;
 651            cx.notify();
 652        }
 653    }
 654
 655    fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 656        let new_cell_id: CellId = Uuid::new_v4().into();
 657        let languages = self.languages.clone();
 658        let metadata: nbformat::v4::CellMetadata =
 659            serde_json::from_str("{}").expect("empty object should parse");
 660
 661        let markdown_cell = cx.new(|cx| {
 662            super::MarkdownCell::new(
 663                new_cell_id.clone(),
 664                metadata,
 665                String::new(),
 666                languages,
 667                window,
 668                cx,
 669            )
 670        });
 671
 672        let insert_index = if self.cell_order.is_empty() {
 673            0
 674        } else {
 675            self.selected_cell_index + 1
 676        };
 677        self.cell_order.insert(insert_index, new_cell_id.clone());
 678        self.cell_map
 679            .insert(new_cell_id.clone(), Cell::Markdown(markdown_cell.clone()));
 680        self.selected_cell_index = insert_index;
 681
 682        cx.subscribe(
 683            &markdown_cell,
 684            move |_this, cell, event: &MarkdownCellEvent, cx| match event {
 685                MarkdownCellEvent::FinishedEditing | MarkdownCellEvent::Run(_) => {
 686                    cell.update(cx, |cell, cx| {
 687                        cell.reparse_markdown(cx);
 688                    });
 689                }
 690            },
 691        )
 692        .detach();
 693
 694        let cell_id_for_editor = new_cell_id.clone();
 695        let editor = markdown_cell.read(cx).editor().clone();
 696        cx.subscribe(&editor, move |this, _editor, event, cx| {
 697            if let editor::EditorEvent::Focused = event {
 698                if let Some(index) = this
 699                    .cell_order
 700                    .iter()
 701                    .position(|id| id == &cell_id_for_editor)
 702                {
 703                    this.selected_cell_index = index;
 704                    cx.notify();
 705                }
 706            }
 707        })
 708        .detach();
 709
 710        self.cell_list.reset(self.cell_order.len());
 711        cx.notify();
 712    }
 713
 714    fn add_code_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 715        let new_cell_id: CellId = Uuid::new_v4().into();
 716        let notebook_language = self.notebook_language.clone();
 717        let metadata: nbformat::v4::CellMetadata =
 718            serde_json::from_str("{}").expect("empty object should parse");
 719
 720        let code_cell = cx.new(|cx| {
 721            super::CodeCell::new(
 722                new_cell_id.clone(),
 723                metadata,
 724                String::new(),
 725                notebook_language,
 726                window,
 727                cx,
 728            )
 729        });
 730
 731        let insert_index = if self.cell_order.is_empty() {
 732            0
 733        } else {
 734            self.selected_cell_index + 1
 735        };
 736        self.cell_order.insert(insert_index, new_cell_id.clone());
 737        self.cell_map
 738            .insert(new_cell_id.clone(), Cell::Code(code_cell.clone()));
 739        self.selected_cell_index = insert_index;
 740
 741        let cell_id_for_run = new_cell_id.clone();
 742        cx.subscribe(&code_cell, move |this, _cell, event, cx| match event {
 743            CellEvent::Run(cell_id) => this.execute_cell(cell_id.clone(), cx),
 744            CellEvent::FocusedIn(_) => {
 745                if let Some(index) = this.cell_order.iter().position(|id| id == &cell_id_for_run) {
 746                    this.selected_cell_index = index;
 747                    cx.notify();
 748                }
 749            }
 750        })
 751        .detach();
 752
 753        let cell_id_for_editor = new_cell_id.clone();
 754        let editor = code_cell.read(cx).editor().clone();
 755        cx.subscribe(&editor, move |this, _editor, event, cx| {
 756            if let editor::EditorEvent::Focused = event {
 757                if let Some(index) = this
 758                    .cell_order
 759                    .iter()
 760                    .position(|id| id == &cell_id_for_editor)
 761                {
 762                    this.selected_cell_index = index;
 763                    cx.notify();
 764                }
 765            }
 766        })
 767        .detach();
 768
 769        self.cell_list.reset(self.cell_order.len());
 770        cx.notify();
 771    }
 772
 773    fn cell_count(&self) -> usize {
 774        self.cell_map.len()
 775    }
 776
 777    fn selected_index(&self) -> usize {
 778        self.selected_cell_index
 779    }
 780
 781    pub fn set_selected_index(
 782        &mut self,
 783        index: usize,
 784        jump_to_index: bool,
 785        window: &mut Window,
 786        cx: &mut Context<Self>,
 787    ) {
 788        // let previous_index = self.selected_cell_index;
 789        self.selected_cell_index = index;
 790        let current_index = self.selected_cell_index;
 791
 792        // in the future we may have some `on_cell_change` event that we want to fire here
 793
 794        if jump_to_index {
 795            self.jump_to_cell(current_index, window, cx);
 796        }
 797    }
 798
 799    pub fn select_next(
 800        &mut self,
 801        _: &menu::SelectNext,
 802        window: &mut Window,
 803        cx: &mut Context<Self>,
 804    ) {
 805        let count = self.cell_count();
 806        if count > 0 {
 807            let index = self.selected_index();
 808            let ix = if index == count - 1 {
 809                count - 1
 810            } else {
 811                index + 1
 812            };
 813            self.set_selected_index(ix, true, window, cx);
 814            cx.notify();
 815        }
 816    }
 817
 818    pub fn select_previous(
 819        &mut self,
 820        _: &menu::SelectPrevious,
 821        window: &mut Window,
 822        cx: &mut Context<Self>,
 823    ) {
 824        let count = self.cell_count();
 825        if count > 0 {
 826            let index = self.selected_index();
 827            let ix = if index == 0 { 0 } else { index - 1 };
 828            self.set_selected_index(ix, true, window, cx);
 829            cx.notify();
 830        }
 831    }
 832
 833    pub fn select_first(
 834        &mut self,
 835        _: &menu::SelectFirst,
 836        window: &mut Window,
 837        cx: &mut Context<Self>,
 838    ) {
 839        let count = self.cell_count();
 840        if count > 0 {
 841            self.set_selected_index(0, true, window, cx);
 842            cx.notify();
 843        }
 844    }
 845
 846    pub fn select_last(
 847        &mut self,
 848        _: &menu::SelectLast,
 849        window: &mut Window,
 850        cx: &mut Context<Self>,
 851    ) {
 852        let count = self.cell_count();
 853        if count > 0 {
 854            self.set_selected_index(count - 1, true, window, cx);
 855            cx.notify();
 856        }
 857    }
 858
 859    fn jump_to_cell(&mut self, index: usize, _window: &mut Window, _cx: &mut Context<Self>) {
 860        self.cell_list.scroll_to_reveal_item(index);
 861    }
 862
 863    fn button_group(window: &mut Window, cx: &mut Context<Self>) -> Div {
 864        v_flex()
 865            .gap(DynamicSpacing::Base04.rems(cx))
 866            .items_center()
 867            .w(px(CONTROL_SIZE + 4.0))
 868            .overflow_hidden()
 869            .rounded(px(5.))
 870            .bg(cx.theme().colors().title_bar_background)
 871            .p_px()
 872            .border_1()
 873            .border_color(cx.theme().colors().border)
 874    }
 875
 876    fn render_notebook_control(
 877        id: impl Into<SharedString>,
 878        icon: IconName,
 879        _window: &mut Window,
 880        _cx: &mut Context<Self>,
 881    ) -> IconButton {
 882        let id: ElementId = ElementId::Name(id.into());
 883        IconButton::new(id, icon).width(px(CONTROL_SIZE))
 884    }
 885
 886    fn render_notebook_controls(
 887        &self,
 888        window: &mut Window,
 889        cx: &mut Context<Self>,
 890    ) -> impl IntoElement {
 891        let has_outputs = self.has_outputs(window, cx);
 892
 893        v_flex()
 894            .max_w(px(CONTROL_SIZE + 4.0))
 895            .items_center()
 896            .gap(DynamicSpacing::Base16.rems(cx))
 897            .justify_between()
 898            .flex_none()
 899            .h_full()
 900            .py(DynamicSpacing::Base12.px(cx))
 901            .child(
 902                v_flex()
 903                    .gap(DynamicSpacing::Base08.rems(cx))
 904                    .child(
 905                        Self::button_group(window, cx)
 906                            .child(
 907                                Self::render_notebook_control(
 908                                    "run-all-cells",
 909                                    IconName::PlayFilled,
 910                                    window,
 911                                    cx,
 912                                )
 913                                .tooltip(move |window, cx| {
 914                                    Tooltip::for_action("Execute all cells", &RunAll, cx)
 915                                })
 916                                .on_click(|_, window, cx| {
 917                                    window.dispatch_action(Box::new(RunAll), cx);
 918                                }),
 919                            )
 920                            .child(
 921                                Self::render_notebook_control(
 922                                    "clear-all-outputs",
 923                                    IconName::ListX,
 924                                    window,
 925                                    cx,
 926                                )
 927                                .disabled(!has_outputs)
 928                                .tooltip(move |window, cx| {
 929                                    Tooltip::for_action("Clear all outputs", &ClearOutputs, cx)
 930                                })
 931                                .on_click(|_, window, cx| {
 932                                    window.dispatch_action(Box::new(ClearOutputs), cx);
 933                                }),
 934                            ),
 935                    )
 936                    .child(
 937                        Self::button_group(window, cx)
 938                            .child(
 939                                Self::render_notebook_control(
 940                                    "move-cell-up",
 941                                    IconName::ArrowUp,
 942                                    window,
 943                                    cx,
 944                                )
 945                                .tooltip(move |window, cx| {
 946                                    Tooltip::for_action("Move cell up", &MoveCellUp, cx)
 947                                })
 948                                .on_click(|_, window, cx| {
 949                                    window.dispatch_action(Box::new(MoveCellUp), cx);
 950                                }),
 951                            )
 952                            .child(
 953                                Self::render_notebook_control(
 954                                    "move-cell-down",
 955                                    IconName::ArrowDown,
 956                                    window,
 957                                    cx,
 958                                )
 959                                .tooltip(move |window, cx| {
 960                                    Tooltip::for_action("Move cell down", &MoveCellDown, cx)
 961                                })
 962                                .on_click(|_, window, cx| {
 963                                    window.dispatch_action(Box::new(MoveCellDown), cx);
 964                                }),
 965                            ),
 966                    )
 967                    .child(
 968                        Self::button_group(window, cx)
 969                            .child(
 970                                Self::render_notebook_control(
 971                                    "new-markdown-cell",
 972                                    IconName::Plus,
 973                                    window,
 974                                    cx,
 975                                )
 976                                .tooltip(move |window, cx| {
 977                                    Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx)
 978                                })
 979                                .on_click(|_, window, cx| {
 980                                    window.dispatch_action(Box::new(AddMarkdownBlock), cx);
 981                                }),
 982                            )
 983                            .child(
 984                                Self::render_notebook_control(
 985                                    "new-code-cell",
 986                                    IconName::Code,
 987                                    window,
 988                                    cx,
 989                                )
 990                                .tooltip(move |window, cx| {
 991                                    Tooltip::for_action("Add code block", &AddCodeBlock, cx)
 992                                })
 993                                .on_click(|_, window, cx| {
 994                                    window.dispatch_action(Box::new(AddCodeBlock), cx);
 995                                }),
 996                            ),
 997                    ),
 998            )
 999            .child(
1000                v_flex()
1001                    .gap(DynamicSpacing::Base08.rems(cx))
1002                    .items_center()
1003                    .child(
1004                        Self::render_notebook_control("more-menu", IconName::Ellipsis, window, cx)
1005                            .tooltip(move |window, cx| (Tooltip::text("More options"))(window, cx)),
1006                    )
1007                    .child(Self::button_group(window, cx).child({
1008                        let kernel_status = self.kernel.status();
1009                        let (icon, icon_color) = match &kernel_status {
1010                            KernelStatus::Idle => (IconName::ReplNeutral, Color::Success),
1011                            KernelStatus::Busy => (IconName::ReplNeutral, Color::Warning),
1012                            KernelStatus::Starting => (IconName::ReplNeutral, Color::Muted),
1013                            KernelStatus::Error => (IconName::ReplNeutral, Color::Error),
1014                            KernelStatus::ShuttingDown => (IconName::ReplNeutral, Color::Muted),
1015                            KernelStatus::Shutdown => (IconName::ReplNeutral, Color::Disabled),
1016                            KernelStatus::Restarting => (IconName::ReplNeutral, Color::Warning),
1017                        };
1018                        let kernel_name = self
1019                            .kernel_specification
1020                            .as_ref()
1021                            .map(|spec| spec.name().to_string())
1022                            .unwrap_or_else(|| "Select Kernel".to_string());
1023                        IconButton::new("repl", icon)
1024                            .icon_color(icon_color)
1025                            .tooltip(move |window, cx| {
1026                                Tooltip::text(format!(
1027                                    "{} ({}). Click to change kernel.",
1028                                    kernel_name,
1029                                    kernel_status.to_string()
1030                                ))(window, cx)
1031                            })
1032                            .on_click(cx.listener(|this, _, window, cx| {
1033                                this.kernel_picker_handle.toggle(window, cx);
1034                            }))
1035                    })),
1036            )
1037    }
1038
1039    fn render_kernel_status_bar(
1040        &self,
1041        _window: &mut Window,
1042        cx: &mut Context<Self>,
1043    ) -> impl IntoElement {
1044        let kernel_status = self.kernel.status();
1045        let kernel_name = self
1046            .kernel_specification
1047            .as_ref()
1048            .map(|spec| spec.name().to_string())
1049            .unwrap_or_else(|| "Select Kernel".to_string());
1050
1051        let (status_icon, status_color) = match &kernel_status {
1052            KernelStatus::Idle => (IconName::Circle, Color::Success),
1053            KernelStatus::Busy => (IconName::ArrowCircle, Color::Warning),
1054            KernelStatus::Starting => (IconName::ArrowCircle, Color::Muted),
1055            KernelStatus::Error => (IconName::XCircle, Color::Error),
1056            KernelStatus::ShuttingDown => (IconName::ArrowCircle, Color::Muted),
1057            KernelStatus::Shutdown => (IconName::Circle, Color::Muted),
1058            KernelStatus::Restarting => (IconName::ArrowCircle, Color::Warning),
1059        };
1060
1061        let is_spinning = matches!(
1062            kernel_status,
1063            KernelStatus::Busy
1064                | KernelStatus::Starting
1065                | KernelStatus::ShuttingDown
1066                | KernelStatus::Restarting
1067        );
1068
1069        let status_icon_element = if is_spinning {
1070            Icon::new(status_icon)
1071                .size(IconSize::Small)
1072                .color(status_color)
1073                .with_rotate_animation(2)
1074                .into_any_element()
1075        } else {
1076            Icon::new(status_icon)
1077                .size(IconSize::Small)
1078                .color(status_color)
1079                .into_any_element()
1080        };
1081
1082        let worktree_id = self.worktree_id;
1083        let kernel_picker_handle = self.kernel_picker_handle.clone();
1084        let view = cx.entity().downgrade();
1085
1086        h_flex()
1087            .w_full()
1088            .px_3()
1089            .py_1()
1090            .gap_2()
1091            .items_center()
1092            .justify_between()
1093            .bg(cx.theme().colors().status_bar_background)
1094            .child(
1095                KernelSelector::new(
1096                    Box::new(move |spec: KernelSpecification, window, cx| {
1097                        if let Some(view) = view.upgrade() {
1098                            view.update(cx, |this, cx| {
1099                                this.change_kernel(spec, window, cx);
1100                            });
1101                        }
1102                    }),
1103                    worktree_id,
1104                    Button::new("kernel-selector", kernel_name.clone())
1105                        .label_size(LabelSize::Small)
1106                        .icon(status_icon)
1107                        .icon_size(IconSize::Small)
1108                        .icon_color(status_color)
1109                        .icon_position(IconPosition::Start),
1110                    Tooltip::text(format!(
1111                        "Kernel: {} ({}). Click to change.",
1112                        kernel_name,
1113                        kernel_status.to_string()
1114                    )),
1115                )
1116                .with_handle(kernel_picker_handle),
1117            )
1118            .child(
1119                h_flex()
1120                    .gap_1()
1121                    .child(
1122                        IconButton::new("restart-kernel", IconName::RotateCw)
1123                            .icon_size(IconSize::Small)
1124                            .tooltip(|window, cx| {
1125                                Tooltip::for_action("Restart Kernel", &RestartKernel, cx)
1126                            })
1127                            .on_click(cx.listener(|this, _, window, cx| {
1128                                this.restart_kernel(&RestartKernel, window, cx);
1129                            })),
1130                    )
1131                    .child(
1132                        IconButton::new("interrupt-kernel", IconName::Stop)
1133                            .icon_size(IconSize::Small)
1134                            .disabled(!matches!(kernel_status, KernelStatus::Busy))
1135                            .tooltip(|window, cx| {
1136                                Tooltip::for_action("Interrupt Kernel", &InterruptKernel, cx)
1137                            })
1138                            .on_click(cx.listener(|this, _, window, cx| {
1139                                this.interrupt_kernel(&InterruptKernel, window, cx);
1140                            })),
1141                    ),
1142            )
1143    }
1144
1145    fn cell_list(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1146        let view = cx.entity();
1147        list(self.cell_list.clone(), move |index, window, cx| {
1148            view.update(cx, |this, cx| {
1149                let cell_id = &this.cell_order[index];
1150                let cell = this.cell_map.get(cell_id).unwrap();
1151                this.render_cell(index, cell, window, cx).into_any_element()
1152            })
1153        })
1154        .size_full()
1155    }
1156
1157    fn cell_position(&self, index: usize) -> CellPosition {
1158        match index {
1159            0 => CellPosition::First,
1160            index if index == self.cell_count() - 1 => CellPosition::Last,
1161            _ => CellPosition::Middle,
1162        }
1163    }
1164
1165    fn render_cell(
1166        &self,
1167        index: usize,
1168        cell: &Cell,
1169        window: &mut Window,
1170        cx: &mut Context<Self>,
1171    ) -> impl IntoElement {
1172        let cell_position = self.cell_position(index);
1173
1174        let is_selected = index == self.selected_cell_index;
1175
1176        match cell {
1177            Cell::Code(cell) => {
1178                cell.update(cx, |cell, _cx| {
1179                    cell.set_selected(is_selected)
1180                        .set_cell_position(cell_position);
1181                });
1182                cell.clone().into_any_element()
1183            }
1184            Cell::Markdown(cell) => {
1185                cell.update(cx, |cell, _cx| {
1186                    cell.set_selected(is_selected)
1187                        .set_cell_position(cell_position);
1188                });
1189                cell.clone().into_any_element()
1190            }
1191            Cell::Raw(cell) => {
1192                cell.update(cx, |cell, _cx| {
1193                    cell.set_selected(is_selected)
1194                        .set_cell_position(cell_position);
1195                });
1196                cell.clone().into_any_element()
1197            }
1198        }
1199    }
1200}
1201
1202impl Render for NotebookEditor {
1203    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1204        v_flex()
1205            .size_full()
1206            .key_context("NotebookEditor")
1207            .track_focus(&self.focus_handle)
1208            .on_action(cx.listener(|this, &OpenNotebook, window, cx| {
1209                this.open_notebook(&OpenNotebook, window, cx)
1210            }))
1211            .on_action(
1212                cx.listener(|this, &ClearOutputs, window, cx| this.clear_outputs(window, cx)),
1213            )
1214            .on_action(
1215                cx.listener(|this, &Run, window, cx| this.run_current_cell(&Run, window, cx)),
1216            )
1217            .on_action(cx.listener(|this, &RunAll, window, cx| this.run_cells(window, cx)))
1218            .on_action(cx.listener(|this, &MoveCellUp, window, cx| this.move_cell_up(window, cx)))
1219            .on_action(
1220                cx.listener(|this, &MoveCellDown, window, cx| this.move_cell_down(window, cx)),
1221            )
1222            .on_action(cx.listener(|this, &AddMarkdownBlock, window, cx| {
1223                this.add_markdown_block(window, cx)
1224            }))
1225            .on_action(
1226                cx.listener(|this, &AddCodeBlock, window, cx| this.add_code_block(window, cx)),
1227            )
1228            .on_action(cx.listener(|this, _: &MoveUp, window, cx| {
1229                this.select_previous(&menu::SelectPrevious, window, cx);
1230                if let Some(cell_id) = this.cell_order.get(this.selected_cell_index) {
1231                    if let Some(cell) = this.cell_map.get(cell_id) {
1232                        match cell {
1233                            Cell::Code(cell) => {
1234                                let editor = cell.read(cx).editor().clone();
1235                                editor.update(cx, |editor, cx| {
1236                                    editor.move_to_end(&Default::default(), window, cx);
1237                                });
1238                                editor.focus_handle(cx).focus(window, cx);
1239                            }
1240                            Cell::Markdown(cell) => {
1241                                cell.update(cx, |cell, cx| {
1242                                    cell.set_editing(true);
1243                                    cx.notify();
1244                                });
1245                                let editor = cell.read(cx).editor().clone();
1246                                editor.update(cx, |editor, cx| {
1247                                    editor.move_to_end(&Default::default(), window, cx);
1248                                });
1249                                editor.focus_handle(cx).focus(window, cx);
1250                            }
1251                            _ => {}
1252                        }
1253                    }
1254                }
1255            }))
1256            .on_action(cx.listener(|this, _: &MoveDown, window, cx| {
1257                this.select_next(&menu::SelectNext, window, cx);
1258                if let Some(cell_id) = this.cell_order.get(this.selected_cell_index) {
1259                    if let Some(cell) = this.cell_map.get(cell_id) {
1260                        match cell {
1261                            Cell::Code(cell) => {
1262                                let editor = cell.read(cx).editor().clone();
1263                                editor.update(cx, |editor, cx| {
1264                                    editor.move_to_beginning(&Default::default(), window, cx);
1265                                });
1266                                editor.focus_handle(cx).focus(window, cx);
1267                            }
1268                            Cell::Markdown(cell) => {
1269                                cell.update(cx, |cell, cx| {
1270                                    cell.set_editing(true);
1271                                    cx.notify();
1272                                });
1273                                let editor = cell.read(cx).editor().clone();
1274                                editor.update(cx, |editor, cx| {
1275                                    editor.move_to_beginning(&Default::default(), window, cx);
1276                                });
1277                                editor.focus_handle(cx).focus(window, cx);
1278                            }
1279                            _ => {}
1280                        }
1281                    }
1282                }
1283            }))
1284            .on_action(
1285                cx.listener(|this, action, window, cx| this.restart_kernel(action, window, cx)),
1286            )
1287            .on_action(
1288                cx.listener(|this, action, window, cx| this.interrupt_kernel(action, window, cx)),
1289            )
1290            .child(
1291                h_flex()
1292                    .flex_1()
1293                    .w_full()
1294                    .h_full()
1295                    .gap_2()
1296                    .child(div().flex_1().h_full().child(self.cell_list(window, cx)))
1297                    .child(self.render_notebook_controls(window, cx)),
1298            )
1299            .child(self.render_kernel_status_bar(window, cx))
1300    }
1301}
1302
1303impl Focusable for NotebookEditor {
1304    fn focus_handle(&self, _: &App) -> FocusHandle {
1305        self.focus_handle.clone()
1306    }
1307}
1308
1309// Intended to be a NotebookBuffer
1310pub struct NotebookItem {
1311    path: PathBuf,
1312    project_path: ProjectPath,
1313    languages: Arc<LanguageRegistry>,
1314    // Raw notebook data
1315    notebook: nbformat::v4::Notebook,
1316    // Store our version of the notebook in memory (cell_order, cell_map)
1317    id: ProjectEntryId,
1318}
1319
1320impl project::ProjectItem for NotebookItem {
1321    fn try_open(
1322        project: &Entity<Project>,
1323        path: &ProjectPath,
1324        cx: &mut App,
1325    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
1326        let path = path.clone();
1327        let project = project.clone();
1328        let fs = project.read(cx).fs().clone();
1329        let languages = project.read(cx).languages().clone();
1330
1331        if path.path.extension().unwrap_or_default() == "ipynb" {
1332            Some(cx.spawn(async move |cx| {
1333                let abs_path = project
1334                    .read_with(cx, |project, cx| project.absolute_path(&path, cx))
1335                    .with_context(|| format!("finding the absolute path of {path:?}"))?;
1336
1337                // todo: watch for changes to the file
1338                let file_content = fs.load(abs_path.as_path()).await?;
1339
1340                let notebook = if file_content.trim().is_empty() {
1341                    nbformat::v4::Notebook {
1342                        nbformat: 4,
1343                        nbformat_minor: 5,
1344                        cells: vec![],
1345                        metadata: serde_json::from_str("{}").unwrap(),
1346                    }
1347                } else {
1348                    let notebook = match nbformat::parse_notebook(&file_content) {
1349                        Ok(nb) => nb,
1350                        Err(_) => {
1351                            // Pre-process to ensure IDs exist
1352                            let mut json: serde_json::Value = serde_json::from_str(&file_content)?;
1353                            if let Some(cells) =
1354                                json.get_mut("cells").and_then(|c| c.as_array_mut())
1355                            {
1356                                for cell in cells {
1357                                    if cell.get("id").is_none() {
1358                                        cell["id"] =
1359                                            serde_json::Value::String(Uuid::new_v4().to_string());
1360                                    }
1361                                }
1362                            }
1363                            let file_content = serde_json::to_string(&json)?;
1364                            nbformat::parse_notebook(&file_content)?
1365                        }
1366                    };
1367
1368                    match notebook {
1369                        nbformat::Notebook::V4(notebook) => notebook,
1370                        // 4.1 - 4.4 are converted to 4.5
1371                        nbformat::Notebook::Legacy(legacy_notebook) => {
1372                            // TODO: Decide if we want to mutate the notebook by including Cell IDs
1373                            // and any other conversions
1374
1375                            nbformat::upgrade_legacy_notebook(legacy_notebook)?
1376                        }
1377                    }
1378                };
1379
1380                let id = project
1381                    .update(cx, |project, cx| {
1382                        project.entry_for_path(&path, cx).map(|entry| entry.id)
1383                    })
1384                    .context("Entry not found")?;
1385
1386                Ok(cx.new(|_| NotebookItem {
1387                    path: abs_path,
1388                    project_path: path,
1389                    languages,
1390                    notebook,
1391                    id,
1392                }))
1393            }))
1394        } else {
1395            None
1396        }
1397    }
1398
1399    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
1400        Some(self.id)
1401    }
1402
1403    fn project_path(&self, _: &App) -> Option<ProjectPath> {
1404        Some(self.project_path.clone())
1405    }
1406
1407    fn is_dirty(&self) -> bool {
1408        // TODO: Track if notebook metadata or structure has changed
1409        false
1410    }
1411}
1412
1413impl NotebookItem {
1414    pub fn language_name(&self) -> Option<String> {
1415        self.notebook
1416            .metadata
1417            .language_info
1418            .as_ref()
1419            .map(|l| l.name.clone())
1420            .or(self
1421                .notebook
1422                .metadata
1423                .kernelspec
1424                .as_ref()
1425                .and_then(|spec| spec.language.clone()))
1426    }
1427
1428    pub fn notebook_language(&self) -> impl Future<Output = Option<Arc<Language>>> + use<> {
1429        let language_name = self.language_name();
1430        let languages = self.languages.clone();
1431
1432        async move {
1433            if let Some(language_name) = language_name {
1434                languages.language_for_name(&language_name).await.ok()
1435            } else {
1436                None
1437            }
1438        }
1439    }
1440}
1441
1442impl EventEmitter<()> for NotebookItem {}
1443
1444impl EventEmitter<()> for NotebookEditor {}
1445
1446// pub struct NotebookControls {
1447//     pane_focused: bool,
1448//     active_item: Option<Box<dyn ItemHandle>>,
1449//     // subscription: Option<Subscription>,
1450// }
1451
1452// impl NotebookControls {
1453//     pub fn new() -> Self {
1454//         Self {
1455//             pane_focused: false,
1456//             active_item: Default::default(),
1457//             // subscription: Default::default(),
1458//         }
1459//     }
1460// }
1461
1462// impl EventEmitter<ToolbarItemEvent> for NotebookControls {}
1463
1464// impl Render for NotebookControls {
1465//     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1466//         div().child("notebook controls")
1467//     }
1468// }
1469
1470// impl ToolbarItemView for NotebookControls {
1471//     fn set_active_pane_item(
1472//         &mut self,
1473//         active_pane_item: Option<&dyn workspace::ItemHandle>,
1474//         window: &mut Window, cx: &mut Context<Self>,
1475//     ) -> workspace::ToolbarItemLocation {
1476//         cx.notify();
1477//         self.active_item = None;
1478
1479//         let Some(item) = active_pane_item else {
1480//             return ToolbarItemLocation::Hidden;
1481//         };
1482
1483//         ToolbarItemLocation::PrimaryLeft
1484//     }
1485
1486//     fn pane_focus_update(&mut self, pane_focused: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1487//         self.pane_focused = pane_focused;
1488//     }
1489// }
1490
1491impl Item for NotebookEditor {
1492    type Event = ();
1493
1494    fn can_split(&self) -> bool {
1495        true
1496    }
1497
1498    fn clone_on_split(
1499        &self,
1500        _workspace_id: Option<workspace::WorkspaceId>,
1501        window: &mut Window,
1502        cx: &mut Context<Self>,
1503    ) -> Task<Option<Entity<Self>>>
1504    where
1505        Self: Sized,
1506    {
1507        Task::ready(Some(cx.new(|cx| {
1508            Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)
1509        })))
1510    }
1511
1512    fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {
1513        workspace::item::ItemBufferKind::Singleton
1514    }
1515
1516    fn for_each_project_item(
1517        &self,
1518        cx: &App,
1519        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1520    ) {
1521        f(self.notebook_item.entity_id(), self.notebook_item.read(cx))
1522    }
1523
1524    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
1525        self.notebook_item
1526            .read(cx)
1527            .project_path
1528            .path
1529            .file_name()
1530            .map(|s| s.to_string())
1531            .unwrap_or_default()
1532            .into()
1533    }
1534
1535    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
1536        Label::new(self.tab_content_text(params.detail.unwrap_or(0), cx))
1537            .single_line()
1538            .color(params.text_color())
1539            .when(params.preview, |this| this.italic())
1540            .into_any_element()
1541    }
1542
1543    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
1544        Some(IconName::Book.into())
1545    }
1546
1547    fn show_toolbar(&self) -> bool {
1548        false
1549    }
1550
1551    // TODO
1552    fn pixel_position_of_cursor(&self, _: &App) -> Option<Point<Pixels>> {
1553        None
1554    }
1555
1556    // TODO
1557    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
1558        None
1559    }
1560
1561    fn set_nav_history(
1562        &mut self,
1563        _: workspace::ItemNavHistory,
1564        _window: &mut Window,
1565        _: &mut Context<Self>,
1566    ) {
1567        // TODO
1568    }
1569
1570    fn can_save(&self, _cx: &App) -> bool {
1571        true
1572    }
1573
1574    fn save(
1575        &mut self,
1576        _options: SaveOptions,
1577        project: Entity<Project>,
1578        _window: &mut Window,
1579        cx: &mut Context<Self>,
1580    ) -> Task<Result<()>> {
1581        let notebook = self.to_notebook(cx);
1582        let path = self.notebook_item.read(cx).path.clone();
1583        let fs = project.read(cx).fs().clone();
1584
1585        self.mark_as_saved(cx);
1586
1587        cx.spawn(async move |_this, _cx| {
1588            let json =
1589                serde_json::to_string_pretty(&notebook).context("Failed to serialize notebook")?;
1590            fs.atomic_write(path, json).await?;
1591            Ok(())
1592        })
1593    }
1594
1595    fn save_as(
1596        &mut self,
1597        project: Entity<Project>,
1598        path: ProjectPath,
1599        _window: &mut Window,
1600        cx: &mut Context<Self>,
1601    ) -> Task<Result<()>> {
1602        let notebook = self.to_notebook(cx);
1603        let fs = project.read(cx).fs().clone();
1604
1605        let abs_path = project.read(cx).absolute_path(&path, cx);
1606
1607        self.mark_as_saved(cx);
1608
1609        cx.spawn(async move |_this, _cx| {
1610            let abs_path = abs_path.context("Failed to get absolute path")?;
1611            let json =
1612                serde_json::to_string_pretty(&notebook).context("Failed to serialize notebook")?;
1613            fs.atomic_write(abs_path, json).await?;
1614            Ok(())
1615        })
1616    }
1617
1618    fn reload(
1619        &mut self,
1620        project: Entity<Project>,
1621        window: &mut Window,
1622        cx: &mut Context<Self>,
1623    ) -> Task<Result<()>> {
1624        let path = self.notebook_item.read(cx).path.clone();
1625        let fs = project.read(cx).fs().clone();
1626        let languages = self.languages.clone();
1627        let notebook_language = self.notebook_language.clone();
1628
1629        cx.spawn_in(window, async move |this, cx| {
1630            let file_content = fs.load(&path).await?;
1631
1632            let mut json: serde_json::Value = serde_json::from_str(&file_content)?;
1633            if let Some(cells) = json.get_mut("cells").and_then(|c| c.as_array_mut()) {
1634                for cell in cells {
1635                    if cell.get("id").is_none() {
1636                        cell["id"] = serde_json::Value::String(Uuid::new_v4().to_string());
1637                    }
1638                }
1639            }
1640            let file_content = serde_json::to_string(&json)?;
1641
1642            let notebook = nbformat::parse_notebook(&file_content);
1643            let notebook = match notebook {
1644                Ok(nbformat::Notebook::V4(notebook)) => notebook,
1645                Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
1646                    nbformat::upgrade_legacy_notebook(legacy_notebook)?
1647                }
1648                Err(e) => {
1649                    anyhow::bail!("Failed to parse notebook: {:?}", e);
1650                }
1651            };
1652
1653            this.update_in(cx, |this, window, cx| {
1654                let mut cell_order = vec![];
1655                let mut cell_map = HashMap::default();
1656
1657                for cell in notebook.cells.iter() {
1658                    let cell_id = cell.id();
1659                    cell_order.push(cell_id.clone());
1660                    let cell_entity =
1661                        Cell::load(cell, &languages, notebook_language.clone(), window, cx);
1662                    cell_map.insert(cell_id.clone(), cell_entity);
1663                }
1664
1665                this.cell_order = cell_order.clone();
1666                this.original_cell_order = cell_order;
1667                this.cell_map = cell_map;
1668                this.cell_list =
1669                    ListState::new(this.cell_order.len(), gpui::ListAlignment::Top, px(1000.));
1670                cx.notify();
1671            })?;
1672
1673            Ok(())
1674        })
1675    }
1676
1677    fn is_dirty(&self, cx: &App) -> bool {
1678        self.has_structural_changes() || self.has_content_changes(cx)
1679    }
1680}
1681
1682impl ProjectItem for NotebookEditor {
1683    type Item = NotebookItem;
1684
1685    fn for_project_item(
1686        project: Entity<Project>,
1687        _pane: Option<&Pane>,
1688        item: Entity<Self::Item>,
1689        window: &mut Window,
1690        cx: &mut Context<Self>,
1691    ) -> Self {
1692        Self::new(project, item, window, cx)
1693    }
1694}
1695
1696impl KernelSession for NotebookEditor {
1697    fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context<Self>) {
1698        // Handle kernel status updates (these are broadcast to all)
1699        if let JupyterMessageContent::Status(status) = &message.content {
1700            self.kernel.set_execution_state(&status.execution_state);
1701            cx.notify();
1702        }
1703
1704        if let JupyterMessageContent::KernelInfoReply(reply) = &message.content {
1705            self.kernel.set_kernel_info(reply);
1706
1707            if let Ok(language_info) = serde_json::from_value::<nbformat::v4::LanguageInfo>(
1708                serde_json::to_value(&reply.language_info).unwrap(),
1709            ) {
1710                self.notebook_item.update(cx, |item, cx| {
1711                    item.notebook.metadata.language_info = Some(language_info);
1712                    cx.emit(());
1713                });
1714            }
1715            cx.notify();
1716        }
1717
1718        // Handle cell-specific messages
1719        if let Some(parent_header) = &message.parent_header {
1720            if let Some(cell_id) = self.execution_requests.get(&parent_header.msg_id) {
1721                if let Some(Cell::Code(cell)) = self.cell_map.get(cell_id) {
1722                    cell.update(cx, |cell, cx| {
1723                        cell.handle_message(message, window, cx);
1724                    });
1725                }
1726            }
1727        }
1728    }
1729
1730    fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
1731        self.kernel = Kernel::ErroredLaunch(error_message);
1732        cx.notify();
1733    }
1734}