diff --git a/Cargo.lock b/Cargo.lock index 2c0df1c10f99635793a76b42fdea6ee95b739145..bc2ed2ccde40c14fc74c727c6d359a9da2e1e955 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13863,6 +13863,7 @@ dependencies = [ "util", "uuid", "workspace", + "zed_actions", ] [[package]] diff --git a/assets/icons/file_icons/jupyter.svg b/assets/icons/file_icons/jupyter.svg new file mode 100644 index 0000000000000000000000000000000000000000..9f2715ddd28418c5434e5be3d1ba5116dc112401 --- /dev/null +++ b/assets/icons/file_icons/jupyter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 2becf8daee3e4930590afc68b0f318786722bdfa..ca154e22b05f5358fab58678f5af52b503160c26 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1338,6 +1338,20 @@ "alt-right": "dev::EditPredictionContextGoForward", }, }, + { + "context": "NotebookEditor", + "bindings": { + "shift-enter": "notebook::Run", + "ctrl-enter": "notebook::Run", + "ctrl-shift-enter": "notebook::RunAll", + "alt-up": "notebook::MoveCellUp", + "alt-down": "notebook::MoveCellDown", + "ctrl-m": "notebook::AddCodeBlock", + "ctrl-shift-m": "notebook::AddMarkdownBlock", + "ctrl-shift-r": "notebook::RestartKernel", + "ctrl-c": "notebook::InterruptKernel", + }, + }, { "context": "NotebookEditor > Editor", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index b8b7399fb0c3e72abe282ada19cb866c306d4f22..0a40df26168567c62d24dd9cc83ede764edd30f1 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1483,6 +1483,20 @@ "shift-backspace": "project_dropdown::RemoveSelectedFolder", }, }, + { + "context": "NotebookEditor", + "bindings": { + "shift-enter": "notebook::Run", + "cmd-enter": "notebook::Run", + "cmd-shift-enter": "notebook::RunAll", + "alt-up": "notebook::MoveCellUp", + "alt-down": "notebook::MoveCellDown", + "cmd-m": "notebook::AddCodeBlock", + "cmd-shift-m": "notebook::AddMarkdownBlock", + "cmd-shift-r": "notebook::RestartKernel", + "cmd-c": "notebook::InterruptKernel", + }, + }, { "context": "NotebookEditor > Editor", "bindings": { diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 373befeee7fcf2d4fdd145a17fd58e43d850505a..69235b47a58870e3418a04927599ecba5ba180c7 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1401,6 +1401,20 @@ "shift-backspace": "project_dropdown::RemoveSelectedFolder", }, }, + { + "context": "NotebookEditor", + "bindings": { + "shift-enter": "notebook::Run", + "ctrl-enter": "notebook::Run", + "ctrl-shift-enter": "notebook::RunAll", + "alt-up": "notebook::MoveCellUp", + "alt-down": "notebook::MoveCellDown", + "ctrl-m": "notebook::AddCodeBlock", + "ctrl-shift-m": "notebook::AddMarkdownBlock", + "ctrl-shift-r": "notebook::RestartKernel", + "ctrl-c": "notebook::InterruptKernel", + }, + }, { "context": "NotebookEditor > Editor", "bindings": { diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index 1047dd68f2b10181765916612fd330b27fed66ad..e03676ba3bd26135b327969afde569a01c57dc6f 100644 --- a/crates/repl/Cargo.toml +++ b/crates/repl/Cargo.toml @@ -55,6 +55,7 @@ util.workspace = true uuid.workspace = true workspace.workspace = true picker.workspace = true +zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 02a8e0f7adaf90741c579584b5f58d4fe3b0e5bb..5559458da4a5c7212982fcb25d2496e39d039547 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -672,6 +672,18 @@ impl CodeCell { } } + pub fn set_language(&mut self, language: Option>, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + if let Some(buffer) = buffer.as_singleton() { + buffer.update(cx, |buffer, cx| { + buffer.set_language(language, cx); + }); + } + }); + }); + } + /// Load a code cell from notebook file data, including existing outputs and execution count pub fn load( id: CellId, diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 35a285d213e63086a9c1c2d5a6f565dd5b7a4d01..6c3848046b5c330ebfacccca090d23db50242af6 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -34,9 +34,11 @@ use crate::kernels::{ NativeRunningKernel, RemoteRunningKernel, }; use crate::repl_store::ReplStore; + use picker::Picker; use runtimelib::{ExecuteRequest, JupyterMessage, JupyterMessageContent}; use ui::PopoverMenuHandle; +use zed_actions::editor::{MoveDown, MoveUp}; actions!( notebook, @@ -133,17 +135,12 @@ impl NotebookEditor { let mut cell_order = vec![]; // Vec let mut cell_map = HashMap::default(); // HashMap - for (index, cell) in notebook_item - .read(cx) - .notebook - .clone() - .cells - .iter() - .enumerate() - { + let cell_count = notebook_item.read(cx).notebook.cells.len(); + for index in 0..cell_count { + let cell = notebook_item.read(cx).notebook.cells[index].clone(); let cell_id = cell.id(); cell_order.push(cell_id.clone()); - let cell_entity = Cell::load(cell, &languages, notebook_language.clone(), window, cx); + let cell_entity = Cell::load(&cell, &languages, notebook_language.clone(), window, cx); match &cell_entity { Cell::Code(code_cell) => { @@ -235,7 +232,7 @@ impl NotebookEditor { languages: languages.clone(), worktree_id, focus_handle, - notebook_item, + notebook_item: notebook_item.clone(), notebook_language, remote_id: None, cell_list, @@ -243,15 +240,42 @@ impl NotebookEditor { cell_order: cell_order.clone(), original_cell_order: cell_order.clone(), cell_map: cell_map.clone(), - kernel: Kernel::StartingKernel(Task::ready(()).shared()), + kernel: Kernel::Shutdown, // TODO: use recommended kernel after the implementation is done in repl kernel_specification: None, execution_requests: HashMap::default(), kernel_picker_handle: PopoverMenuHandle::default(), }; editor.launch_kernel(window, cx); + editor.refresh_language(cx); + + cx.subscribe(¬ebook_item, |this, _item, _event, cx| { + this.refresh_language(cx); + }) + .detach(); + editor } + fn refresh_language(&mut self, cx: &mut Context) { + let notebook_language = self.notebook_item.read(cx).notebook_language(); + let task = cx.spawn(async move |this, cx| { + let language = notebook_language.await; + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| { + for cell in this.cell_map.values() { + if let Cell::Code(code_cell) = cell { + code_cell.update(cx, |cell, cx| { + cell.set_language(language.clone(), cx); + }); + } + } + }); + } + language + }); + self.notebook_language = task.shared(); + } + fn has_structural_changes(&self) -> bool { self.cell_order != self.original_cell_order } @@ -367,6 +391,28 @@ impl NotebookEditor { self.kernel_specification = Some(spec.clone()); + self.notebook_item.update(cx, |item, cx| { + let kernel_name = spec.name().to_string(); + let language = spec.language().to_string(); + + let display_name = match &spec { + KernelSpecification::Jupyter(s) => s.kernelspec.display_name.clone(), + KernelSpecification::PythonEnv(s) => s.kernelspec.display_name.clone(), + KernelSpecification::Remote(s) => s.kernelspec.display_name.clone(), + }; + + let kernelspec_json = serde_json::json!({ + "display_name": display_name, + "name": kernel_name, + "language": language + }); + + if let Ok(k) = serde_json::from_value(kernelspec_json) { + item.notebook.metadata.kernelspec = Some(k); + cx.emit(()); + } + }); + let kernel_task = match spec { KernelSpecification::Jupyter(local_spec) | KernelSpecification::PythonEnv(local_spec) => NativeRunningKernel::new( @@ -516,7 +562,6 @@ impl NotebookEditor { } fn run_cells(&mut self, window: &mut Window, cx: &mut Context) { - println!("Cells would run here!"); for cell_id in self.cell_order.clone() { self.execute_cell(cell_id, cx); } @@ -550,7 +595,7 @@ impl NotebookEditor { // Discussion can be done on this default implementation /// Moves focus to the next cell, or creates a new code cell if at the end fn move_to_next_cell(&mut self, window: &mut Window, cx: &mut Context) { - if self.selected_cell_index < self.cell_order.len() - 1 { + if !self.cell_order.is_empty() && self.selected_cell_index < self.cell_order.len() - 1 { self.selected_cell_index += 1; // focus the new cell's editor if let Some(cell_id) = self.cell_order.get(self.selected_cell_index) { @@ -591,7 +636,7 @@ impl NotebookEditor { fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context) { println!("Move cell down triggered"); - if self.selected_cell_index < self.cell_order.len() - 1 { + if !self.cell_order.is_empty() && self.selected_cell_index < self.cell_order.len() - 1 { self.cell_order .swap(self.selected_cell_index, self.selected_cell_index + 1); self.selected_cell_index += 1; @@ -616,7 +661,11 @@ impl NotebookEditor { ) }); - let insert_index = self.selected_cell_index + 1; + 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())); @@ -671,7 +720,11 @@ impl NotebookEditor { ) }); - let insert_index = self.selected_cell_index + 1; + 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())); @@ -1164,6 +1217,62 @@ impl Render for NotebookEditor { .on_action( cx.listener(|this, &AddCodeBlock, window, cx| this.add_code_block(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) { + if let Some(cell) = this.cell_map.get(cell_id) { + match cell { + Cell::Code(cell) => { + let editor = cell.read(cx).editor().clone(); + editor.update(cx, |editor, cx| { + editor.move_to_end(&Default::default(), window, cx); + }); + editor.focus_handle(cx).focus(window, cx); + } + Cell::Markdown(cell) => { + cell.update(cx, |cell, cx| { + cell.set_editing(true); + cx.notify(); + }); + let editor = cell.read(cx).editor().clone(); + editor.update(cx, |editor, cx| { + editor.move_to_end(&Default::default(), window, cx); + }); + editor.focus_handle(cx).focus(window, cx); + } + _ => {} + } + } + } + })) + .on_action(cx.listener(|this, _: &MoveDown, window, cx| { + this.select_next(&menu::SelectNext, window, cx); + if let Some(cell_id) = this.cell_order.get(this.selected_cell_index) { + if let Some(cell) = this.cell_map.get(cell_id) { + match cell { + Cell::Code(cell) => { + let editor = cell.read(cx).editor().clone(); + editor.update(cx, |editor, cx| { + editor.move_to_beginning(&Default::default(), window, cx); + }); + editor.focus_handle(cx).focus(window, cx); + } + Cell::Markdown(cell) => { + cell.update(cx, |cell, cx| { + cell.set_editing(true); + cx.notify(); + }); + let editor = cell.read(cx).editor().clone(); + editor.update(cx, |editor, cx| { + editor.move_to_beginning(&Default::default(), window, cx); + }); + editor.focus_handle(cx).focus(window, cx); + } + _ => {} + } + } + } + })) .on_action( cx.listener(|this, action, window, cx| this.restart_kernel(action, window, cx)), ) @@ -1220,31 +1329,43 @@ impl project::ProjectItem for NotebookItem { // todo: watch for changes to the file let file_content = fs.load(abs_path.as_path()).await?; - // Pre-process to ensure IDs exist - let mut json: serde_json::Value = serde_json::from_str(&file_content)?; - if let Some(cells) = json.get_mut("cells").and_then(|c| c.as_array_mut()) { - for cell in cells { - if cell.get("id").is_none() { - cell["id"] = serde_json::Value::String(Uuid::new_v4().to_string()); - } + let notebook = if file_content.trim().is_empty() { + nbformat::v4::Notebook { + nbformat: 4, + nbformat_minor: 5, + cells: vec![], + metadata: serde_json::from_str("{}").unwrap(), } - } - let file_content = serde_json::to_string(&json)?; - - let notebook = nbformat::parse_notebook(&file_content); + } else { + let notebook = match nbformat::parse_notebook(&file_content) { + Ok(nb) => nb, + Err(_) => { + // Pre-process to ensure IDs exist + let mut json: serde_json::Value = serde_json::from_str(&file_content)?; + if let Some(cells) = + json.get_mut("cells").and_then(|c| c.as_array_mut()) + { + for cell in cells { + if cell.get("id").is_none() { + cell["id"] = + serde_json::Value::String(Uuid::new_v4().to_string()); + } + } + } + let file_content = serde_json::to_string(&json)?; + nbformat::parse_notebook(&file_content)? + } + }; - let notebook = match notebook { - Ok(nbformat::Notebook::V4(notebook)) => notebook, - // 4.1 - 4.4 are converted to 4.5 - Ok(nbformat::Notebook::Legacy(legacy_notebook)) => { - // TODO: Decide if we want to mutate the notebook by including Cell IDs - // and any other conversions + match notebook { + nbformat::Notebook::V4(notebook) => notebook, + // 4.1 - 4.4 are converted to 4.5 + nbformat::Notebook::Legacy(legacy_notebook) => { + // TODO: Decide if we want to mutate the notebook by including Cell IDs + // and any other conversions - nbformat::upgrade_legacy_notebook(legacy_notebook)? - } - // Bad notebooks and notebooks v4.0 and below are not supported - Err(e) => { - anyhow::bail!("Failed to parse notebook: {:?}", e); + nbformat::upgrade_legacy_notebook(legacy_notebook)? + } } }; @@ -1310,6 +1431,8 @@ impl NotebookItem { } } +impl EventEmitter<()> for NotebookItem {} + impl EventEmitter<()> for NotebookEditor {} // pub struct NotebookControls { @@ -1570,6 +1693,20 @@ impl KernelSession for NotebookEditor { cx.notify(); } + if let JupyterMessageContent::KernelInfoReply(reply) = &message.content { + self.kernel.set_kernel_info(reply); + + if let Ok(language_info) = serde_json::from_value::( + serde_json::to_value(&reply.language_info).unwrap(), + ) { + self.notebook_item.update(cx, |item, cx| { + item.notebook.metadata.language_info = Some(language_info); + cx.emit(()); + }); + } + cx.notify(); + } + // Handle cell-specific messages if let Some(parent_header) = &message.parent_header { if let Some(cell_id) = self.execution_requests.get(&parent_header.msg_id) { diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index d3fe60d3cc3d8bd981fe9a6c37ab5a65be7e707f..8415462595cb93a19365a929660b4e8e3f78f8d8 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -152,6 +152,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ "jxl", "png", "psd", "qoi", "svg", "tiff", "webp", ], ), + ("ipynb", &["ipynb"]), ("java", &["java"]), ("javascript", &["cjs", "js", "mjs"]), ("json", &["json", "jsonc"]), @@ -318,6 +319,7 @@ const FILE_ICONS: &[(&str, &str)] = &[ ("heroku", "icons/file_icons/heroku.svg"), ("html", "icons/file_icons/html.svg"), ("image", "icons/file_icons/image.svg"), + ("ipynb", "icons/file_icons/jupyter.svg"), ("java", "icons/file_icons/java.svg"), ("javascript", "icons/file_icons/javascript.svg"), ("json", "icons/file_icons/code.svg"),