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