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