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 language::{Language, LanguageRegistry};
 16use project::{Project, ProjectEntryId, ProjectPath};
 17use ui::{Tooltip, prelude::*};
 18use workspace::item::{ItemEvent, SaveOptions, TabContentParams};
 19use workspace::searchable::SearchableItemHandle;
 20use workspace::{Item, ItemHandle, Pane, ProjectItem, ToolbarItemLocation};
 21use workspace::{ToolbarItemEvent, ToolbarItemView};
 22
 23use super::{Cell, CellPosition, RenderableCell};
 24
 25use nbformat::v4::CellId;
 26use nbformat::v4::Metadata as NotebookMetadata;
 27
 28actions!(
 29    notebook,
 30    [
 31        /// Opens a Jupyter notebook file.
 32        OpenNotebook,
 33        /// Runs all cells in the notebook.
 34        RunAll,
 35        /// Clears all cell outputs.
 36        ClearOutputs,
 37        /// Moves the current cell up.
 38        MoveCellUp,
 39        /// Moves the current cell down.
 40        MoveCellDown,
 41        /// Adds a new markdown cell.
 42        AddMarkdownBlock,
 43        /// Adds a new code cell.
 44        AddCodeBlock,
 45    ]
 46);
 47
 48pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0;
 49pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0;
 50pub(crate) const MEDIUM_SPACING_SIZE: f32 = 12.0;
 51pub(crate) const LARGE_SPACING_SIZE: f32 = 16.0;
 52pub(crate) const GUTTER_WIDTH: f32 = 19.0;
 53pub(crate) const CODE_BLOCK_INSET: f32 = MEDIUM_SPACING_SIZE;
 54pub(crate) const CONTROL_SIZE: f32 = 20.0;
 55
 56pub fn init(cx: &mut App) {
 57    if cx.has_flag::<NotebookFeatureFlag>() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() {
 58        workspace::register_project_item::<NotebookEditor>(cx);
 59    }
 60
 61    cx.observe_flag::<NotebookFeatureFlag, _>({
 62        move |is_enabled, cx| {
 63            if is_enabled {
 64                workspace::register_project_item::<NotebookEditor>(cx);
 65            } else {
 66                // todo: there is no way to unregister a project item, so if the feature flag
 67                // gets turned off they need to restart Zed.
 68            }
 69        }
 70    })
 71    .detach();
 72}
 73
 74pub struct NotebookEditor {
 75    languages: Arc<LanguageRegistry>,
 76    project: Entity<Project>,
 77
 78    focus_handle: FocusHandle,
 79    notebook_item: Entity<NotebookItem>,
 80
 81    remote_id: Option<ViewId>,
 82    cell_list: ListState,
 83
 84    selected_cell_index: usize,
 85    cell_order: Vec<CellId>,
 86    cell_map: HashMap<CellId, Cell>,
 87}
 88
 89impl NotebookEditor {
 90    pub fn new(
 91        project: Entity<Project>,
 92        notebook_item: Entity<NotebookItem>,
 93        window: &mut Window,
 94        cx: &mut Context<Self>,
 95    ) -> Self {
 96        let focus_handle = cx.focus_handle();
 97
 98        let languages = project.read(cx).languages().clone();
 99        let language_name = notebook_item.read(cx).language_name();
100
101        let notebook_language = notebook_item.read(cx).notebook_language();
102        let notebook_language = cx
103            .spawn_in(window, async move |_, _| notebook_language.await)
104            .shared();
105
106        let mut cell_order = vec![]; // Vec<CellId>
107        let mut cell_map = HashMap::default(); // HashMap<CellId, Cell>
108
109        for (index, cell) in notebook_item
110            .read(cx)
111            .notebook
112            .clone()
113            .cells
114            .iter()
115            .enumerate()
116        {
117            let cell_id = cell.id();
118            cell_order.push(cell_id.clone());
119            cell_map.insert(
120                cell_id.clone(),
121                Cell::load(cell, &languages, notebook_language.clone(), window, cx),
122            );
123        }
124
125        let notebook_handle = cx.entity().downgrade();
126        let cell_count = cell_order.len();
127
128        let this = cx.entity();
129        let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.));
130
131        Self {
132            project,
133            languages: languages.clone(),
134            focus_handle,
135            notebook_item,
136            remote_id: None,
137            cell_list,
138            selected_cell_index: 0,
139            cell_order: cell_order.clone(),
140            cell_map: cell_map.clone(),
141        }
142    }
143
144    fn has_outputs(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
145        self.cell_map.values().any(|cell| {
146            if let Cell::Code(code_cell) = cell {
147                code_cell.read(cx).has_outputs()
148            } else {
149                false
150            }
151        })
152    }
153
154    fn clear_outputs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
155        for cell in self.cell_map.values() {
156            if let Cell::Code(code_cell) = cell {
157                code_cell.update(cx, |cell, _cx| {
158                    cell.clear_outputs();
159                });
160            }
161        }
162    }
163
164    fn run_cells(&mut self, window: &mut Window, cx: &mut Context<Self>) {
165        println!("Cells would all run here, if that was implemented!");
166    }
167
168    fn open_notebook(&mut self, _: &OpenNotebook, _window: &mut Window, _cx: &mut Context<Self>) {
169        println!("Open notebook triggered");
170    }
171
172    fn move_cell_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
173        println!("Move cell up triggered");
174    }
175
176    fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
177        println!("Move cell down triggered");
178    }
179
180    fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
181        println!("Add markdown block triggered");
182    }
183
184    fn add_code_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
185        println!("Add code block triggered");
186    }
187
188    fn cell_count(&self) -> usize {
189        self.cell_map.len()
190    }
191
192    fn selected_index(&self) -> usize {
193        self.selected_cell_index
194    }
195
196    pub fn set_selected_index(
197        &mut self,
198        index: usize,
199        jump_to_index: bool,
200        window: &mut Window,
201        cx: &mut Context<Self>,
202    ) {
203        // let previous_index = self.selected_cell_index;
204        self.selected_cell_index = index;
205        let current_index = self.selected_cell_index;
206
207        // in the future we may have some `on_cell_change` event that we want to fire here
208
209        if jump_to_index {
210            self.jump_to_cell(current_index, window, cx);
211        }
212    }
213
214    pub fn select_next(
215        &mut self,
216        _: &menu::SelectNext,
217        window: &mut Window,
218        cx: &mut Context<Self>,
219    ) {
220        let count = self.cell_count();
221        if count > 0 {
222            let index = self.selected_index();
223            let ix = if index == count - 1 {
224                count - 1
225            } else {
226                index + 1
227            };
228            self.set_selected_index(ix, true, window, cx);
229            cx.notify();
230        }
231    }
232
233    pub fn select_previous(
234        &mut self,
235        _: &menu::SelectPrevious,
236        window: &mut Window,
237        cx: &mut Context<Self>,
238    ) {
239        let count = self.cell_count();
240        if count > 0 {
241            let index = self.selected_index();
242            let ix = if index == 0 { 0 } else { index - 1 };
243            self.set_selected_index(ix, true, window, cx);
244            cx.notify();
245        }
246    }
247
248    pub fn select_first(
249        &mut self,
250        _: &menu::SelectFirst,
251        window: &mut Window,
252        cx: &mut Context<Self>,
253    ) {
254        let count = self.cell_count();
255        if count > 0 {
256            self.set_selected_index(0, true, window, cx);
257            cx.notify();
258        }
259    }
260
261    pub fn select_last(
262        &mut self,
263        _: &menu::SelectLast,
264        window: &mut Window,
265        cx: &mut Context<Self>,
266    ) {
267        let count = self.cell_count();
268        if count > 0 {
269            self.set_selected_index(count - 1, true, window, cx);
270            cx.notify();
271        }
272    }
273
274    fn jump_to_cell(&mut self, index: usize, _window: &mut Window, _cx: &mut Context<Self>) {
275        self.cell_list.scroll_to_reveal_item(index);
276    }
277
278    fn button_group(window: &mut Window, cx: &mut Context<Self>) -> Div {
279        v_flex()
280            .gap(DynamicSpacing::Base04.rems(cx))
281            .items_center()
282            .w(px(CONTROL_SIZE + 4.0))
283            .overflow_hidden()
284            .rounded(px(5.))
285            .bg(cx.theme().colors().title_bar_background)
286            .p_px()
287            .border_1()
288            .border_color(cx.theme().colors().border)
289    }
290
291    fn render_notebook_control(
292        id: impl Into<SharedString>,
293        icon: IconName,
294        _window: &mut Window,
295        _cx: &mut Context<Self>,
296    ) -> IconButton {
297        let id: ElementId = ElementId::Name(id.into());
298        IconButton::new(id, icon).width(px(CONTROL_SIZE))
299    }
300
301    fn render_notebook_controls(
302        &self,
303        window: &mut Window,
304        cx: &mut Context<Self>,
305    ) -> impl IntoElement {
306        let has_outputs = self.has_outputs(window, cx);
307
308        v_flex()
309            .max_w(px(CONTROL_SIZE + 4.0))
310            .items_center()
311            .gap(DynamicSpacing::Base16.rems(cx))
312            .justify_between()
313            .flex_none()
314            .h_full()
315            .py(DynamicSpacing::Base12.px(cx))
316            .child(
317                v_flex()
318                    .gap(DynamicSpacing::Base08.rems(cx))
319                    .child(
320                        Self::button_group(window, cx)
321                            .child(
322                                Self::render_notebook_control(
323                                    "run-all-cells",
324                                    IconName::PlayFilled,
325                                    window,
326                                    cx,
327                                )
328                                .tooltip(move |window, cx| {
329                                    Tooltip::for_action("Execute all cells", &RunAll, cx)
330                                })
331                                .on_click(|_, window, cx| {
332                                    window.dispatch_action(Box::new(RunAll), cx);
333                                }),
334                            )
335                            .child(
336                                Self::render_notebook_control(
337                                    "clear-all-outputs",
338                                    IconName::ListX,
339                                    window,
340                                    cx,
341                                )
342                                .disabled(!has_outputs)
343                                .tooltip(move |window, cx| {
344                                    Tooltip::for_action("Clear all outputs", &ClearOutputs, cx)
345                                })
346                                .on_click(|_, window, cx| {
347                                    window.dispatch_action(Box::new(ClearOutputs), cx);
348                                }),
349                            ),
350                    )
351                    .child(
352                        Self::button_group(window, cx)
353                            .child(
354                                Self::render_notebook_control(
355                                    "move-cell-up",
356                                    IconName::ArrowUp,
357                                    window,
358                                    cx,
359                                )
360                                .tooltip(move |window, cx| {
361                                    Tooltip::for_action("Move cell up", &MoveCellUp, cx)
362                                })
363                                .on_click(|_, window, cx| {
364                                    window.dispatch_action(Box::new(MoveCellUp), cx);
365                                }),
366                            )
367                            .child(
368                                Self::render_notebook_control(
369                                    "move-cell-down",
370                                    IconName::ArrowDown,
371                                    window,
372                                    cx,
373                                )
374                                .tooltip(move |window, cx| {
375                                    Tooltip::for_action("Move cell down", &MoveCellDown, cx)
376                                })
377                                .on_click(|_, window, cx| {
378                                    window.dispatch_action(Box::new(MoveCellDown), cx);
379                                }),
380                            ),
381                    )
382                    .child(
383                        Self::button_group(window, cx)
384                            .child(
385                                Self::render_notebook_control(
386                                    "new-markdown-cell",
387                                    IconName::Plus,
388                                    window,
389                                    cx,
390                                )
391                                .tooltip(move |window, cx| {
392                                    Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx)
393                                })
394                                .on_click(|_, window, cx| {
395                                    window.dispatch_action(Box::new(AddMarkdownBlock), cx);
396                                }),
397                            )
398                            .child(
399                                Self::render_notebook_control(
400                                    "new-code-cell",
401                                    IconName::Code,
402                                    window,
403                                    cx,
404                                )
405                                .tooltip(move |window, cx| {
406                                    Tooltip::for_action("Add code block", &AddCodeBlock, cx)
407                                })
408                                .on_click(|_, window, cx| {
409                                    window.dispatch_action(Box::new(AddCodeBlock), cx);
410                                }),
411                            ),
412                    ),
413            )
414            .child(
415                v_flex()
416                    .gap(DynamicSpacing::Base08.rems(cx))
417                    .items_center()
418                    .child(Self::render_notebook_control(
419                        "more-menu",
420                        IconName::Ellipsis,
421                        window,
422                        cx,
423                    ))
424                    .child(
425                        Self::button_group(window, cx)
426                            .child(IconButton::new("repl", IconName::ReplNeutral)),
427                    ),
428            )
429    }
430
431    fn cell_position(&self, index: usize) -> CellPosition {
432        match index {
433            0 => CellPosition::First,
434            index if index == self.cell_count() - 1 => CellPosition::Last,
435            _ => CellPosition::Middle,
436        }
437    }
438
439    fn render_cell(
440        &self,
441        index: usize,
442        cell: &Cell,
443        window: &mut Window,
444        cx: &mut Context<Self>,
445    ) -> impl IntoElement {
446        let cell_position = self.cell_position(index);
447
448        let is_selected = index == self.selected_cell_index;
449
450        match cell {
451            Cell::Code(cell) => {
452                cell.update(cx, |cell, _cx| {
453                    cell.set_selected(is_selected)
454                        .set_cell_position(cell_position);
455                });
456                cell.clone().into_any_element()
457            }
458            Cell::Markdown(cell) => {
459                cell.update(cx, |cell, _cx| {
460                    cell.set_selected(is_selected)
461                        .set_cell_position(cell_position);
462                });
463                cell.clone().into_any_element()
464            }
465            Cell::Raw(cell) => {
466                cell.update(cx, |cell, _cx| {
467                    cell.set_selected(is_selected)
468                        .set_cell_position(cell_position);
469                });
470                cell.clone().into_any_element()
471            }
472        }
473    }
474}
475
476impl Render for NotebookEditor {
477    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
478        div()
479            .key_context("notebook")
480            .track_focus(&self.focus_handle)
481            .on_action(cx.listener(|this, &OpenNotebook, window, cx| {
482                this.open_notebook(&OpenNotebook, window, cx)
483            }))
484            .on_action(
485                cx.listener(|this, &ClearOutputs, window, cx| this.clear_outputs(window, cx)),
486            )
487            .on_action(cx.listener(|this, &RunAll, window, cx| this.run_cells(window, cx)))
488            .on_action(cx.listener(|this, &MoveCellUp, window, cx| this.move_cell_up(window, cx)))
489            .on_action(
490                cx.listener(|this, &MoveCellDown, window, cx| this.move_cell_down(window, cx)),
491            )
492            .on_action(cx.listener(|this, &AddMarkdownBlock, window, cx| {
493                this.add_markdown_block(window, cx)
494            }))
495            .on_action(
496                cx.listener(|this, &AddCodeBlock, window, cx| this.add_code_block(window, cx)),
497            )
498            .on_action(cx.listener(Self::select_next))
499            .on_action(cx.listener(Self::select_previous))
500            .on_action(cx.listener(Self::select_first))
501            .on_action(cx.listener(Self::select_last))
502            .flex()
503            .items_start()
504            .size_full()
505            .overflow_hidden()
506            .px(DynamicSpacing::Base12.px(cx))
507            .gap(DynamicSpacing::Base12.px(cx))
508            .bg(cx.theme().colors().tab_bar_background)
509            .child(
510                v_flex()
511                    .id("notebook-cells")
512                    .flex_1()
513                    .size_full()
514                    .overflow_y_scroll()
515                    .child(list(
516                        self.cell_list.clone(),
517                        cx.processor(|this, ix, window, cx| {
518                            this.cell_order
519                                .get(ix)
520                                .and_then(|cell_id| this.cell_map.get(cell_id))
521                                .map(|cell| {
522                                    this.render_cell(ix, cell, window, cx).into_any_element()
523                                })
524                                .unwrap_or_else(|| div().into_any())
525                        }),
526                    ))
527                    .size_full(),
528            )
529            .child(self.render_notebook_controls(window, cx))
530    }
531}
532
533impl Focusable for NotebookEditor {
534    fn focus_handle(&self, _: &App) -> FocusHandle {
535        self.focus_handle.clone()
536    }
537}
538
539// Intended to be a NotebookBuffer
540pub struct NotebookItem {
541    path: PathBuf,
542    project_path: ProjectPath,
543    languages: Arc<LanguageRegistry>,
544    // Raw notebook data
545    notebook: nbformat::v4::Notebook,
546    // Store our version of the notebook in memory (cell_order, cell_map)
547    id: ProjectEntryId,
548}
549
550impl project::ProjectItem for NotebookItem {
551    fn try_open(
552        project: &Entity<Project>,
553        path: &ProjectPath,
554        cx: &mut App,
555    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
556        let path = path.clone();
557        let project = project.clone();
558        let fs = project.read(cx).fs().clone();
559        let languages = project.read(cx).languages().clone();
560
561        if path.path.extension().unwrap_or_default() == "ipynb" {
562            Some(cx.spawn(async move |cx| {
563                let abs_path = project
564                    .read_with(cx, |project, cx| project.absolute_path(&path, cx))?
565                    .with_context(|| format!("finding the absolute path of {path:?}"))?;
566
567                // todo: watch for changes to the file
568                let file_content = fs.load(abs_path.as_path()).await?;
569                let notebook = nbformat::parse_notebook(&file_content);
570
571                let notebook = match notebook {
572                    Ok(nbformat::Notebook::V4(notebook)) => notebook,
573                    // 4.1 - 4.4 are converted to 4.5
574                    Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
575                        // TODO: Decide if we want to mutate the notebook by including Cell IDs
576                        // and any other conversions
577
578                        nbformat::upgrade_legacy_notebook(legacy_notebook)?
579                    }
580                    // Bad notebooks and notebooks v4.0 and below are not supported
581                    Err(e) => {
582                        anyhow::bail!("Failed to parse notebook: {:?}", e);
583                    }
584                };
585
586                let id = project
587                    .update(cx, |project, cx| {
588                        project.entry_for_path(&path, cx).map(|entry| entry.id)
589                    })?
590                    .context("Entry not found")?;
591
592                cx.new(|_| NotebookItem {
593                    path: abs_path,
594                    project_path: path,
595                    languages,
596                    notebook,
597                    id,
598                })
599            }))
600        } else {
601            None
602        }
603    }
604
605    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
606        Some(self.id)
607    }
608
609    fn project_path(&self, _: &App) -> Option<ProjectPath> {
610        Some(self.project_path.clone())
611    }
612
613    fn is_dirty(&self) -> bool {
614        false
615    }
616}
617
618impl NotebookItem {
619    pub fn language_name(&self) -> Option<String> {
620        self.notebook
621            .metadata
622            .language_info
623            .as_ref()
624            .map(|l| l.name.clone())
625            .or(self
626                .notebook
627                .metadata
628                .kernelspec
629                .as_ref()
630                .and_then(|spec| spec.language.clone()))
631    }
632
633    pub fn notebook_language(&self) -> impl Future<Output = Option<Arc<Language>>> + use<> {
634        let language_name = self.language_name();
635        let languages = self.languages.clone();
636
637        async move {
638            if let Some(language_name) = language_name {
639                languages.language_for_name(&language_name).await.ok()
640            } else {
641                None
642            }
643        }
644    }
645}
646
647impl EventEmitter<()> for NotebookEditor {}
648
649// pub struct NotebookControls {
650//     pane_focused: bool,
651//     active_item: Option<Box<dyn ItemHandle>>,
652//     // subscription: Option<Subscription>,
653// }
654
655// impl NotebookControls {
656//     pub fn new() -> Self {
657//         Self {
658//             pane_focused: false,
659//             active_item: Default::default(),
660//             // subscription: Default::default(),
661//         }
662//     }
663// }
664
665// impl EventEmitter<ToolbarItemEvent> for NotebookControls {}
666
667// impl Render for NotebookControls {
668//     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
669//         div().child("notebook controls")
670//     }
671// }
672
673// impl ToolbarItemView for NotebookControls {
674//     fn set_active_pane_item(
675//         &mut self,
676//         active_pane_item: Option<&dyn workspace::ItemHandle>,
677//         window: &mut Window, cx: &mut Context<Self>,
678//     ) -> workspace::ToolbarItemLocation {
679//         cx.notify();
680//         self.active_item = None;
681
682//         let Some(item) = active_pane_item else {
683//             return ToolbarItemLocation::Hidden;
684//         };
685
686//         ToolbarItemLocation::PrimaryLeft
687//     }
688
689//     fn pane_focus_update(&mut self, pane_focused: bool, _window: &mut Window, _cx: &mut Context<Self>) {
690//         self.pane_focused = pane_focused;
691//     }
692// }
693
694impl Item for NotebookEditor {
695    type Event = ();
696
697    fn can_split(&self) -> bool {
698        true
699    }
700
701    fn clone_on_split(
702        &self,
703        _workspace_id: Option<workspace::WorkspaceId>,
704        window: &mut Window,
705        cx: &mut Context<Self>,
706    ) -> Task<Option<Entity<Self>>>
707    where
708        Self: Sized,
709    {
710        Task::ready(Some(cx.new(|cx| {
711            Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)
712        })))
713    }
714
715    fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {
716        workspace::item::ItemBufferKind::Singleton
717    }
718
719    fn for_each_project_item(
720        &self,
721        cx: &App,
722        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
723    ) {
724        f(self.notebook_item.entity_id(), self.notebook_item.read(cx))
725    }
726
727    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
728        Label::new(self.tab_content_text(params.detail.unwrap_or(0), cx))
729            .single_line()
730            .color(params.text_color())
731            .when(params.preview, |this| this.italic())
732            .into_any_element()
733    }
734
735    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
736        let path = &self.notebook_item.read(cx).path;
737        let title = path
738            .file_name()
739            .unwrap_or_else(|| path.as_os_str())
740            .to_string_lossy()
741            .to_string();
742        title.into()
743    }
744
745    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
746        Some(IconName::Book.into())
747    }
748
749    fn show_toolbar(&self) -> bool {
750        false
751    }
752
753    // TODO
754    fn pixel_position_of_cursor(&self, _: &App) -> Option<Point<Pixels>> {
755        None
756    }
757
758    // TODO
759    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
760        None
761    }
762
763    fn set_nav_history(
764        &mut self,
765        _: workspace::ItemNavHistory,
766        _window: &mut Window,
767        _: &mut Context<Self>,
768    ) {
769        // TODO
770    }
771
772    // TODO
773    fn can_save(&self, _cx: &App) -> bool {
774        false
775    }
776    // TODO
777    fn save(
778        &mut self,
779        _options: SaveOptions,
780        _project: Entity<Project>,
781        _window: &mut Window,
782        _cx: &mut Context<Self>,
783    ) -> Task<Result<()>> {
784        unimplemented!("save() must be implemented if can_save() returns true")
785    }
786
787    // TODO
788    fn save_as(
789        &mut self,
790        _project: Entity<Project>,
791        _path: ProjectPath,
792        _window: &mut Window,
793        _cx: &mut Context<Self>,
794    ) -> Task<Result<()>> {
795        unimplemented!("save_as() must be implemented if can_save() returns true")
796    }
797    // TODO
798    fn reload(
799        &mut self,
800        _project: Entity<Project>,
801        _window: &mut Window,
802        _cx: &mut Context<Self>,
803    ) -> Task<Result<()>> {
804        unimplemented!("reload() must be implemented if can_save() returns true")
805    }
806
807    fn is_dirty(&self, cx: &App) -> bool {
808        self.cell_map.values().any(|cell| {
809            if let Cell::Code(code_cell) = cell {
810                code_cell.read(cx).is_dirty(cx)
811            } else {
812                false
813            }
814        })
815    }
816}
817
818// TODO: Implement this to allow us to persist to the database, etc:
819// impl SerializableItem for NotebookEditor {}
820
821impl ProjectItem for NotebookEditor {
822    type Item = NotebookItem;
823
824    fn for_project_item(
825        project: Entity<Project>,
826        _: Option<&Pane>,
827        item: Entity<Self::Item>,
828        window: &mut Window,
829        cx: &mut Context<Self>,
830    ) -> Self
831    where
832        Self: Sized,
833    {
834        Self::new(project, item, window, cx)
835    }
836}