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 jupyter_protocol::JupyterKernelspec;
16use language::{Language, LanguageRegistry};
17use project::{Project, ProjectEntryId, ProjectPath};
18use settings::Settings as _;
19use ui::{CommonAnimationExt, Tooltip, prelude::*};
20use workspace::item::{ItemEvent, SaveOptions, TabContentParams};
21use workspace::searchable::SearchableItemHandle;
22use workspace::{Item, ItemHandle, Pane, ProjectItem, ToolbarItemLocation};
23
24use super::{Cell, CellEvent, CellPosition, MarkdownCellEvent, RenderableCell};
25
26use nbformat::v4::CellId;
27use nbformat::v4::Metadata as NotebookMetadata;
28use serde_json;
29use uuid::Uuid;
30
31use crate::components::{KernelPickerDelegate, KernelSelector};
32use crate::kernels::{
33 Kernel, KernelSession, KernelSpecification, KernelStatus, LocalKernelSpecification,
34 NativeRunningKernel, RemoteRunningKernel,
35};
36use crate::repl_store::ReplStore;
37use picker::Picker;
38use runtimelib::{ExecuteRequest, JupyterMessage, JupyterMessageContent};
39use ui::PopoverMenuHandle;
40
41actions!(
42 notebook,
43 [
44 /// Opens a Jupyter notebook file.
45 OpenNotebook,
46 /// Runs all cells in the notebook.
47 RunAll,
48 /// Runs the current cell.
49 Run,
50 /// Clears all cell outputs.
51 ClearOutputs,
52 /// Moves the current cell up.
53 MoveCellUp,
54 /// Moves the current cell down.
55 MoveCellDown,
56 /// Adds a new markdown cell.
57 AddMarkdownBlock,
58 /// Adds a new code cell.
59 AddCodeBlock,
60 /// Restarts the kernel.
61 RestartKernel,
62 /// Interrupts the current execution.
63 InterruptKernel,
64 ]
65);
66
67pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0;
68pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0;
69pub(crate) const MEDIUM_SPACING_SIZE: f32 = 12.0;
70pub(crate) const LARGE_SPACING_SIZE: f32 = 16.0;
71pub(crate) const GUTTER_WIDTH: f32 = 19.0;
72pub(crate) const CODE_BLOCK_INSET: f32 = MEDIUM_SPACING_SIZE;
73pub(crate) const CONTROL_SIZE: f32 = 20.0;
74
75pub fn init(cx: &mut App) {
76 if cx.has_flag::<NotebookFeatureFlag>() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() {
77 workspace::register_project_item::<NotebookEditor>(cx);
78 }
79
80 cx.observe_flag::<NotebookFeatureFlag, _>({
81 move |is_enabled, cx| {
82 if is_enabled {
83 workspace::register_project_item::<NotebookEditor>(cx);
84 } else {
85 // todo: there is no way to unregister a project item, so if the feature flag
86 // gets turned off they need to restart Zed.
87 }
88 }
89 })
90 .detach();
91}
92
93pub struct NotebookEditor {
94 languages: Arc<LanguageRegistry>,
95 project: Entity<Project>,
96 worktree_id: project::WorktreeId,
97
98 focus_handle: FocusHandle,
99 notebook_item: Entity<NotebookItem>,
100 notebook_language: Shared<Task<Option<Arc<Language>>>>,
101
102 remote_id: Option<ViewId>,
103 cell_list: ListState,
104
105 selected_cell_index: usize,
106 cell_order: Vec<CellId>,
107 original_cell_order: Vec<CellId>,
108 cell_map: HashMap<CellId, Cell>,
109 kernel: Kernel,
110 kernel_specification: Option<KernelSpecification>,
111 execution_requests: HashMap<String, CellId>,
112 kernel_picker_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>,
113}
114
115impl NotebookEditor {
116 pub fn new(
117 project: Entity<Project>,
118 notebook_item: Entity<NotebookItem>,
119 window: &mut Window,
120 cx: &mut Context<Self>,
121 ) -> Self {
122 let focus_handle = cx.focus_handle();
123
124 let languages = project.read(cx).languages().clone();
125 let language_name = notebook_item.read(cx).language_name();
126 let worktree_id = notebook_item.read(cx).project_path.worktree_id;
127
128 let notebook_language = notebook_item.read(cx).notebook_language();
129 let notebook_language = cx
130 .spawn_in(window, async move |_, _| notebook_language.await)
131 .shared();
132
133 let mut cell_order = vec![]; // Vec<CellId>
134 let mut cell_map = HashMap::default(); // HashMap<CellId, Cell>
135
136 for (index, cell) in notebook_item
137 .read(cx)
138 .notebook
139 .clone()
140 .cells
141 .iter()
142 .enumerate()
143 {
144 let cell_id = cell.id();
145 cell_order.push(cell_id.clone());
146 let cell_entity = Cell::load(cell, &languages, notebook_language.clone(), window, cx);
147
148 match &cell_entity {
149 Cell::Code(code_cell) => {
150 let cell_id_for_focus = cell_id.clone();
151 cx.subscribe(code_cell, move |this, cell, event, cx| match event {
152 CellEvent::Run(cell_id) => this.execute_cell(cell_id.clone(), cx),
153 CellEvent::FocusedIn(_) => {
154 if let Some(index) = this
155 .cell_order
156 .iter()
157 .position(|id| id == &cell_id_for_focus)
158 {
159 this.selected_cell_index = index;
160 cx.notify();
161 }
162 }
163 })
164 .detach();
165
166 let cell_id_for_editor = cell_id.clone();
167 let editor = code_cell.read(cx).editor().clone();
168 cx.subscribe(&editor, move |this, _editor, event, cx| {
169 if let editor::EditorEvent::Focused = event {
170 if let Some(index) = this
171 .cell_order
172 .iter()
173 .position(|id| id == &cell_id_for_editor)
174 {
175 this.selected_cell_index = index;
176 cx.notify();
177 }
178 }
179 })
180 .detach();
181 }
182 Cell::Markdown(markdown_cell) => {
183 let cell_id_for_focus = cell_id.clone();
184 cx.subscribe(
185 markdown_cell,
186 move |_this, cell, event: &MarkdownCellEvent, cx| {
187 match event {
188 MarkdownCellEvent::FinishedEditing => {
189 cell.update(cx, |cell, cx| {
190 cell.reparse_markdown(cx);
191 });
192 }
193 MarkdownCellEvent::Run(_cell_id) => {
194 // run is handled separately by move_to_next_cell
195 // Just reparse here
196 cell.update(cx, |cell, cx| {
197 cell.reparse_markdown(cx);
198 });
199 }
200 }
201 },
202 )
203 .detach();
204
205 let cell_id_for_editor = cell_id.clone();
206 let editor = markdown_cell.read(cx).editor().clone();
207 cx.subscribe(&editor, move |this, _editor, event, cx| {
208 if let editor::EditorEvent::Focused = event {
209 if let Some(index) = this
210 .cell_order
211 .iter()
212 .position(|id| id == &cell_id_for_editor)
213 {
214 this.selected_cell_index = index;
215 cx.notify();
216 }
217 }
218 })
219 .detach();
220 }
221 Cell::Raw(_) => {}
222 }
223
224 cell_map.insert(cell_id.clone(), cell_entity);
225 }
226
227 let notebook_handle = cx.entity().downgrade();
228 let cell_count = cell_order.len();
229
230 let this = cx.entity();
231 let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.));
232
233 let mut editor = Self {
234 project,
235 languages: languages.clone(),
236 worktree_id,
237 focus_handle,
238 notebook_item,
239 notebook_language,
240 remote_id: None,
241 cell_list,
242 selected_cell_index: 0,
243 cell_order: cell_order.clone(),
244 original_cell_order: cell_order.clone(),
245 cell_map: cell_map.clone(),
246 kernel: Kernel::StartingKernel(Task::ready(()).shared()),
247 kernel_specification: None,
248 execution_requests: HashMap::default(),
249 kernel_picker_handle: PopoverMenuHandle::default(),
250 };
251 editor.launch_kernel(window, cx);
252 editor
253 }
254
255 fn has_structural_changes(&self) -> bool {
256 self.cell_order != self.original_cell_order
257 }
258
259 fn has_content_changes(&self, cx: &App) -> bool {
260 self.cell_map.values().any(|cell| cell.is_dirty(cx))
261 }
262
263 pub fn to_notebook(&self, cx: &App) -> nbformat::v4::Notebook {
264 let cells: Vec<nbformat::v4::Cell> = self
265 .cell_order
266 .iter()
267 .filter_map(|cell_id| {
268 self.cell_map
269 .get(cell_id)
270 .map(|cell| cell.to_nbformat_cell(cx))
271 })
272 .collect();
273
274 let metadata = self.notebook_item.read(cx).notebook.metadata.clone();
275
276 nbformat::v4::Notebook {
277 metadata,
278 nbformat: 4,
279 nbformat_minor: 5,
280 cells,
281 }
282 }
283
284 pub fn mark_as_saved(&mut self, cx: &mut Context<Self>) {
285 self.original_cell_order = self.cell_order.clone();
286
287 for cell in self.cell_map.values() {
288 match cell {
289 Cell::Code(code_cell) => {
290 code_cell.update(cx, |code_cell, cx| {
291 let editor = code_cell.editor();
292 editor.update(cx, |editor, cx| {
293 editor.buffer().update(cx, |buffer, cx| {
294 if let Some(buf) = buffer.as_singleton() {
295 buf.update(cx, |b, cx| {
296 let version = b.version();
297 b.did_save(version, None, cx);
298 });
299 }
300 });
301 });
302 });
303 }
304 Cell::Markdown(markdown_cell) => {
305 markdown_cell.update(cx, |markdown_cell, cx| {
306 let editor = markdown_cell.editor();
307 editor.update(cx, |editor, cx| {
308 editor.buffer().update(cx, |buffer, cx| {
309 if let Some(buf) = buffer.as_singleton() {
310 buf.update(cx, |b, cx| {
311 let version = b.version();
312 b.did_save(version, None, cx);
313 });
314 }
315 });
316 });
317 });
318 }
319 Cell::Raw(_) => {}
320 }
321 }
322 cx.notify();
323 }
324
325 fn launch_kernel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
326 // use default Python kernel if no specification is set
327 let spec = self.kernel_specification.clone().unwrap_or_else(|| {
328 KernelSpecification::Jupyter(LocalKernelSpecification {
329 name: "python3".to_string(),
330 path: PathBuf::from("python3"),
331 kernelspec: JupyterKernelspec {
332 argv: vec![
333 "python3".to_string(),
334 "-m".to_string(),
335 "ipykernel_launcher".to_string(),
336 "-f".to_string(),
337 "{connection_file}".to_string(),
338 ],
339 display_name: "Python 3".to_string(),
340 language: "python".to_string(),
341 interrupt_mode: None,
342 metadata: None,
343 env: None,
344 },
345 })
346 });
347
348 self.launch_kernel_with_spec(spec, window, cx);
349 }
350
351 fn launch_kernel_with_spec(
352 &mut self,
353 spec: KernelSpecification,
354 window: &mut Window,
355 cx: &mut Context<Self>,
356 ) {
357 let entity_id = cx.entity_id();
358 let working_directory = self
359 .project
360 .read(cx)
361 .worktrees(cx)
362 .next()
363 .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
364 .unwrap_or_else(std::env::temp_dir);
365 let fs = self.project.read(cx).fs().clone();
366 let view = cx.entity();
367
368 self.kernel_specification = Some(spec.clone());
369
370 let kernel_task = match spec {
371 KernelSpecification::Jupyter(local_spec)
372 | KernelSpecification::PythonEnv(local_spec) => NativeRunningKernel::new(
373 local_spec,
374 entity_id,
375 working_directory,
376 fs,
377 view,
378 window,
379 cx,
380 ),
381 KernelSpecification::Remote(remote_spec) => {
382 RemoteRunningKernel::new(remote_spec, working_directory, view, window, cx)
383 }
384 };
385
386 let pending_kernel = cx
387 .spawn(async move |this, cx| {
388 let kernel = kernel_task.await;
389
390 match kernel {
391 Ok(kernel) => {
392 this.update(cx, |editor, cx| {
393 editor.kernel = Kernel::RunningKernel(kernel);
394 cx.notify();
395 })
396 .ok();
397 }
398 Err(err) => {
399 this.update(cx, |editor, cx| {
400 editor.kernel = Kernel::ErroredLaunch(err.to_string());
401 cx.notify();
402 })
403 .ok();
404 }
405 }
406 })
407 .shared();
408
409 self.kernel = Kernel::StartingKernel(pending_kernel);
410 cx.notify();
411 }
412
413 // Note: Python environments are only detected as kernels if ipykernel is installed.
414 // Users need to run `pip install ipykernel` (or `uv pip install ipykernel`) in their
415 // virtual environment for it to appear in the kernel selector.
416 // This happens because we have an ipykernel check inside the function python_env_kernel_specification in mod.rs L:121
417
418 fn change_kernel(
419 &mut self,
420 spec: KernelSpecification,
421 window: &mut Window,
422 cx: &mut Context<Self>,
423 ) {
424 if let Kernel::RunningKernel(kernel) = &mut self.kernel {
425 kernel.force_shutdown(window, cx).detach();
426 }
427
428 self.execution_requests.clear();
429
430 self.launch_kernel_with_spec(spec, window, cx);
431 }
432
433 fn restart_kernel(&mut self, _: &RestartKernel, window: &mut Window, cx: &mut Context<Self>) {
434 if let Some(spec) = self.kernel_specification.clone() {
435 if let Kernel::RunningKernel(kernel) = &mut self.kernel {
436 kernel.force_shutdown(window, cx).detach();
437 }
438
439 self.kernel = Kernel::Restarting;
440 cx.notify();
441
442 self.launch_kernel_with_spec(spec, window, cx);
443 }
444 }
445
446 fn interrupt_kernel(
447 &mut self,
448 _: &InterruptKernel,
449 _window: &mut Window,
450 cx: &mut Context<Self>,
451 ) {
452 if let Kernel::RunningKernel(kernel) = &self.kernel {
453 let interrupt_request = runtimelib::InterruptRequest {};
454 let message: JupyterMessage = interrupt_request.into();
455 kernel.request_tx().try_send(message).ok();
456 cx.notify();
457 }
458 }
459
460 fn execute_cell(&mut self, cell_id: CellId, cx: &mut Context<Self>) {
461 let code = if let Some(Cell::Code(cell)) = self.cell_map.get(&cell_id) {
462 let editor = cell.read(cx).editor().clone();
463 let buffer = editor.read(cx).buffer().read(cx);
464 buffer
465 .as_singleton()
466 .map(|b| b.read(cx).text())
467 .unwrap_or_default()
468 } else {
469 return;
470 };
471
472 if let Some(Cell::Code(cell)) = self.cell_map.get(&cell_id) {
473 cell.update(cx, |cell, cx| {
474 if cell.has_outputs() {
475 cell.clear_outputs();
476 }
477 cell.start_execution();
478 cx.notify();
479 });
480 }
481
482 let request = ExecuteRequest {
483 code,
484 ..Default::default()
485 };
486 let message: JupyterMessage = request.into();
487 let msg_id = message.header.msg_id.clone();
488
489 self.execution_requests.insert(msg_id, cell_id.clone());
490
491 if let Kernel::RunningKernel(kernel) = &mut self.kernel {
492 kernel.request_tx().try_send(message).ok();
493 }
494 }
495
496 fn has_outputs(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
497 self.cell_map.values().any(|cell| {
498 if let Cell::Code(code_cell) = cell {
499 code_cell.read(cx).has_outputs()
500 } else {
501 false
502 }
503 })
504 }
505
506 fn clear_outputs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
507 for cell in self.cell_map.values() {
508 if let Cell::Code(code_cell) = cell {
509 code_cell.update(cx, |cell, cx| {
510 cell.clear_outputs();
511 cx.notify();
512 });
513 }
514 }
515 cx.notify();
516 }
517
518 fn run_cells(&mut self, window: &mut Window, cx: &mut Context<Self>) {
519 println!("Cells would run here!");
520 for cell_id in self.cell_order.clone() {
521 self.execute_cell(cell_id, cx);
522 }
523 }
524
525 fn run_current_cell(&mut self, _: &Run, window: &mut Window, cx: &mut Context<Self>) {
526 if let Some(cell_id) = self.cell_order.get(self.selected_cell_index).cloned() {
527 if let Some(cell) = self.cell_map.get(&cell_id) {
528 match cell {
529 Cell::Code(_) => {
530 self.execute_cell(cell_id, cx);
531 }
532 Cell::Markdown(markdown_cell) => {
533 // for markdown, finish editing and move to next cell
534 let is_editing = markdown_cell.read(cx).is_editing();
535 if is_editing {
536 markdown_cell.update(cx, |cell, cx| {
537 cell.run(cx);
538 });
539 // move to the next cell
540 // Discussion can be done on this default implementation
541 self.move_to_next_cell(window, cx);
542 }
543 }
544 Cell::Raw(_) => {}
545 }
546 }
547 }
548 }
549
550 // Discussion can be done on this default implementation
551 /// Moves focus to the next cell, or creates a new code cell if at the end
552 fn move_to_next_cell(&mut self, window: &mut Window, cx: &mut Context<Self>) {
553 if self.selected_cell_index < self.cell_order.len() - 1 {
554 self.selected_cell_index += 1;
555 // focus the new cell's editor
556 if let Some(cell_id) = self.cell_order.get(self.selected_cell_index) {
557 if let Some(cell) = self.cell_map.get(cell_id) {
558 match cell {
559 Cell::Code(code_cell) => {
560 let editor = code_cell.read(cx).editor();
561 window.focus(&editor.focus_handle(cx), cx);
562 }
563 Cell::Markdown(markdown_cell) => {
564 // Don't auto-enter edit mode for next markdown cell
565 // Just select it
566 }
567 Cell::Raw(_) => {}
568 }
569 }
570 }
571 cx.notify();
572 } else {
573 // in the end, could optionally create a new cell
574 // For now, just stay on the current cell
575 }
576 }
577
578 fn open_notebook(&mut self, _: &OpenNotebook, _window: &mut Window, _cx: &mut Context<Self>) {
579 println!("Open notebook triggered");
580 }
581
582 fn move_cell_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
583 println!("Move cell up triggered");
584 if self.selected_cell_index > 0 {
585 self.cell_order
586 .swap(self.selected_cell_index, self.selected_cell_index - 1);
587 self.selected_cell_index -= 1;
588 cx.notify();
589 }
590 }
591
592 fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
593 println!("Move cell down triggered");
594 if self.selected_cell_index < self.cell_order.len() - 1 {
595 self.cell_order
596 .swap(self.selected_cell_index, self.selected_cell_index + 1);
597 self.selected_cell_index += 1;
598 cx.notify();
599 }
600 }
601
602 fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
603 let new_cell_id: CellId = Uuid::new_v4().into();
604 let languages = self.languages.clone();
605 let metadata: nbformat::v4::CellMetadata =
606 serde_json::from_str("{}").expect("empty object should parse");
607
608 let markdown_cell = cx.new(|cx| {
609 super::MarkdownCell::new(
610 new_cell_id.clone(),
611 metadata,
612 String::new(),
613 languages,
614 window,
615 cx,
616 )
617 });
618
619 let insert_index = self.selected_cell_index + 1;
620 self.cell_order.insert(insert_index, new_cell_id.clone());
621 self.cell_map
622 .insert(new_cell_id.clone(), Cell::Markdown(markdown_cell.clone()));
623 self.selected_cell_index = insert_index;
624
625 cx.subscribe(
626 &markdown_cell,
627 move |_this, cell, event: &MarkdownCellEvent, cx| match event {
628 MarkdownCellEvent::FinishedEditing | MarkdownCellEvent::Run(_) => {
629 cell.update(cx, |cell, cx| {
630 cell.reparse_markdown(cx);
631 });
632 }
633 },
634 )
635 .detach();
636
637 let cell_id_for_editor = new_cell_id.clone();
638 let editor = markdown_cell.read(cx).editor().clone();
639 cx.subscribe(&editor, move |this, _editor, event, cx| {
640 if let editor::EditorEvent::Focused = event {
641 if let Some(index) = this
642 .cell_order
643 .iter()
644 .position(|id| id == &cell_id_for_editor)
645 {
646 this.selected_cell_index = index;
647 cx.notify();
648 }
649 }
650 })
651 .detach();
652
653 self.cell_list.reset(self.cell_order.len());
654 cx.notify();
655 }
656
657 fn add_code_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
658 let new_cell_id: CellId = Uuid::new_v4().into();
659 let notebook_language = self.notebook_language.clone();
660 let metadata: nbformat::v4::CellMetadata =
661 serde_json::from_str("{}").expect("empty object should parse");
662
663 let code_cell = cx.new(|cx| {
664 super::CodeCell::new(
665 new_cell_id.clone(),
666 metadata,
667 String::new(),
668 notebook_language,
669 window,
670 cx,
671 )
672 });
673
674 let insert_index = self.selected_cell_index + 1;
675 self.cell_order.insert(insert_index, new_cell_id.clone());
676 self.cell_map
677 .insert(new_cell_id.clone(), Cell::Code(code_cell.clone()));
678 self.selected_cell_index = insert_index;
679
680 let cell_id_for_run = new_cell_id.clone();
681 cx.subscribe(&code_cell, move |this, _cell, event, cx| match event {
682 CellEvent::Run(cell_id) => this.execute_cell(cell_id.clone(), cx),
683 CellEvent::FocusedIn(_) => {
684 if let Some(index) = this.cell_order.iter().position(|id| id == &cell_id_for_run) {
685 this.selected_cell_index = index;
686 cx.notify();
687 }
688 }
689 })
690 .detach();
691
692 let cell_id_for_editor = new_cell_id.clone();
693 let editor = code_cell.read(cx).editor().clone();
694 cx.subscribe(&editor, move |this, _editor, event, cx| {
695 if let editor::EditorEvent::Focused = event {
696 if let Some(index) = this
697 .cell_order
698 .iter()
699 .position(|id| id == &cell_id_for_editor)
700 {
701 this.selected_cell_index = index;
702 cx.notify();
703 }
704 }
705 })
706 .detach();
707
708 self.cell_list.reset(self.cell_order.len());
709 cx.notify();
710 }
711
712 fn cell_count(&self) -> usize {
713 self.cell_map.len()
714 }
715
716 fn selected_index(&self) -> usize {
717 self.selected_cell_index
718 }
719
720 pub fn set_selected_index(
721 &mut self,
722 index: usize,
723 jump_to_index: bool,
724 window: &mut Window,
725 cx: &mut Context<Self>,
726 ) {
727 // let previous_index = self.selected_cell_index;
728 self.selected_cell_index = index;
729 let current_index = self.selected_cell_index;
730
731 // in the future we may have some `on_cell_change` event that we want to fire here
732
733 if jump_to_index {
734 self.jump_to_cell(current_index, window, cx);
735 }
736 }
737
738 pub fn select_next(
739 &mut self,
740 _: &menu::SelectNext,
741 window: &mut Window,
742 cx: &mut Context<Self>,
743 ) {
744 let count = self.cell_count();
745 if count > 0 {
746 let index = self.selected_index();
747 let ix = if index == count - 1 {
748 count - 1
749 } else {
750 index + 1
751 };
752 self.set_selected_index(ix, true, window, cx);
753 cx.notify();
754 }
755 }
756
757 pub fn select_previous(
758 &mut self,
759 _: &menu::SelectPrevious,
760 window: &mut Window,
761 cx: &mut Context<Self>,
762 ) {
763 let count = self.cell_count();
764 if count > 0 {
765 let index = self.selected_index();
766 let ix = if index == 0 { 0 } else { index - 1 };
767 self.set_selected_index(ix, true, window, cx);
768 cx.notify();
769 }
770 }
771
772 pub fn select_first(
773 &mut self,
774 _: &menu::SelectFirst,
775 window: &mut Window,
776 cx: &mut Context<Self>,
777 ) {
778 let count = self.cell_count();
779 if count > 0 {
780 self.set_selected_index(0, true, window, cx);
781 cx.notify();
782 }
783 }
784
785 pub fn select_last(
786 &mut self,
787 _: &menu::SelectLast,
788 window: &mut Window,
789 cx: &mut Context<Self>,
790 ) {
791 let count = self.cell_count();
792 if count > 0 {
793 self.set_selected_index(count - 1, true, window, cx);
794 cx.notify();
795 }
796 }
797
798 fn jump_to_cell(&mut self, index: usize, _window: &mut Window, _cx: &mut Context<Self>) {
799 self.cell_list.scroll_to_reveal_item(index);
800 }
801
802 fn button_group(window: &mut Window, cx: &mut Context<Self>) -> Div {
803 v_flex()
804 .gap(DynamicSpacing::Base04.rems(cx))
805 .items_center()
806 .w(px(CONTROL_SIZE + 4.0))
807 .overflow_hidden()
808 .rounded(px(5.))
809 .bg(cx.theme().colors().title_bar_background)
810 .p_px()
811 .border_1()
812 .border_color(cx.theme().colors().border)
813 }
814
815 fn render_notebook_control(
816 id: impl Into<SharedString>,
817 icon: IconName,
818 _window: &mut Window,
819 _cx: &mut Context<Self>,
820 ) -> IconButton {
821 let id: ElementId = ElementId::Name(id.into());
822 IconButton::new(id, icon).width(px(CONTROL_SIZE))
823 }
824
825 fn render_notebook_controls(
826 &self,
827 window: &mut Window,
828 cx: &mut Context<Self>,
829 ) -> impl IntoElement {
830 let has_outputs = self.has_outputs(window, cx);
831
832 v_flex()
833 .max_w(px(CONTROL_SIZE + 4.0))
834 .items_center()
835 .gap(DynamicSpacing::Base16.rems(cx))
836 .justify_between()
837 .flex_none()
838 .h_full()
839 .py(DynamicSpacing::Base12.px(cx))
840 .child(
841 v_flex()
842 .gap(DynamicSpacing::Base08.rems(cx))
843 .child(
844 Self::button_group(window, cx)
845 .child(
846 Self::render_notebook_control(
847 "run-all-cells",
848 IconName::PlayFilled,
849 window,
850 cx,
851 )
852 .tooltip(move |window, cx| {
853 Tooltip::for_action("Execute all cells", &RunAll, cx)
854 })
855 .on_click(|_, window, cx| {
856 window.dispatch_action(Box::new(RunAll), cx);
857 }),
858 )
859 .child(
860 Self::render_notebook_control(
861 "clear-all-outputs",
862 IconName::ListX,
863 window,
864 cx,
865 )
866 .disabled(!has_outputs)
867 .tooltip(move |window, cx| {
868 Tooltip::for_action("Clear all outputs", &ClearOutputs, cx)
869 })
870 .on_click(|_, window, cx| {
871 window.dispatch_action(Box::new(ClearOutputs), cx);
872 }),
873 ),
874 )
875 .child(
876 Self::button_group(window, cx)
877 .child(
878 Self::render_notebook_control(
879 "move-cell-up",
880 IconName::ArrowUp,
881 window,
882 cx,
883 )
884 .tooltip(move |window, cx| {
885 Tooltip::for_action("Move cell up", &MoveCellUp, cx)
886 })
887 .on_click(|_, window, cx| {
888 window.dispatch_action(Box::new(MoveCellUp), cx);
889 }),
890 )
891 .child(
892 Self::render_notebook_control(
893 "move-cell-down",
894 IconName::ArrowDown,
895 window,
896 cx,
897 )
898 .tooltip(move |window, cx| {
899 Tooltip::for_action("Move cell down", &MoveCellDown, cx)
900 })
901 .on_click(|_, window, cx| {
902 window.dispatch_action(Box::new(MoveCellDown), cx);
903 }),
904 ),
905 )
906 .child(
907 Self::button_group(window, cx)
908 .child(
909 Self::render_notebook_control(
910 "new-markdown-cell",
911 IconName::Plus,
912 window,
913 cx,
914 )
915 .tooltip(move |window, cx| {
916 Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx)
917 })
918 .on_click(|_, window, cx| {
919 window.dispatch_action(Box::new(AddMarkdownBlock), cx);
920 }),
921 )
922 .child(
923 Self::render_notebook_control(
924 "new-code-cell",
925 IconName::Code,
926 window,
927 cx,
928 )
929 .tooltip(move |window, cx| {
930 Tooltip::for_action("Add code block", &AddCodeBlock, cx)
931 })
932 .on_click(|_, window, cx| {
933 window.dispatch_action(Box::new(AddCodeBlock), cx);
934 }),
935 ),
936 ),
937 )
938 .child(
939 v_flex()
940 .gap(DynamicSpacing::Base08.rems(cx))
941 .items_center()
942 .child(
943 Self::render_notebook_control("more-menu", IconName::Ellipsis, window, cx)
944 .tooltip(move |window, cx| (Tooltip::text("More options"))(window, cx)),
945 )
946 .child(Self::button_group(window, cx).child({
947 let kernel_status = self.kernel.status();
948 let (icon, icon_color) = match &kernel_status {
949 KernelStatus::Idle => (IconName::ReplNeutral, Color::Success),
950 KernelStatus::Busy => (IconName::ReplNeutral, Color::Warning),
951 KernelStatus::Starting => (IconName::ReplNeutral, Color::Muted),
952 KernelStatus::Error => (IconName::ReplNeutral, Color::Error),
953 KernelStatus::ShuttingDown => (IconName::ReplNeutral, Color::Muted),
954 KernelStatus::Shutdown => (IconName::ReplNeutral, Color::Disabled),
955 KernelStatus::Restarting => (IconName::ReplNeutral, Color::Warning),
956 };
957 let kernel_name = self
958 .kernel_specification
959 .as_ref()
960 .map(|spec| spec.name().to_string())
961 .unwrap_or_else(|| "Select Kernel".to_string());
962 IconButton::new("repl", icon)
963 .icon_color(icon_color)
964 .tooltip(move |window, cx| {
965 Tooltip::text(format!(
966 "{} ({}). Click to change kernel.",
967 kernel_name,
968 kernel_status.to_string()
969 ))(window, cx)
970 })
971 .on_click(cx.listener(|this, _, window, cx| {
972 this.kernel_picker_handle.toggle(window, cx);
973 }))
974 })),
975 )
976 }
977
978 fn render_kernel_status_bar(
979 &self,
980 _window: &mut Window,
981 cx: &mut Context<Self>,
982 ) -> impl IntoElement {
983 let kernel_status = self.kernel.status();
984 let kernel_name = self
985 .kernel_specification
986 .as_ref()
987 .map(|spec| spec.name().to_string())
988 .unwrap_or_else(|| "Select Kernel".to_string());
989
990 let (status_icon, status_color) = match &kernel_status {
991 KernelStatus::Idle => (IconName::Circle, Color::Success),
992 KernelStatus::Busy => (IconName::ArrowCircle, Color::Warning),
993 KernelStatus::Starting => (IconName::ArrowCircle, Color::Muted),
994 KernelStatus::Error => (IconName::XCircle, Color::Error),
995 KernelStatus::ShuttingDown => (IconName::ArrowCircle, Color::Muted),
996 KernelStatus::Shutdown => (IconName::Circle, Color::Muted),
997 KernelStatus::Restarting => (IconName::ArrowCircle, Color::Warning),
998 };
999
1000 let is_spinning = matches!(
1001 kernel_status,
1002 KernelStatus::Busy
1003 | KernelStatus::Starting
1004 | KernelStatus::ShuttingDown
1005 | KernelStatus::Restarting
1006 );
1007
1008 let status_icon_element = if is_spinning {
1009 Icon::new(status_icon)
1010 .size(IconSize::Small)
1011 .color(status_color)
1012 .with_rotate_animation(2)
1013 .into_any_element()
1014 } else {
1015 Icon::new(status_icon)
1016 .size(IconSize::Small)
1017 .color(status_color)
1018 .into_any_element()
1019 };
1020
1021 let worktree_id = self.worktree_id;
1022 let kernel_picker_handle = self.kernel_picker_handle.clone();
1023 let view = cx.entity().downgrade();
1024
1025 h_flex()
1026 .w_full()
1027 .px_3()
1028 .py_1()
1029 .gap_2()
1030 .items_center()
1031 .justify_between()
1032 .bg(cx.theme().colors().status_bar_background)
1033 .child(
1034 KernelSelector::new(
1035 Box::new(move |spec: KernelSpecification, window, cx| {
1036 if let Some(view) = view.upgrade() {
1037 view.update(cx, |this, cx| {
1038 this.change_kernel(spec, window, cx);
1039 });
1040 }
1041 }),
1042 worktree_id,
1043 Button::new("kernel-selector", kernel_name.clone())
1044 .label_size(LabelSize::Small)
1045 .icon(status_icon)
1046 .icon_size(IconSize::Small)
1047 .icon_color(status_color)
1048 .icon_position(IconPosition::Start),
1049 Tooltip::text(format!(
1050 "Kernel: {} ({}). Click to change.",
1051 kernel_name,
1052 kernel_status.to_string()
1053 )),
1054 )
1055 .with_handle(kernel_picker_handle),
1056 )
1057 .child(
1058 h_flex()
1059 .gap_1()
1060 .child(
1061 IconButton::new("restart-kernel", IconName::RotateCw)
1062 .icon_size(IconSize::Small)
1063 .tooltip(|window, cx| {
1064 Tooltip::for_action("Restart Kernel", &RestartKernel, cx)
1065 })
1066 .on_click(cx.listener(|this, _, window, cx| {
1067 this.restart_kernel(&RestartKernel, window, cx);
1068 })),
1069 )
1070 .child(
1071 IconButton::new("interrupt-kernel", IconName::Stop)
1072 .icon_size(IconSize::Small)
1073 .disabled(!matches!(kernel_status, KernelStatus::Busy))
1074 .tooltip(|window, cx| {
1075 Tooltip::for_action("Interrupt Kernel", &InterruptKernel, cx)
1076 })
1077 .on_click(cx.listener(|this, _, window, cx| {
1078 this.interrupt_kernel(&InterruptKernel, window, cx);
1079 })),
1080 ),
1081 )
1082 }
1083
1084 fn cell_list(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1085 let view = cx.entity();
1086 list(self.cell_list.clone(), move |index, window, cx| {
1087 view.update(cx, |this, cx| {
1088 let cell_id = &this.cell_order[index];
1089 let cell = this.cell_map.get(cell_id).unwrap();
1090 this.render_cell(index, cell, window, cx).into_any_element()
1091 })
1092 })
1093 .size_full()
1094 }
1095
1096 fn cell_position(&self, index: usize) -> CellPosition {
1097 match index {
1098 0 => CellPosition::First,
1099 index if index == self.cell_count() - 1 => CellPosition::Last,
1100 _ => CellPosition::Middle,
1101 }
1102 }
1103
1104 fn render_cell(
1105 &self,
1106 index: usize,
1107 cell: &Cell,
1108 window: &mut Window,
1109 cx: &mut Context<Self>,
1110 ) -> impl IntoElement {
1111 let cell_position = self.cell_position(index);
1112
1113 let is_selected = index == self.selected_cell_index;
1114
1115 match cell {
1116 Cell::Code(cell) => {
1117 cell.update(cx, |cell, _cx| {
1118 cell.set_selected(is_selected)
1119 .set_cell_position(cell_position);
1120 });
1121 cell.clone().into_any_element()
1122 }
1123 Cell::Markdown(cell) => {
1124 cell.update(cx, |cell, _cx| {
1125 cell.set_selected(is_selected)
1126 .set_cell_position(cell_position);
1127 });
1128 cell.clone().into_any_element()
1129 }
1130 Cell::Raw(cell) => {
1131 cell.update(cx, |cell, _cx| {
1132 cell.set_selected(is_selected)
1133 .set_cell_position(cell_position);
1134 });
1135 cell.clone().into_any_element()
1136 }
1137 }
1138 }
1139}
1140
1141impl Render for NotebookEditor {
1142 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1143 v_flex()
1144 .size_full()
1145 .key_context("NotebookEditor")
1146 .track_focus(&self.focus_handle)
1147 .on_action(cx.listener(|this, &OpenNotebook, window, cx| {
1148 this.open_notebook(&OpenNotebook, window, cx)
1149 }))
1150 .on_action(
1151 cx.listener(|this, &ClearOutputs, window, cx| this.clear_outputs(window, cx)),
1152 )
1153 .on_action(
1154 cx.listener(|this, &Run, window, cx| this.run_current_cell(&Run, window, cx)),
1155 )
1156 .on_action(cx.listener(|this, &RunAll, window, cx| this.run_cells(window, cx)))
1157 .on_action(cx.listener(|this, &MoveCellUp, window, cx| this.move_cell_up(window, cx)))
1158 .on_action(
1159 cx.listener(|this, &MoveCellDown, window, cx| this.move_cell_down(window, cx)),
1160 )
1161 .on_action(cx.listener(|this, &AddMarkdownBlock, window, cx| {
1162 this.add_markdown_block(window, cx)
1163 }))
1164 .on_action(
1165 cx.listener(|this, &AddCodeBlock, window, cx| this.add_code_block(window, cx)),
1166 )
1167 .on_action(
1168 cx.listener(|this, action, window, cx| this.restart_kernel(action, window, cx)),
1169 )
1170 .on_action(
1171 cx.listener(|this, action, window, cx| this.interrupt_kernel(action, window, cx)),
1172 )
1173 .child(
1174 h_flex()
1175 .flex_1()
1176 .w_full()
1177 .h_full()
1178 .gap_2()
1179 .child(div().flex_1().h_full().child(self.cell_list(window, cx)))
1180 .child(self.render_notebook_controls(window, cx)),
1181 )
1182 .child(self.render_kernel_status_bar(window, cx))
1183 }
1184}
1185
1186impl Focusable for NotebookEditor {
1187 fn focus_handle(&self, _: &App) -> FocusHandle {
1188 self.focus_handle.clone()
1189 }
1190}
1191
1192// Intended to be a NotebookBuffer
1193pub struct NotebookItem {
1194 path: PathBuf,
1195 project_path: ProjectPath,
1196 languages: Arc<LanguageRegistry>,
1197 // Raw notebook data
1198 notebook: nbformat::v4::Notebook,
1199 // Store our version of the notebook in memory (cell_order, cell_map)
1200 id: ProjectEntryId,
1201}
1202
1203impl project::ProjectItem for NotebookItem {
1204 fn try_open(
1205 project: &Entity<Project>,
1206 path: &ProjectPath,
1207 cx: &mut App,
1208 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
1209 let path = path.clone();
1210 let project = project.clone();
1211 let fs = project.read(cx).fs().clone();
1212 let languages = project.read(cx).languages().clone();
1213
1214 if path.path.extension().unwrap_or_default() == "ipynb" {
1215 Some(cx.spawn(async move |cx| {
1216 let abs_path = project
1217 .read_with(cx, |project, cx| project.absolute_path(&path, cx))
1218 .with_context(|| format!("finding the absolute path of {path:?}"))?;
1219
1220 // todo: watch for changes to the file
1221 let file_content = fs.load(abs_path.as_path()).await?;
1222
1223 // Pre-process to ensure IDs exist
1224 let mut json: serde_json::Value = serde_json::from_str(&file_content)?;
1225 if let Some(cells) = json.get_mut("cells").and_then(|c| c.as_array_mut()) {
1226 for cell in cells {
1227 if cell.get("id").is_none() {
1228 cell["id"] = serde_json::Value::String(Uuid::new_v4().to_string());
1229 }
1230 }
1231 }
1232 let file_content = serde_json::to_string(&json)?;
1233
1234 let notebook = nbformat::parse_notebook(&file_content);
1235
1236 let notebook = match notebook {
1237 Ok(nbformat::Notebook::V4(notebook)) => notebook,
1238 // 4.1 - 4.4 are converted to 4.5
1239 Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
1240 // TODO: Decide if we want to mutate the notebook by including Cell IDs
1241 // and any other conversions
1242
1243 nbformat::upgrade_legacy_notebook(legacy_notebook)?
1244 }
1245 // Bad notebooks and notebooks v4.0 and below are not supported
1246 Err(e) => {
1247 anyhow::bail!("Failed to parse notebook: {:?}", e);
1248 }
1249 };
1250
1251 let id = project
1252 .update(cx, |project, cx| {
1253 project.entry_for_path(&path, cx).map(|entry| entry.id)
1254 })
1255 .context("Entry not found")?;
1256
1257 Ok(cx.new(|_| NotebookItem {
1258 path: abs_path,
1259 project_path: path,
1260 languages,
1261 notebook,
1262 id,
1263 }))
1264 }))
1265 } else {
1266 None
1267 }
1268 }
1269
1270 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
1271 Some(self.id)
1272 }
1273
1274 fn project_path(&self, _: &App) -> Option<ProjectPath> {
1275 Some(self.project_path.clone())
1276 }
1277
1278 fn is_dirty(&self) -> bool {
1279 // TODO: Track if notebook metadata or structure has changed
1280 false
1281 }
1282}
1283
1284impl NotebookItem {
1285 pub fn language_name(&self) -> Option<String> {
1286 self.notebook
1287 .metadata
1288 .language_info
1289 .as_ref()
1290 .map(|l| l.name.clone())
1291 .or(self
1292 .notebook
1293 .metadata
1294 .kernelspec
1295 .as_ref()
1296 .and_then(|spec| spec.language.clone()))
1297 }
1298
1299 pub fn notebook_language(&self) -> impl Future<Output = Option<Arc<Language>>> + use<> {
1300 let language_name = self.language_name();
1301 let languages = self.languages.clone();
1302
1303 async move {
1304 if let Some(language_name) = language_name {
1305 languages.language_for_name(&language_name).await.ok()
1306 } else {
1307 None
1308 }
1309 }
1310 }
1311}
1312
1313impl EventEmitter<()> for NotebookEditor {}
1314
1315// pub struct NotebookControls {
1316// pane_focused: bool,
1317// active_item: Option<Box<dyn ItemHandle>>,
1318// // subscription: Option<Subscription>,
1319// }
1320
1321// impl NotebookControls {
1322// pub fn new() -> Self {
1323// Self {
1324// pane_focused: false,
1325// active_item: Default::default(),
1326// // subscription: Default::default(),
1327// }
1328// }
1329// }
1330
1331// impl EventEmitter<ToolbarItemEvent> for NotebookControls {}
1332
1333// impl Render for NotebookControls {
1334// fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1335// div().child("notebook controls")
1336// }
1337// }
1338
1339// impl ToolbarItemView for NotebookControls {
1340// fn set_active_pane_item(
1341// &mut self,
1342// active_pane_item: Option<&dyn workspace::ItemHandle>,
1343// window: &mut Window, cx: &mut Context<Self>,
1344// ) -> workspace::ToolbarItemLocation {
1345// cx.notify();
1346// self.active_item = None;
1347
1348// let Some(item) = active_pane_item else {
1349// return ToolbarItemLocation::Hidden;
1350// };
1351
1352// ToolbarItemLocation::PrimaryLeft
1353// }
1354
1355// fn pane_focus_update(&mut self, pane_focused: bool, _window: &mut Window, _cx: &mut Context<Self>) {
1356// self.pane_focused = pane_focused;
1357// }
1358// }
1359
1360impl Item for NotebookEditor {
1361 type Event = ();
1362
1363 fn can_split(&self) -> bool {
1364 true
1365 }
1366
1367 fn clone_on_split(
1368 &self,
1369 _workspace_id: Option<workspace::WorkspaceId>,
1370 window: &mut Window,
1371 cx: &mut Context<Self>,
1372 ) -> Task<Option<Entity<Self>>>
1373 where
1374 Self: Sized,
1375 {
1376 Task::ready(Some(cx.new(|cx| {
1377 Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)
1378 })))
1379 }
1380
1381 fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {
1382 workspace::item::ItemBufferKind::Singleton
1383 }
1384
1385 fn for_each_project_item(
1386 &self,
1387 cx: &App,
1388 f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1389 ) {
1390 f(self.notebook_item.entity_id(), self.notebook_item.read(cx))
1391 }
1392
1393 fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
1394 self.notebook_item
1395 .read(cx)
1396 .project_path
1397 .path
1398 .file_name()
1399 .map(|s| s.to_string())
1400 .unwrap_or_default()
1401 .into()
1402 }
1403
1404 fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement {
1405 Label::new(self.tab_content_text(params.detail.unwrap_or(0), cx))
1406 .single_line()
1407 .color(params.text_color())
1408 .when(params.preview, |this| this.italic())
1409 .into_any_element()
1410 }
1411
1412 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
1413 Some(IconName::Book.into())
1414 }
1415
1416 fn show_toolbar(&self) -> bool {
1417 false
1418 }
1419
1420 // TODO
1421 fn pixel_position_of_cursor(&self, _: &App) -> Option<Point<Pixels>> {
1422 None
1423 }
1424
1425 // TODO
1426 fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
1427 None
1428 }
1429
1430 fn set_nav_history(
1431 &mut self,
1432 _: workspace::ItemNavHistory,
1433 _window: &mut Window,
1434 _: &mut Context<Self>,
1435 ) {
1436 // TODO
1437 }
1438
1439 fn can_save(&self, _cx: &App) -> bool {
1440 true
1441 }
1442
1443 fn save(
1444 &mut self,
1445 _options: SaveOptions,
1446 project: Entity<Project>,
1447 _window: &mut Window,
1448 cx: &mut Context<Self>,
1449 ) -> Task<Result<()>> {
1450 let notebook = self.to_notebook(cx);
1451 let path = self.notebook_item.read(cx).path.clone();
1452 let fs = project.read(cx).fs().clone();
1453
1454 self.mark_as_saved(cx);
1455
1456 cx.spawn(async move |_this, _cx| {
1457 let json =
1458 serde_json::to_string_pretty(¬ebook).context("Failed to serialize notebook")?;
1459 fs.atomic_write(path, json).await?;
1460 Ok(())
1461 })
1462 }
1463
1464 fn save_as(
1465 &mut self,
1466 project: Entity<Project>,
1467 path: ProjectPath,
1468 _window: &mut Window,
1469 cx: &mut Context<Self>,
1470 ) -> Task<Result<()>> {
1471 let notebook = self.to_notebook(cx);
1472 let fs = project.read(cx).fs().clone();
1473
1474 let abs_path = project.read(cx).absolute_path(&path, cx);
1475
1476 self.mark_as_saved(cx);
1477
1478 cx.spawn(async move |_this, _cx| {
1479 let abs_path = abs_path.context("Failed to get absolute path")?;
1480 let json =
1481 serde_json::to_string_pretty(¬ebook).context("Failed to serialize notebook")?;
1482 fs.atomic_write(abs_path, json).await?;
1483 Ok(())
1484 })
1485 }
1486
1487 fn reload(
1488 &mut self,
1489 project: Entity<Project>,
1490 window: &mut Window,
1491 cx: &mut Context<Self>,
1492 ) -> Task<Result<()>> {
1493 let path = self.notebook_item.read(cx).path.clone();
1494 let fs = project.read(cx).fs().clone();
1495 let languages = self.languages.clone();
1496 let notebook_language = self.notebook_language.clone();
1497
1498 cx.spawn_in(window, async move |this, cx| {
1499 let file_content = fs.load(&path).await?;
1500
1501 let mut json: serde_json::Value = serde_json::from_str(&file_content)?;
1502 if let Some(cells) = json.get_mut("cells").and_then(|c| c.as_array_mut()) {
1503 for cell in cells {
1504 if cell.get("id").is_none() {
1505 cell["id"] = serde_json::Value::String(Uuid::new_v4().to_string());
1506 }
1507 }
1508 }
1509 let file_content = serde_json::to_string(&json)?;
1510
1511 let notebook = nbformat::parse_notebook(&file_content);
1512 let notebook = match notebook {
1513 Ok(nbformat::Notebook::V4(notebook)) => notebook,
1514 Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
1515 nbformat::upgrade_legacy_notebook(legacy_notebook)?
1516 }
1517 Err(e) => {
1518 anyhow::bail!("Failed to parse notebook: {:?}", e);
1519 }
1520 };
1521
1522 this.update_in(cx, |this, window, cx| {
1523 let mut cell_order = vec![];
1524 let mut cell_map = HashMap::default();
1525
1526 for cell in notebook.cells.iter() {
1527 let cell_id = cell.id();
1528 cell_order.push(cell_id.clone());
1529 let cell_entity =
1530 Cell::load(cell, &languages, notebook_language.clone(), window, cx);
1531 cell_map.insert(cell_id.clone(), cell_entity);
1532 }
1533
1534 this.cell_order = cell_order.clone();
1535 this.original_cell_order = cell_order;
1536 this.cell_map = cell_map;
1537 this.cell_list =
1538 ListState::new(this.cell_order.len(), gpui::ListAlignment::Top, px(1000.));
1539 cx.notify();
1540 })?;
1541
1542 Ok(())
1543 })
1544 }
1545
1546 fn is_dirty(&self, cx: &App) -> bool {
1547 self.has_structural_changes() || self.has_content_changes(cx)
1548 }
1549}
1550
1551impl ProjectItem for NotebookEditor {
1552 type Item = NotebookItem;
1553
1554 fn for_project_item(
1555 project: Entity<Project>,
1556 _pane: Option<&Pane>,
1557 item: Entity<Self::Item>,
1558 window: &mut Window,
1559 cx: &mut Context<Self>,
1560 ) -> Self {
1561 Self::new(project, item, window, cx)
1562 }
1563}
1564
1565impl KernelSession for NotebookEditor {
1566 fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context<Self>) {
1567 // Handle kernel status updates (these are broadcast to all)
1568 if let JupyterMessageContent::Status(status) = &message.content {
1569 self.kernel.set_execution_state(&status.execution_state);
1570 cx.notify();
1571 }
1572
1573 // Handle cell-specific messages
1574 if let Some(parent_header) = &message.parent_header {
1575 if let Some(cell_id) = self.execution_requests.get(&parent_header.msg_id) {
1576 if let Some(Cell::Code(cell)) = self.cell_map.get(cell_id) {
1577 cell.update(cx, |cell, cx| {
1578 cell.handle_message(message, window, cx);
1579 });
1580 }
1581 }
1582 }
1583 }
1584
1585 fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
1586 self.kernel = Kernel::ErroredLaunch(error_message);
1587 cx.notify();
1588 }
1589}