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