notebook_ui.rs

  1#![allow(unused, dead_code)]
  2use std::future::Future;
  3use std::{path::PathBuf, sync::Arc};
  4
  5use anyhow::{Context as _, Result};
  6use client::proto::ViewId;
  7use collections::HashMap;
  8use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag};
  9use futures::FutureExt;
 10use futures::future::Shared;
 11use gpui::{
 12    AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, ListScrollEvent, ListState,
 13    Point, Task, actions, list, prelude::*,
 14};
 15use 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    const 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, window, 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(
345                                        "Clear all outputs",
346                                        &ClearOutputs,
347                                        window,
348                                        cx,
349                                    )
350                                })
351                                .on_click(|_, window, cx| {
352                                    window.dispatch_action(Box::new(ClearOutputs), cx);
353                                }),
354                            ),
355                    )
356                    .child(
357                        Self::button_group(window, cx)
358                            .child(
359                                Self::render_notebook_control(
360                                    "move-cell-up",
361                                    IconName::ArrowUp,
362                                    window,
363                                    cx,
364                                )
365                                .tooltip(move |window, cx| {
366                                    Tooltip::for_action("Move cell up", &MoveCellUp, window, cx)
367                                })
368                                .on_click(|_, window, cx| {
369                                    window.dispatch_action(Box::new(MoveCellUp), cx);
370                                }),
371                            )
372                            .child(
373                                Self::render_notebook_control(
374                                    "move-cell-down",
375                                    IconName::ArrowDown,
376                                    window,
377                                    cx,
378                                )
379                                .tooltip(move |window, cx| {
380                                    Tooltip::for_action("Move cell down", &MoveCellDown, window, cx)
381                                })
382                                .on_click(|_, window, cx| {
383                                    window.dispatch_action(Box::new(MoveCellDown), cx);
384                                }),
385                            ),
386                    )
387                    .child(
388                        Self::button_group(window, cx)
389                            .child(
390                                Self::render_notebook_control(
391                                    "new-markdown-cell",
392                                    IconName::Plus,
393                                    window,
394                                    cx,
395                                )
396                                .tooltip(move |window, cx| {
397                                    Tooltip::for_action(
398                                        "Add markdown block",
399                                        &AddMarkdownBlock,
400                                        window,
401                                        cx,
402                                    )
403                                })
404                                .on_click(|_, window, cx| {
405                                    window.dispatch_action(Box::new(AddMarkdownBlock), cx);
406                                }),
407                            )
408                            .child(
409                                Self::render_notebook_control(
410                                    "new-code-cell",
411                                    IconName::Code,
412                                    window,
413                                    cx,
414                                )
415                                .tooltip(move |window, cx| {
416                                    Tooltip::for_action("Add code block", &AddCodeBlock, window, cx)
417                                })
418                                .on_click(|_, window, cx| {
419                                    window.dispatch_action(Box::new(AddCodeBlock), cx);
420                                }),
421                            ),
422                    ),
423            )
424            .child(
425                v_flex()
426                    .gap(DynamicSpacing::Base08.rems(cx))
427                    .items_center()
428                    .child(Self::render_notebook_control(
429                        "more-menu",
430                        IconName::Ellipsis,
431                        window,
432                        cx,
433                    ))
434                    .child(
435                        Self::button_group(window, cx)
436                            .child(IconButton::new("repl", IconName::ReplNeutral)),
437                    ),
438            )
439    }
440
441    fn cell_position(&self, index: usize) -> CellPosition {
442        match index {
443            0 => CellPosition::First,
444            index if index == self.cell_count() - 1 => CellPosition::Last,
445            _ => CellPosition::Middle,
446        }
447    }
448
449    fn render_cell(
450        &self,
451        index: usize,
452        cell: &Cell,
453        window: &mut Window,
454        cx: &mut Context<Self>,
455    ) -> impl IntoElement {
456        let cell_position = self.cell_position(index);
457
458        let is_selected = index == self.selected_cell_index;
459
460        match cell {
461            Cell::Code(cell) => {
462                cell.update(cx, |cell, _cx| {
463                    cell.set_selected(is_selected)
464                        .set_cell_position(cell_position);
465                });
466                cell.clone().into_any_element()
467            }
468            Cell::Markdown(cell) => {
469                cell.update(cx, |cell, _cx| {
470                    cell.set_selected(is_selected)
471                        .set_cell_position(cell_position);
472                });
473                cell.clone().into_any_element()
474            }
475            Cell::Raw(cell) => {
476                cell.update(cx, |cell, _cx| {
477                    cell.set_selected(is_selected)
478                        .set_cell_position(cell_position);
479                });
480                cell.clone().into_any_element()
481            }
482        }
483    }
484}
485
486impl Render for NotebookEditor {
487    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
488        div()
489            .key_context("notebook")
490            .track_focus(&self.focus_handle)
491            .on_action(cx.listener(|this, &OpenNotebook, window, cx| {
492                this.open_notebook(&OpenNotebook, window, cx)
493            }))
494            .on_action(
495                cx.listener(|this, &ClearOutputs, window, cx| this.clear_outputs(window, cx)),
496            )
497            .on_action(cx.listener(|this, &RunAll, window, cx| this.run_cells(window, cx)))
498            .on_action(cx.listener(|this, &MoveCellUp, window, cx| this.move_cell_up(window, cx)))
499            .on_action(
500                cx.listener(|this, &MoveCellDown, window, cx| this.move_cell_down(window, cx)),
501            )
502            .on_action(cx.listener(|this, &AddMarkdownBlock, window, cx| {
503                this.add_markdown_block(window, cx)
504            }))
505            .on_action(
506                cx.listener(|this, &AddCodeBlock, window, cx| this.add_code_block(window, cx)),
507            )
508            .on_action(cx.listener(Self::select_next))
509            .on_action(cx.listener(Self::select_previous))
510            .on_action(cx.listener(Self::select_first))
511            .on_action(cx.listener(Self::select_last))
512            .flex()
513            .items_start()
514            .size_full()
515            .overflow_hidden()
516            .px(DynamicSpacing::Base12.px(cx))
517            .gap(DynamicSpacing::Base12.px(cx))
518            .bg(cx.theme().colors().tab_bar_background)
519            .child(
520                v_flex()
521                    .id("notebook-cells")
522                    .flex_1()
523                    .size_full()
524                    .overflow_y_scroll()
525                    .child(list(
526                        self.cell_list.clone(),
527                        cx.processor(|this, ix, window, cx| {
528                            this.cell_order
529                                .get(ix)
530                                .and_then(|cell_id| this.cell_map.get(cell_id))
531                                .map(|cell| {
532                                    this.render_cell(ix, cell, window, cx).into_any_element()
533                                })
534                                .unwrap_or_else(|| div().into_any())
535                        }),
536                    ))
537                    .size_full(),
538            )
539            .child(self.render_notebook_controls(window, cx))
540    }
541}
542
543impl Focusable for NotebookEditor {
544    fn focus_handle(&self, _: &App) -> FocusHandle {
545        self.focus_handle.clone()
546    }
547}
548
549// Intended to be a NotebookBuffer
550pub struct NotebookItem {
551    path: PathBuf,
552    project_path: ProjectPath,
553    languages: Arc<LanguageRegistry>,
554    // Raw notebook data
555    notebook: nbformat::v4::Notebook,
556    // Store our version of the notebook in memory (cell_order, cell_map)
557    id: ProjectEntryId,
558}
559
560impl project::ProjectItem for NotebookItem {
561    fn try_open(
562        project: &Entity<Project>,
563        path: &ProjectPath,
564        cx: &mut App,
565    ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
566        let path = path.clone();
567        let project = project.clone();
568        let fs = project.read(cx).fs().clone();
569        let languages = project.read(cx).languages().clone();
570
571        if path.path.extension().unwrap_or_default() == "ipynb" {
572            Some(cx.spawn(async move |cx| {
573                let abs_path = project
574                    .read_with(cx, |project, cx| project.absolute_path(&path, cx))?
575                    .with_context(|| format!("finding the absolute path of {path:?}"))?;
576
577                // todo: watch for changes to the file
578                let file_content = fs.load(abs_path.as_path()).await?;
579                let notebook = nbformat::parse_notebook(&file_content);
580
581                let notebook = match notebook {
582                    Ok(nbformat::Notebook::V4(notebook)) => notebook,
583                    // 4.1 - 4.4 are converted to 4.5
584                    Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
585                        // TODO: Decide if we want to mutate the notebook by including Cell IDs
586                        // and any other conversions
587
588                        nbformat::upgrade_legacy_notebook(legacy_notebook)?
589                    }
590                    // Bad notebooks and notebooks v4.0 and below are not supported
591                    Err(e) => {
592                        anyhow::bail!("Failed to parse notebook: {:?}", e);
593                    }
594                };
595
596                let id = project
597                    .update(cx, |project, cx| {
598                        project.entry_for_path(&path, cx).map(|entry| entry.id)
599                    })?
600                    .context("Entry not found")?;
601
602                cx.new(|_| NotebookItem {
603                    path: abs_path,
604                    project_path: path,
605                    languages,
606                    notebook,
607                    id,
608                })
609            }))
610        } else {
611            None
612        }
613    }
614
615    fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
616        Some(self.id)
617    }
618
619    fn project_path(&self, _: &App) -> Option<ProjectPath> {
620        Some(self.project_path.clone())
621    }
622
623    fn is_dirty(&self) -> bool {
624        false
625    }
626}
627
628impl NotebookItem {
629    pub fn language_name(&self) -> Option<String> {
630        self.notebook
631            .metadata
632            .language_info
633            .as_ref()
634            .map(|l| l.name.clone())
635            .or(self
636                .notebook
637                .metadata
638                .kernelspec
639                .as_ref()
640                .and_then(|spec| spec.language.clone()))
641    }
642
643    pub fn notebook_language(&self) -> impl Future<Output = Option<Arc<Language>>> + use<> {
644        let language_name = self.language_name();
645        let languages = self.languages.clone();
646
647        async move {
648            if let Some(language_name) = language_name {
649                languages.language_for_name(&language_name).await.ok()
650            } else {
651                None
652            }
653        }
654    }
655}
656
657impl EventEmitter<()> for NotebookEditor {}
658
659// pub struct NotebookControls {
660//     pane_focused: bool,
661//     active_item: Option<Box<dyn ItemHandle>>,
662//     // subscription: Option<Subscription>,
663// }
664
665// impl NotebookControls {
666//     pub fn new() -> Self {
667//         Self {
668//             pane_focused: false,
669//             active_item: Default::default(),
670//             // subscription: Default::default(),
671//         }
672//     }
673// }
674
675// impl EventEmitter<ToolbarItemEvent> for NotebookControls {}
676
677// impl Render for NotebookControls {
678//     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
679//         div().child("notebook controls")
680//     }
681// }
682
683// impl ToolbarItemView for NotebookControls {
684//     fn set_active_pane_item(
685//         &mut self,
686//         active_pane_item: Option<&dyn workspace::ItemHandle>,
687//         window: &mut Window, cx: &mut Context<Self>,
688//     ) -> workspace::ToolbarItemLocation {
689//         cx.notify();
690//         self.active_item = None;
691
692//         let Some(item) = active_pane_item else {
693//             return ToolbarItemLocation::Hidden;
694//         };
695
696//         ToolbarItemLocation::PrimaryLeft
697//     }
698
699//     fn pane_focus_update(&mut self, pane_focused: bool, _window: &mut Window, _cx: &mut Context<Self>) {
700//         self.pane_focused = pane_focused;
701//     }
702// }
703
704impl Item for NotebookEditor {
705    type Event = ();
706
707    fn clone_on_split(
708        &self,
709        _workspace_id: Option<workspace::WorkspaceId>,
710        window: &mut Window,
711        cx: &mut Context<Self>,
712    ) -> Option<Entity<Self>>
713    where
714        Self: Sized,
715    {
716        Some(cx.new(|cx| Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)))
717    }
718
719    fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {
720        workspace::item::ItemBufferKind::Singleton
721    }
722
723    fn for_each_project_item(
724        &self,
725        cx: &App,
726        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
727    ) {
728        f(self.notebook_item.entity_id(), self.notebook_item.read(cx))
729    }
730
731    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
732        Label::new(self.tab_content_text(params.detail.unwrap_or(0), cx))
733            .single_line()
734            .color(params.text_color())
735            .when(params.preview, |this| this.italic())
736            .into_any_element()
737    }
738
739    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
740        let path = &self.notebook_item.read(cx).path;
741        let title = path
742            .file_name()
743            .unwrap_or_else(|| path.as_os_str())
744            .to_string_lossy()
745            .to_string();
746        title.into()
747    }
748
749    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
750        Some(IconName::Book.into())
751    }
752
753    fn show_toolbar(&self) -> bool {
754        false
755    }
756
757    // TODO
758    fn pixel_position_of_cursor(&self, _: &App) -> Option<Point<Pixels>> {
759        None
760    }
761
762    // TODO
763    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
764        None
765    }
766
767    fn set_nav_history(
768        &mut self,
769        _: workspace::ItemNavHistory,
770        _window: &mut Window,
771        _: &mut Context<Self>,
772    ) {
773        // TODO
774    }
775
776    // TODO
777    fn can_save(&self, _cx: &App) -> bool {
778        false
779    }
780    // TODO
781    fn save(
782        &mut self,
783        _options: SaveOptions,
784        _project: Entity<Project>,
785        _window: &mut Window,
786        _cx: &mut Context<Self>,
787    ) -> Task<Result<()>> {
788        unimplemented!("save() must be implemented if can_save() returns true")
789    }
790
791    // TODO
792    fn save_as(
793        &mut self,
794        _project: Entity<Project>,
795        _path: ProjectPath,
796        _window: &mut Window,
797        _cx: &mut Context<Self>,
798    ) -> Task<Result<()>> {
799        unimplemented!("save_as() must be implemented if can_save() returns true")
800    }
801    // TODO
802    fn reload(
803        &mut self,
804        _project: Entity<Project>,
805        _window: &mut Window,
806        _cx: &mut Context<Self>,
807    ) -> Task<Result<()>> {
808        unimplemented!("reload() must be implemented if can_save() returns true")
809    }
810
811    fn is_dirty(&self, cx: &App) -> bool {
812        self.cell_map.values().any(|cell| {
813            if let Cell::Code(code_cell) = cell {
814                code_cell.read(cx).is_dirty(cx)
815            } else {
816                false
817            }
818        })
819    }
820}
821
822// TODO: Implement this to allow us to persist to the database, etc:
823// impl SerializableItem for NotebookEditor {}
824
825impl ProjectItem for NotebookEditor {
826    type Item = NotebookItem;
827
828    fn for_project_item(
829        project: Entity<Project>,
830        _: Option<&Pane>,
831        item: Entity<Self::Item>,
832        window: &mut Window,
833        cx: &mut Context<Self>,
834    ) -> Self
835    where
836        Self: Sized,
837    {
838        Self::new(project, item, window, cx)
839    }
840}