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::future::Shared;
 10use futures::FutureExt;
 11use gpui::{
 12    actions, list, prelude::*, AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable,
 13    ListScrollEvent, ListState, Point, Task,
 14};
 15use language::{Language, LanguageRegistry};
 16use project::{Project, ProjectEntryId, ProjectPath};
 17use ui::{prelude::*, Tooltip};
 18use workspace::item::{ItemEvent, TabContentParams};
 19use workspace::searchable::SearchableItemHandle;
 20use workspace::{Item, ItemHandle, 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        OpenNotebook,
 32        RunAll,
 33        ClearOutputs,
 34        MoveCellUp,
 35        MoveCellDown,
 36        AddMarkdownBlock,
 37        AddCodeBlock,
 38    ]
 39);
 40
 41pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0;
 42pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0;
 43pub(crate) const MEDIUM_SPACING_SIZE: f32 = 12.0;
 44pub(crate) const LARGE_SPACING_SIZE: f32 = 16.0;
 45pub(crate) const GUTTER_WIDTH: f32 = 19.0;
 46pub(crate) const CODE_BLOCK_INSET: f32 = MEDIUM_SPACING_SIZE;
 47pub(crate) const CONTROL_SIZE: f32 = 20.0;
 48
 49pub fn init(cx: &mut App) {
 50    if cx.has_flag::<NotebookFeatureFlag>() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() {
 51        workspace::register_project_item::<NotebookEditor>(cx);
 52    }
 53
 54    cx.observe_flag::<NotebookFeatureFlag, _>({
 55        move |is_enabled, cx| {
 56            if is_enabled {
 57                workspace::register_project_item::<NotebookEditor>(cx);
 58            } else {
 59                // todo: there is no way to unregister a project item, so if the feature flag
 60                // gets turned off they need to restart Zed.
 61            }
 62        }
 63    })
 64    .detach();
 65}
 66
 67pub struct NotebookEditor {
 68    languages: Arc<LanguageRegistry>,
 69    project: Entity<Project>,
 70
 71    focus_handle: FocusHandle,
 72    notebook_item: Entity<NotebookItem>,
 73
 74    remote_id: Option<ViewId>,
 75    cell_list: ListState,
 76
 77    selected_cell_index: usize,
 78    cell_order: Vec<CellId>,
 79    cell_map: HashMap<CellId, Cell>,
 80}
 81
 82impl NotebookEditor {
 83    pub fn new(
 84        project: Entity<Project>,
 85        notebook_item: Entity<NotebookItem>,
 86        window: &mut Window,
 87        cx: &mut Context<Self>,
 88    ) -> Self {
 89        let focus_handle = cx.focus_handle();
 90
 91        let languages = project.read(cx).languages().clone();
 92        let language_name = notebook_item.read(cx).language_name();
 93
 94        let notebook_language = notebook_item.read(cx).notebook_language();
 95        let notebook_language = cx.spawn_in(window, |_, _| notebook_language).shared();
 96
 97        let mut cell_order = vec![]; // Vec<CellId>
 98        let mut cell_map = HashMap::default(); // HashMap<CellId, Cell>
 99
100        for (index, cell) in notebook_item
101            .read(cx)
102            .notebook
103            .clone()
104            .cells
105            .iter()
106            .enumerate()
107        {
108            let cell_id = cell.id();
109            cell_order.push(cell_id.clone());
110            cell_map.insert(
111                cell_id.clone(),
112                Cell::load(cell, &languages, notebook_language.clone(), window, cx),
113            );
114        }
115
116        let notebook_handle = cx.entity().downgrade();
117        let cell_count = cell_order.len();
118
119        let this = cx.entity();
120        let cell_list = ListState::new(
121            cell_count,
122            gpui::ListAlignment::Top,
123            px(1000.),
124            move |ix, window, cx| {
125                notebook_handle
126                    .upgrade()
127                    .and_then(|notebook_handle| {
128                        notebook_handle.update(cx, |notebook, cx| {
129                            notebook
130                                .cell_order
131                                .get(ix)
132                                .and_then(|cell_id| notebook.cell_map.get(cell_id))
133                                .map(|cell| {
134                                    notebook
135                                        .render_cell(ix, cell, window, cx)
136                                        .into_any_element()
137                                })
138                        })
139                    })
140                    .unwrap_or_else(|| div().into_any())
141            },
142        );
143
144        Self {
145            project,
146            languages: languages.clone(),
147            focus_handle,
148            notebook_item,
149            remote_id: None,
150            cell_list,
151            selected_cell_index: 0,
152            cell_order: cell_order.clone(),
153            cell_map: cell_map.clone(),
154        }
155    }
156
157    fn has_outputs(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
158        self.cell_map.values().any(|cell| {
159            if let Cell::Code(code_cell) = cell {
160                code_cell.read(cx).has_outputs()
161            } else {
162                false
163            }
164        })
165    }
166
167    fn clear_outputs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
168        for cell in self.cell_map.values() {
169            if let Cell::Code(code_cell) = cell {
170                code_cell.update(cx, |cell, _cx| {
171                    cell.clear_outputs();
172                });
173            }
174        }
175    }
176
177    fn run_cells(&mut self, window: &mut Window, cx: &mut Context<Self>) {
178        println!("Cells would all run here, if that was implemented!");
179    }
180
181    fn open_notebook(&mut self, _: &OpenNotebook, _window: &mut Window, _cx: &mut Context<Self>) {
182        println!("Open notebook triggered");
183    }
184
185    fn move_cell_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
186        println!("Move cell up triggered");
187    }
188
189    fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
190        println!("Move cell down triggered");
191    }
192
193    fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
194        println!("Add markdown block triggered");
195    }
196
197    fn add_code_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
198        println!("Add code block triggered");
199    }
200
201    fn cell_count(&self) -> usize {
202        self.cell_map.len()
203    }
204
205    fn selected_index(&self) -> usize {
206        self.selected_cell_index
207    }
208
209    pub fn set_selected_index(
210        &mut self,
211        index: usize,
212        jump_to_index: bool,
213        window: &mut Window,
214        cx: &mut Context<Self>,
215    ) {
216        // let previous_index = self.selected_cell_index;
217        self.selected_cell_index = index;
218        let current_index = self.selected_cell_index;
219
220        // in the future we may have some `on_cell_change` event that we want to fire here
221
222        if jump_to_index {
223            self.jump_to_cell(current_index, window, cx);
224        }
225    }
226
227    pub fn select_next(
228        &mut self,
229        _: &menu::SelectNext,
230        window: &mut Window,
231        cx: &mut Context<Self>,
232    ) {
233        let count = self.cell_count();
234        if count > 0 {
235            let index = self.selected_index();
236            let ix = if index == count - 1 {
237                count - 1
238            } else {
239                index + 1
240            };
241            self.set_selected_index(ix, true, window, cx);
242            cx.notify();
243        }
244    }
245
246    pub fn select_previous(
247        &mut self,
248        _: &menu::SelectPrevious,
249        window: &mut Window,
250        cx: &mut Context<Self>,
251    ) {
252        let count = self.cell_count();
253        if count > 0 {
254            let index = self.selected_index();
255            let ix = if index == 0 { 0 } else { index - 1 };
256            self.set_selected_index(ix, true, window, cx);
257            cx.notify();
258        }
259    }
260
261    pub fn select_first(
262        &mut self,
263        _: &menu::SelectFirst,
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(0, true, window, cx);
270            cx.notify();
271        }
272    }
273
274    pub fn select_last(
275        &mut self,
276        _: &menu::SelectLast,
277        window: &mut Window,
278        cx: &mut Context<Self>,
279    ) {
280        let count = self.cell_count();
281        if count > 0 {
282            self.set_selected_index(count - 1, true, window, cx);
283            cx.notify();
284        }
285    }
286
287    fn jump_to_cell(&mut self, index: usize, _window: &mut Window, _cx: &mut Context<Self>) {
288        self.cell_list.scroll_to_reveal_item(index);
289    }
290
291    fn button_group(window: &mut Window, cx: &mut Context<Self>) -> Div {
292        v_flex()
293            .gap(DynamicSpacing::Base04.rems(cx))
294            .items_center()
295            .w(px(CONTROL_SIZE + 4.0))
296            .overflow_hidden()
297            .rounded(px(5.))
298            .bg(cx.theme().colors().title_bar_background)
299            .p_px()
300            .border_1()
301            .border_color(cx.theme().colors().border)
302    }
303
304    fn render_notebook_control(
305        id: impl Into<SharedString>,
306        icon: IconName,
307        _window: &mut Window,
308        _cx: &mut Context<Self>,
309    ) -> IconButton {
310        let id: ElementId = ElementId::Name(id.into());
311        IconButton::new(id, icon).width(px(CONTROL_SIZE).into())
312    }
313
314    fn render_notebook_controls(
315        &self,
316        window: &mut Window,
317        cx: &mut Context<Self>,
318    ) -> impl IntoElement {
319        let has_outputs = self.has_outputs(window, cx);
320
321        v_flex()
322            .max_w(px(CONTROL_SIZE + 4.0))
323            .items_center()
324            .gap(DynamicSpacing::Base16.rems(cx))
325            .justify_between()
326            .flex_none()
327            .h_full()
328            .py(DynamicSpacing::Base12.px(cx))
329            .child(
330                v_flex()
331                    .gap(DynamicSpacing::Base08.rems(cx))
332                    .child(
333                        Self::button_group(window, cx)
334                            .child(
335                                Self::render_notebook_control(
336                                    "run-all-cells",
337                                    IconName::Play,
338                                    window,
339                                    cx,
340                                )
341                                .tooltip(move |window, cx| {
342                                    Tooltip::for_action("Execute all cells", &RunAll, window, cx)
343                                })
344                                .on_click(|_, window, cx| {
345                                    window.dispatch_action(Box::new(RunAll), cx);
346                                }),
347                            )
348                            .child(
349                                Self::render_notebook_control(
350                                    "clear-all-outputs",
351                                    IconName::ListX,
352                                    window,
353                                    cx,
354                                )
355                                .disabled(!has_outputs)
356                                .tooltip(move |window, cx| {
357                                    Tooltip::for_action(
358                                        "Clear all outputs",
359                                        &ClearOutputs,
360                                        window,
361                                        cx,
362                                    )
363                                })
364                                .on_click(|_, window, cx| {
365                                    window.dispatch_action(Box::new(ClearOutputs), cx);
366                                }),
367                            ),
368                    )
369                    .child(
370                        Self::button_group(window, cx)
371                            .child(
372                                Self::render_notebook_control(
373                                    "move-cell-up",
374                                    IconName::ArrowUp,
375                                    window,
376                                    cx,
377                                )
378                                .tooltip(move |window, cx| {
379                                    Tooltip::for_action("Move cell up", &MoveCellUp, window, cx)
380                                })
381                                .on_click(|_, window, cx| {
382                                    window.dispatch_action(Box::new(MoveCellUp), cx);
383                                }),
384                            )
385                            .child(
386                                Self::render_notebook_control(
387                                    "move-cell-down",
388                                    IconName::ArrowDown,
389                                    window,
390                                    cx,
391                                )
392                                .tooltip(move |window, cx| {
393                                    Tooltip::for_action("Move cell down", &MoveCellDown, window, cx)
394                                })
395                                .on_click(|_, window, cx| {
396                                    window.dispatch_action(Box::new(MoveCellDown), cx);
397                                }),
398                            ),
399                    )
400                    .child(
401                        Self::button_group(window, cx)
402                            .child(
403                                Self::render_notebook_control(
404                                    "new-markdown-cell",
405                                    IconName::Plus,
406                                    window,
407                                    cx,
408                                )
409                                .tooltip(move |window, cx| {
410                                    Tooltip::for_action(
411                                        "Add markdown block",
412                                        &AddMarkdownBlock,
413                                        window,
414                                        cx,
415                                    )
416                                })
417                                .on_click(|_, window, cx| {
418                                    window.dispatch_action(Box::new(AddMarkdownBlock), cx);
419                                }),
420                            )
421                            .child(
422                                Self::render_notebook_control(
423                                    "new-code-cell",
424                                    IconName::Code,
425                                    window,
426                                    cx,
427                                )
428                                .tooltip(move |window, cx| {
429                                    Tooltip::for_action("Add code block", &AddCodeBlock, window, cx)
430                                })
431                                .on_click(|_, window, cx| {
432                                    window.dispatch_action(Box::new(AddCodeBlock), cx);
433                                }),
434                            ),
435                    ),
436            )
437            .child(
438                v_flex()
439                    .gap(DynamicSpacing::Base08.rems(cx))
440                    .items_center()
441                    .child(Self::render_notebook_control(
442                        "more-menu",
443                        IconName::Ellipsis,
444                        window,
445                        cx,
446                    ))
447                    .child(
448                        Self::button_group(window, cx)
449                            .child(IconButton::new("repl", IconName::ReplNeutral)),
450                    ),
451            )
452    }
453
454    fn cell_position(&self, index: usize) -> CellPosition {
455        match index {
456            0 => CellPosition::First,
457            index if index == self.cell_count() - 1 => CellPosition::Last,
458            _ => CellPosition::Middle,
459        }
460    }
461
462    fn render_cell(
463        &self,
464        index: usize,
465        cell: &Cell,
466        window: &mut Window,
467        cx: &mut Context<Self>,
468    ) -> impl IntoElement {
469        let cell_position = self.cell_position(index);
470
471        let is_selected = index == self.selected_cell_index;
472
473        match cell {
474            Cell::Code(cell) => {
475                cell.update(cx, |cell, _cx| {
476                    cell.set_selected(is_selected)
477                        .set_cell_position(cell_position);
478                });
479                cell.clone().into_any_element()
480            }
481            Cell::Markdown(cell) => {
482                cell.update(cx, |cell, _cx| {
483                    cell.set_selected(is_selected)
484                        .set_cell_position(cell_position);
485                });
486                cell.clone().into_any_element()
487            }
488            Cell::Raw(cell) => {
489                cell.update(cx, |cell, _cx| {
490                    cell.set_selected(is_selected)
491                        .set_cell_position(cell_position);
492                });
493                cell.clone().into_any_element()
494            }
495        }
496    }
497}
498
499impl Render for NotebookEditor {
500    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
501        div()
502            .key_context("notebook")
503            .track_focus(&self.focus_handle)
504            .on_action(cx.listener(|this, &OpenNotebook, window, cx| {
505                this.open_notebook(&OpenNotebook, window, cx)
506            }))
507            .on_action(
508                cx.listener(|this, &ClearOutputs, window, cx| this.clear_outputs(window, cx)),
509            )
510            .on_action(cx.listener(|this, &RunAll, window, cx| this.run_cells(window, cx)))
511            .on_action(cx.listener(|this, &MoveCellUp, window, cx| this.move_cell_up(window, cx)))
512            .on_action(
513                cx.listener(|this, &MoveCellDown, window, cx| this.move_cell_down(window, cx)),
514            )
515            .on_action(cx.listener(|this, &AddMarkdownBlock, window, cx| {
516                this.add_markdown_block(window, cx)
517            }))
518            .on_action(
519                cx.listener(|this, &AddCodeBlock, window, cx| this.add_code_block(window, cx)),
520            )
521            .on_action(cx.listener(Self::select_next))
522            .on_action(cx.listener(Self::select_previous))
523            .on_action(cx.listener(Self::select_first))
524            .on_action(cx.listener(Self::select_last))
525            .flex()
526            .items_start()
527            .size_full()
528            .overflow_hidden()
529            .px(DynamicSpacing::Base12.px(cx))
530            .gap(DynamicSpacing::Base12.px(cx))
531            .bg(cx.theme().colors().tab_bar_background)
532            .child(
533                v_flex()
534                    .id("notebook-cells")
535                    .flex_1()
536                    .size_full()
537                    .overflow_y_scroll()
538                    .child(list(self.cell_list.clone()).size_full()),
539            )
540            .child(self.render_notebook_controls(window, cx))
541    }
542}
543
544impl Focusable for NotebookEditor {
545    fn focus_handle(&self, _: &App) -> FocusHandle {
546        self.focus_handle.clone()
547    }
548}
549
550// Intended to be a NotebookBuffer
551pub struct NotebookItem {
552    path: PathBuf,
553    project_path: ProjectPath,
554    languages: Arc<LanguageRegistry>,
555    // Raw notebook data
556    notebook: nbformat::v4::Notebook,
557    // Store our version of the notebook in memory (cell_order, cell_map)
558    id: ProjectEntryId,
559}
560
561impl project::ProjectItem for NotebookItem {
562    fn try_open(
563        project: &Entity<Project>,
564        path: &ProjectPath,
565        cx: &mut App,
566    ) -> Option<Task<gpui::Result<Entity<Self>>>> {
567        let path = path.clone();
568        let project = project.clone();
569        let fs = project.read(cx).fs().clone();
570        let languages = project.read(cx).languages().clone();
571
572        if path.path.extension().unwrap_or_default() == "ipynb" {
573            Some(cx.spawn(|mut cx| async move {
574                let abs_path = project
575                    .read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
576                    .ok_or_else(|| anyhow::anyhow!("Failed to find the absolute path"))?;
577
578                // todo: watch for changes to the file
579                let file_content = fs.load(&abs_path.as_path()).await?;
580                let notebook = nbformat::parse_notebook(&file_content);
581
582                let notebook = match notebook {
583                    Ok(nbformat::Notebook::V4(notebook)) => notebook,
584                    // 4.1 - 4.4 are converted to 4.5
585                    Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
586                        // TODO: Decide if we want to mutate the notebook by including Cell IDs
587                        // and any other conversions
588                        let notebook = nbformat::upgrade_legacy_notebook(legacy_notebook)?;
589                        notebook
590                    }
591                    // Bad notebooks and notebooks v4.0 and below are not supported
592                    Err(e) => {
593                        anyhow::bail!("Failed to parse notebook: {:?}", e);
594                    }
595                };
596
597                let id = project
598                    .update(&mut cx, |project, cx| project.entry_for_path(&path, cx))?
599                    .context("Entry not found")?
600                    .id;
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>>> {
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 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 is_singleton(&self, _cx: &App) -> bool {
728        true
729    }
730
731    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
732        let path = &self.notebook_item.read(cx).path;
733        let title = path
734            .file_name()
735            .unwrap_or_else(|| path.as_os_str())
736            .to_string_lossy()
737            .to_string();
738        Label::new(title)
739            .single_line()
740            .color(params.text_color())
741            .when(params.preview, |this| this.italic())
742            .into_any_element()
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        _format: bool,
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        item: Entity<Self::Item>,
827        window: &mut Window,
828        cx: &mut Context<Self>,
829    ) -> Self
830    where
831        Self: Sized,
832    {
833        Self::new(project, item, window, cx)
834    }
835}