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