diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 11f262d1b4c4f8666d4d4dc5c35769a55801e841..190f915d86f1c3f5a69449fbc17ca456800f6d52 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1436,7 +1436,7 @@ { "context": "NotebookEditor", "bindings": { - "shift-enter": "notebook::Run", + "shift-enter": "notebook::RunAndAdvance", "ctrl-enter": "notebook::Run", "ctrl-shift-enter": "notebook::RunAll", "alt-up": "notebook::MoveCellUp", @@ -1447,11 +1447,19 @@ "ctrl-c": "notebook::InterruptKernel", }, }, + { + "context": "NotebookEditor && notebook_mode == command", + "bindings": { + "enter": "notebook::EnterEditMode", + "down": "menu::SelectNext", + "up": "menu::SelectPrevious", + }, + }, { "context": "NotebookEditor > Editor", "bindings": { "enter": "editor::Newline", - "shift-enter": "notebook::Run", + "shift-enter": "notebook::RunAndAdvance", "ctrl-enter": "notebook::Run", "ctrl-shift-enter": "notebook::RunAll", "alt-up": "notebook::MoveCellUp", @@ -1460,6 +1468,7 @@ "ctrl-shift-m": "notebook::AddMarkdownBlock", "ctrl-shift-r": "notebook::RestartKernel", "ctrl-c": "notebook::InterruptKernel", + "escape": "notebook::EnterCommandMode", }, }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 7466fa31fe52c0ad53a445e251d63aaf5746e778..e1119be12980e49538bfbfe81d0834b61d0181f3 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1572,7 +1572,7 @@ { "context": "NotebookEditor", "bindings": { - "shift-enter": "notebook::Run", + "shift-enter": "notebook::RunAndAdvance", "cmd-enter": "notebook::Run", "cmd-shift-enter": "notebook::RunAll", "alt-up": "notebook::MoveCellUp", @@ -1583,11 +1583,19 @@ "cmd-c": "notebook::InterruptKernel", }, }, + { + "context": "NotebookEditor && notebook_mode == command", + "bindings": { + "enter": "notebook::EnterEditMode", + "down": "menu::SelectNext", + "up": "menu::SelectPrevious", + }, + }, { "context": "NotebookEditor > Editor", "bindings": { "enter": "editor::Newline", - "shift-enter": "notebook::Run", + "shift-enter": "notebook::RunAndAdvance", "cmd-enter": "notebook::Run", "cmd-shift-enter": "notebook::RunAll", "alt-up": "notebook::MoveCellUp", @@ -1596,6 +1604,7 @@ "cmd-shift-m": "notebook::AddMarkdownBlock", "cmd-shift-r": "notebook::RestartKernel", "cmd-c": "notebook::InterruptKernel", + "escape": "notebook::EnterCommandMode", }, }, ] diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index a9eb3933423ff60fe60ac391b12773ce7146fb0d..0736e49162d8186990c2af62f971245e1fc825fe 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1488,7 +1488,7 @@ { "context": "NotebookEditor", "bindings": { - "shift-enter": "notebook::Run", + "shift-enter": "notebook::RunAndAdvance", "ctrl-enter": "notebook::Run", "ctrl-shift-enter": "notebook::RunAll", "alt-up": "notebook::MoveCellUp", @@ -1499,11 +1499,19 @@ "ctrl-c": "notebook::InterruptKernel", }, }, + { + "context": "NotebookEditor && notebook_mode == command", + "bindings": { + "enter": "notebook::EnterEditMode", + "down": "menu::SelectNext", + "up": "menu::SelectPrevious", + }, + }, { "context": "NotebookEditor > Editor", "bindings": { "enter": "editor::Newline", - "shift-enter": "notebook::Run", + "shift-enter": "notebook::RunAndAdvance", "ctrl-enter": "notebook::Run", "ctrl-shift-enter": "notebook::RunAll", "alt-up": "notebook::MoveCellUp", @@ -1512,6 +1520,7 @@ "ctrl-shift-m": "notebook::AddMarkdownBlock", "ctrl-shift-r": "notebook::RestartKernel", "ctrl-c": "notebook::InterruptKernel", + "escape": "notebook::EnterCommandMode", }, }, ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 220b44ff537ffa791b23c0c5b7d86b6768d74dc2..efc375795ee70c57c372aa8c56352bcae5f8e8f7 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -1110,10 +1110,24 @@ }, { "context": "NotebookEditor > Editor && VimControl && vim_mode == normal", - "bindings": { "j": "notebook::NotebookMoveDown", "k": "notebook::NotebookMoveUp", + "escape": "notebook::EnterCommandMode", + }, + }, + { + "context": "NotebookEditor && notebook_mode == command", + "bindings": { + "j": "menu::SelectNext", + "k": "menu::SelectPrevious", + "g g": "menu::SelectFirst", + "shift-g": "menu::SelectLast", + "i": "notebook::EnterEditMode", + "a": "notebook::EnterEditMode", + "enter": "notebook::EnterEditMode", + "shift-enter": "notebook::RunAndAdvance", + "ctrl-enter": "notebook::Run", }, }, { diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 76a0d2a47037f0ccd48fcfe9cb088ceb9e37aeaa..0f80b20050b678590c9cf7fd20d2e0a2cfbd2428 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -10,8 +10,8 @@ use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag}; use futures::FutureExt; use futures::future::Shared; use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, ListScrollEvent, ListState, - Point, Task, actions, list, prelude::*, + AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, ListScrollEvent, + ListState, Point, Task, actions, list, prelude::*, }; use jupyter_protocol::JupyterKernelspec; use language::{Language, LanguageRegistry}; @@ -41,33 +41,18 @@ use picker::Picker; use runtimelib::{ExecuteRequest, JupyterMessage, JupyterMessageContent}; use ui::PopoverMenuHandle; use zed_actions::editor::{MoveDown, MoveUp}; -use zed_actions::notebook::{NotebookMoveDown, NotebookMoveUp}; - -actions!( - notebook, - [ - /// Opens a Jupyter notebook file. - OpenNotebook, - /// Runs all cells in the notebook. - RunAll, - /// Runs the current cell. - Run, - /// Clears all cell outputs. - ClearOutputs, - /// Moves the current cell up. - MoveCellUp, - /// Moves the current cell down. - MoveCellDown, - /// Adds a new markdown cell. - AddMarkdownBlock, - /// Adds a new code cell. - AddCodeBlock, - /// Restarts the kernel. - RestartKernel, - /// Interrupts the current execution. - InterruptKernel, - ] -); +use zed_actions::notebook::{ + AddCodeBlock, AddMarkdownBlock, ClearOutputs, EnterCommandMode, EnterEditMode, InterruptKernel, + MoveCellDown, MoveCellUp, NotebookMoveDown, NotebookMoveUp, OpenNotebook, RestartKernel, Run, + RunAll, RunAndAdvance, +}; + +/// Whether the notebook is in command mode (navigating cells) or edit mode (editing a cell). +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum NotebookMode { + Command, + Edit, +} pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0; pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0; @@ -107,6 +92,7 @@ pub struct NotebookEditor { remote_id: Option, cell_list: ListState, + notebook_mode: NotebookMode, selected_cell_index: usize, cell_order: Vec, original_cell_order: Vec, @@ -148,18 +134,9 @@ impl NotebookEditor { match &cell_entity { Cell::Code(code_cell) => { let cell_id_for_focus = cell_id.clone(); - cx.subscribe(code_cell, move |this, cell, event, cx| match event { + cx.subscribe(code_cell, move |this, _cell, event, cx| match event { CellEvent::Run(cell_id) => this.execute_cell(cell_id.clone(), cx), - CellEvent::FocusedIn(_) => { - if let Some(index) = this - .cell_order - .iter() - .position(|id| id == &cell_id_for_focus) - { - this.selected_cell_index = index; - cx.notify(); - } - } + CellEvent::FocusedIn(_) => this.select_cell_by_id(&cell_id_for_focus, cx), }) .detach(); @@ -167,20 +144,12 @@ impl NotebookEditor { let editor = code_cell.read(cx).editor().clone(); cx.subscribe(&editor, move |this, _editor, event, cx| { if let editor::EditorEvent::Focused = event { - if let Some(index) = this - .cell_order - .iter() - .position(|id| id == &cell_id_for_editor) - { - this.selected_cell_index = index; - cx.notify(); - } + this.select_cell_by_id(&cell_id_for_editor, cx); } }) .detach(); } Cell::Markdown(markdown_cell) => { - let cell_id_for_focus = cell_id.clone(); cx.subscribe( markdown_cell, move |_this, cell, event: &MarkdownCellEvent, cx| { @@ -206,14 +175,7 @@ impl NotebookEditor { let editor = markdown_cell.read(cx).editor().clone(); cx.subscribe(&editor, move |this, _editor, event, cx| { if let editor::EditorEvent::Focused = event { - if let Some(index) = this - .cell_order - .iter() - .position(|id| id == &cell_id_for_editor) - { - this.selected_cell_index = index; - cx.notify(); - } + this.select_cell_by_id(&cell_id_for_editor, cx); } }) .detach(); @@ -239,6 +201,7 @@ impl NotebookEditor { notebook_language, remote_id: None, cell_list, + notebook_mode: NotebookMode::Command, selected_cell_index: 0, cell_order: cell_order.clone(), original_cell_order: cell_order.clone(), @@ -385,8 +348,7 @@ impl NotebookEditor { let working_directory = self .project .read(cx) - .worktrees(cx) - .next() + .worktree_for_id(self.worktree_id, cx) .map(|worktree| worktree.read(cx).abs_path().to_path_buf()) .unwrap_or_else(std::env::temp_dir); let fs = self.project.read(cx).fs().clone(); @@ -590,6 +552,31 @@ impl NotebookEditor { } fn run_current_cell(&mut self, _: &Run, window: &mut Window, cx: &mut Context) { + let Some(cell_id) = self.cell_order.get(self.selected_cell_index).cloned() else { + return; + }; + let Some(cell) = self.cell_map.get(&cell_id) else { + return; + }; + match cell { + Cell::Code(_) => { + self.execute_cell(cell_id, cx); + } + Cell::Markdown(markdown_cell) => { + // for markdown, finish editing and move to next cell + let is_editing = markdown_cell.read(cx).is_editing(); + if is_editing { + markdown_cell.update(cx, |cell, cx| { + cell.run(cx); + }); + self.enter_command_mode(window, cx); + } + } + Cell::Raw(_) => {} + } + } + + fn run_and_advance(&mut self, _: &RunAndAdvance, window: &mut Window, cx: &mut Context) { if let Some(cell_id) = self.cell_order.get(self.selected_cell_index).cloned() { if let Some(cell) = self.cell_map.get(&cell_id) { match cell { @@ -597,25 +584,83 @@ impl NotebookEditor { self.execute_cell(cell_id, cx); } Cell::Markdown(markdown_cell) => { - // for markdown, finish editing and move to next cell - let is_editing = markdown_cell.read(cx).is_editing(); - if is_editing { + if markdown_cell.read(cx).is_editing() { markdown_cell.update(cx, |cell, cx| { cell.run(cx); }); - // move to the next cell - // Discussion can be done on this default implementation - self.move_to_next_cell(window, cx); } } Cell::Raw(_) => {} } } } + + let is_last_cell = self.selected_cell_index == self.cell_count().saturating_sub(1); + if is_last_cell { + self.add_code_block(window, cx); + self.enter_command_mode(window, cx); + } else { + self.advance_in_command_mode(window, cx); + } + } + + fn enter_edit_mode(&mut self, _: &EnterEditMode, window: &mut Window, cx: &mut Context) { + self.notebook_mode = NotebookMode::Edit; + if let Some(cell_id) = self.cell_order.get(self.selected_cell_index) { + if let Some(cell) = self.cell_map.get(cell_id) { + match cell { + Cell::Code(code_cell) => { + let editor = code_cell.read(cx).editor().clone(); + window.focus(&editor.focus_handle(cx), cx); + } + Cell::Markdown(markdown_cell) => { + markdown_cell.update(cx, |cell, cx| { + cell.set_editing(true); + cx.notify(); + }); + let editor = markdown_cell.read(cx).editor().clone(); + window.focus(&editor.focus_handle(cx), cx); + } + Cell::Raw(_) => {} + } + } + } + cx.notify(); + } + + fn enter_command_mode(&mut self, window: &mut Window, cx: &mut Context) { + self.notebook_mode = NotebookMode::Command; + self.focus_handle.focus(window, cx); + cx.notify(); + } + + fn handle_enter_command_mode( + &mut self, + _: &EnterCommandMode, + window: &mut Window, + cx: &mut Context, + ) { + self.enter_command_mode(window, cx); + } + + /// Advances to the next cell while staying in command mode (used by RunAndAdvance and shift-enter). + fn advance_in_command_mode(&mut self, window: &mut Window, cx: &mut Context) { + let count = self.cell_count(); + if count == 0 { + return; + } + if self.selected_cell_index < count - 1 { + self.selected_cell_index += 1; + self.cell_list + .scroll_to_reveal_item(self.selected_cell_index); + } + self.notebook_mode = NotebookMode::Command; + self.focus_handle.focus(window, cx); + cx.notify(); } // Discussion can be done on this default implementation - /// Moves focus to the next cell, or creates a new code cell if at the end + /// Moves focus to the next cell editor (used when already in edit mode). fn move_to_next_cell(&mut self, window: &mut Window, cx: &mut Context) { if !self.cell_order.is_empty() && self.selected_cell_index < self.cell_order.len() - 1 { self.selected_cell_index += 1; @@ -666,6 +711,19 @@ impl NotebookEditor { } } + fn insert_cell_at_current_position(&mut self, cell_id: CellId, cell: Cell) { + let insert_index = if self.cell_order.is_empty() { + 0 + } else { + self.selected_cell_index + 1 + }; + self.cell_order.insert(insert_index, cell_id.clone()); + self.cell_map.insert(cell_id, cell); + self.selected_cell_index = insert_index; + self.cell_list.splice(insert_index..insert_index, 1); + self.cell_list.scroll_to_reveal_item(insert_index); + } + fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context) { let new_cell_id: CellId = Uuid::new_v4().into(); let languages = self.languages.clone(); @@ -683,16 +741,6 @@ impl NotebookEditor { ) }); - let insert_index = if self.cell_order.is_empty() { - 0 - } else { - self.selected_cell_index + 1 - }; - self.cell_order.insert(insert_index, new_cell_id.clone()); - self.cell_map - .insert(new_cell_id.clone(), Cell::Markdown(markdown_cell.clone())); - self.selected_cell_index = insert_index; - cx.subscribe( &markdown_cell, move |_this, cell, event: &MarkdownCellEvent, cx| match event { @@ -709,19 +757,19 @@ impl NotebookEditor { let editor = markdown_cell.read(cx).editor().clone(); cx.subscribe(&editor, move |this, _editor, event, cx| { if let editor::EditorEvent::Focused = event { - if let Some(index) = this - .cell_order - .iter() - .position(|id| id == &cell_id_for_editor) - { - this.selected_cell_index = index; - cx.notify(); - } + this.select_cell_by_id(&cell_id_for_editor, cx); } }) .detach(); - self.cell_list.reset(self.cell_order.len()); + self.insert_cell_at_current_position(new_cell_id, Cell::Markdown(markdown_cell.clone())); + markdown_cell.update(cx, |cell, cx| { + cell.set_editing(true); + cx.notify(); + }); + let editor = markdown_cell.read(cx).editor().clone(); + window.focus(&editor.focus_handle(cx), cx); + self.notebook_mode = NotebookMode::Edit; cx.notify(); } @@ -742,25 +790,10 @@ impl NotebookEditor { ) }); - let insert_index = if self.cell_order.is_empty() { - 0 - } else { - self.selected_cell_index + 1 - }; - self.cell_order.insert(insert_index, new_cell_id.clone()); - self.cell_map - .insert(new_cell_id.clone(), Cell::Code(code_cell.clone())); - self.selected_cell_index = insert_index; - let cell_id_for_run = new_cell_id.clone(); cx.subscribe(&code_cell, move |this, _cell, event, cx| match event { CellEvent::Run(cell_id) => this.execute_cell(cell_id.clone(), cx), - CellEvent::FocusedIn(_) => { - if let Some(index) = this.cell_order.iter().position(|id| id == &cell_id_for_run) { - this.selected_cell_index = index; - cx.notify(); - } - } + CellEvent::FocusedIn(_) => this.select_cell_by_id(&cell_id_for_run, cx), }) .detach(); @@ -768,19 +801,15 @@ impl NotebookEditor { let editor = code_cell.read(cx).editor().clone(); cx.subscribe(&editor, move |this, _editor, event, cx| { if let editor::EditorEvent::Focused = event { - if let Some(index) = this - .cell_order - .iter() - .position(|id| id == &cell_id_for_editor) - { - this.selected_cell_index = index; - cx.notify(); - } + this.select_cell_by_id(&cell_id_for_editor, cx); } }) .detach(); - self.cell_list.reset(self.cell_order.len()); + self.insert_cell_at_current_position(new_cell_id, Cell::Code(code_cell.clone())); + let editor = code_cell.read(cx).editor().clone(); + window.focus(&editor.focus_handle(cx), cx); + self.notebook_mode = NotebookMode::Edit; cx.notify(); } @@ -792,6 +821,14 @@ impl NotebookEditor { self.selected_cell_index } + fn select_cell_by_id(&mut self, cell_id: &CellId, cx: &mut Context) { + if let Some(index) = self.cell_order.iter().position(|id| id == cell_id) { + self.selected_cell_index = index; + self.notebook_mode = NotebookMode::Edit; + cx.notify(); + } + } + pub fn set_selected_index( &mut self, index: usize, @@ -1216,9 +1253,19 @@ impl NotebookEditor { impl Render for NotebookEditor { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("NotebookEditor"); + key_context.set( + "notebook_mode", + match self.notebook_mode { + NotebookMode::Command => "command", + NotebookMode::Edit => "edit", + }, + ); + v_flex() .size_full() - .key_context("NotebookEditor") + .key_context(key_context) .track_focus(&self.focus_handle) .on_action(cx.listener(|this, _: &OpenNotebook, window, cx| { this.open_notebook(&OpenNotebook, window, cx) @@ -1229,6 +1276,9 @@ impl Render for NotebookEditor { .on_action( cx.listener(|this, _: &Run, window, cx| this.run_current_cell(&Run, window, cx)), ) + .on_action( + cx.listener(|this, action, window, cx| this.run_and_advance(action, window, cx)), + ) .on_action(cx.listener(|this, _: &RunAll, window, cx| this.run_cells(window, cx))) .on_action( cx.listener(|this, _: &MoveCellUp, window, cx| this.move_cell_up(window, cx)), @@ -1242,6 +1292,20 @@ impl Render for NotebookEditor { .on_action( cx.listener(|this, _: &AddCodeBlock, window, cx| this.add_code_block(window, cx)), ) + .on_action( + cx.listener(|this, action, window, cx| this.enter_edit_mode(action, window, cx)), + ) + .on_action(cx.listener(|this, action, window, cx| { + this.handle_enter_command_mode(action, window, cx) + })) + .on_action(cx.listener(|this, action, window, cx| this.select_next(action, window, cx))) + .on_action( + cx.listener(|this, action, window, cx| this.select_previous(action, window, cx)), + ) + .on_action( + cx.listener(|this, action, window, cx| this.select_first(action, window, cx)), + ) + .on_action(cx.listener(|this, action, window, cx| this.select_last(action, window, cx))) .on_action(cx.listener(|this, _: &MoveUp, window, cx| { this.select_previous(&menu::SelectPrevious, window, cx); if let Some(cell_id) = this.cell_order.get(this.selected_cell_index) { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 0a75471da974638a330c8786306c2010508fbebd..83628c33254728549b478df24cf6cf5e191ead72 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -795,10 +795,36 @@ pub mod notebook { actions!( notebook, [ - /// Move to down in cells + /// Opens a Jupyter notebook file. + OpenNotebook, + /// Runs all cells in the notebook. + RunAll, + /// Runs the current cell and stays on it. + Run, + /// Runs the current cell and advances to the next cell. + RunAndAdvance, + /// Clears all cell outputs. + ClearOutputs, + /// Moves the current cell up. + MoveCellUp, + /// Moves the current cell down. + MoveCellDown, + /// Adds a new markdown cell. + AddMarkdownBlock, + /// Adds a new code cell. + AddCodeBlock, + /// Restarts the kernel. + RestartKernel, + /// Interrupts the current execution. + InterruptKernel, + /// Move down in cells. NotebookMoveDown, - /// Move to up in cells + /// Move up in cells. NotebookMoveUp, + /// Enters the current cell's editor (edit mode). + EnterEditMode, + /// Exits the cell editor and returns to cell command mode. + EnterCommandMode, ] ); }