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