notebook_ui.rs

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