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}