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