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}