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