diff --git a/Cargo.lock b/Cargo.lock index 3cac0269ba27a4..a771101b88c87e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6344,6 +6344,7 @@ dependencies = [ "command_palette", "command_palette_hooks", "editor", + "futures 0.3.28", "gpui", "indoc", "itertools 0.11.0", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 97eef0d804ba18..02e864964d21d6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1270,7 +1270,6 @@ impl Language { let tree = grammar.parse_text(text, None); let captures = SyntaxSnapshot::single_tree_captures(range.clone(), text, &tree, self, |grammar| { - log::info!("Did capture"); grammar.highlights_query.as_ref() }); let highlight_maps = vec![grammar.highlight_map()]; diff --git a/crates/notebook/Cargo.toml b/crates/notebook/Cargo.toml index 0ac7c9e17411c1..c8d7661b2c0e1c 100644 --- a/crates/notebook/Cargo.toml +++ b/crates/notebook/Cargo.toml @@ -19,6 +19,7 @@ async-compat = { version = "0.2.1", "optional" = true } collections.workspace = true command_palette_hooks.workspace = true editor.workspace = true +futures.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true diff --git a/crates/notebook/src/cell.rs b/crates/notebook/src/cell.rs index b5799847a00615..5528fe4d963515 100644 --- a/crates/notebook/src/cell.rs +++ b/crates/notebook/src/cell.rs @@ -1,27 +1,32 @@ +// use crate::common::python_lang; use anyhow::{anyhow, Result}; use collections::HashMap; use editor::ExcerptId; -use gpui::{AppContext, Flatten, Model, WeakModel}; +use gpui::{AppContext, AsyncAppContext, Flatten, Model, WeakModel}; +use itertools::Itertools; use language::{Buffer, File}; - use project::Project; +use rope::Rope; use runtimelib::media::MimeType; use serde::{de::Visitor, Deserialize}; -use std::{any::Any, fmt::Debug, sync::Arc}; +use serde_json::Value; +use std::{any::Any, fmt::Debug, path::PathBuf, sync::Arc}; use sum_tree::{SumTree, Summary}; -use ui::Context; +use ui::{Context, ViewContext}; -use crate::common::python_lang; +use crate::editor::NotebookEditor; #[derive(Clone, Debug)] pub struct Cell { pub id: CellId, // `cell_id` is a notebook field - cell_id: Option, - cell_type: CellType, - metadata: HashMap, + pub cell_id: Option, + pub cell_type: CellType, + pub metadata: HashMap, pub source: Model, - execution_count: Option, + pub execution_count: Option, + pub outputs: Option>, + pub output_actions: Option>, } pub struct CellBuilder { @@ -31,6 +36,8 @@ pub struct CellBuilder { metadata: Option>, source: Option>, execution_count: Option, + outputs: Option>, + output_actions: Option>, } impl Debug for CellBuilder { @@ -40,7 +47,7 @@ impl Debug for CellBuilder { } // A phony file struct we use for excerpt titles. -struct PhonyFile { +pub(crate) struct PhonyFile { worktree_id: usize, title: Arc, cell_idx: CellId, @@ -93,10 +100,37 @@ impl File for PhonyFile { } } +pub(crate) fn cell_tab_title( + idx: u64, + cell_id: Option<&String>, + cell_type: &CellType, + for_output: bool, +) -> PhonyFile { + // Not sure why we need to reverse for the desired formatting, but there you go + let path_buf: PathBuf = match for_output { + false => [format!("Cell {idx}"), format!("({:#?})", cell_type)] + .iter() + .rev() + .map(|s| s.as_str()) + .collect(), + true => [format!("Cell {idx}"), "[Output]".to_string()] + .iter() + .rev() + .map(|s| s.as_str()) + .collect(), + }; + PhonyFile { + worktree_id: 0, + title: Arc::from(path_buf.as_path()), + cell_idx: CellId::from(idx), + cell_id: cell_id.map(|id| id.clone()), + } +} + impl CellBuilder { pub fn new( project_handle: &mut WeakModel, - cx: &mut AppContext, + cx: &mut AsyncAppContext, id: u64, map: serde_json::Map, ) -> CellBuilder { @@ -107,6 +141,8 @@ impl CellBuilder { metadata: None, source: None, execution_count: None, + outputs: None, + output_actions: None, }; for (key, val) in map { @@ -118,7 +154,6 @@ impl CellBuilder { } "metadata" => this.metadata = serde_json::from_value(val).unwrap_or_default(), "source" => { - let language = python_lang(cx); let source_lines: Vec = match val { serde_json::Value::String(src) => Ok([src].into()), serde_json::Value::Array(src_lines) => src_lines.into_iter().try_fold( @@ -134,21 +169,20 @@ impl CellBuilder { Ok(source_lines) }, ), - _ => Err(anyhow::anyhow!("Unexpected source format: {:#?}", val)), + _ => Err(anyhow!("Unexpected source format: {:#?}", val)), }?; project_handle .update(cx, |project, project_cx| -> Result<()> { - // TODO: Detect this from the file. Also, get it to work. - + let mut source_text = source_lines.join("\n"); + source_text.push_str("\n"); let source_buffer = project.create_buffer( - source_lines.join("\n").as_str(), - Some(language), + source_text.as_str(), + None, project_cx, )?; this.source.replace(source_buffer); - Ok(()) }) .flatten()?; @@ -156,22 +190,63 @@ impl CellBuilder { "execution_count" => { this.execution_count = serde_json::from_value(val).unwrap_or_default() } + "outputs" => { + // TODO: Validate `cell_type == 'code'` + log::debug!("Cell output value: {:#?}", val); + let outputs = serde_json::from_value::>>(val)?; + log::debug!("Parsed cell output as: {:#?}", outputs); + this.outputs = outputs; + this.output_actions = match this.outputs.as_deref() { + Some([]) => None, + Some(outputs) => { + let output_actions = outputs + .iter() + .filter_map(|output| { + use IpynbCodeOutput::*; + match output { + Stream { name, text } => { + log::info!("Output text: {:#?}", text); + match name { + StreamOutputTarget::Stdout => Some( + OutputHandler::print(Some(text.clone())), + ), + StreamOutputTarget::Stderr => { + Some(OutputHandler::print(None)) + } + } + } + DisplayData { data, metadata } => { + Some(OutputHandler::print(None)) + } + ExecutionResult { + // TODO: Handle MIME types here + execution_count, + data, + metadata, + } => Some(OutputHandler::print(None)), + } + }) + .collect_vec(); + Some(output_actions) + } + None => None, + }; + } _ => {} }; - let title_text = format!("Cell {:#?}", id); - let title = PhonyFile { - worktree_id: 0, - title: Arc::from(std::path::Path::new(title_text.as_str())), - cell_idx: CellId(id), - cell_id: this.cell_id.clone(), - }; - - if let Some(buffer_handle) = &this.source { - cx.update_model(&buffer_handle, |buffer, cx| { - buffer.file_updated(Arc::new(title), cx); - }); - } + let cell_id = this.cell_id.clone(); + let cell_type = this.cell_type.clone(); + (|| -> Option<()> { + let title = cell_tab_title(id, (&cell_id).as_ref(), &cell_type?, false); + if let Some(buffer_handle) = &this.source { + cx.update_model(&buffer_handle, |buffer, cx| { + buffer.file_updated(Arc::new(title), cx); + }) + .ok()?; + }; + Some(()) + })(); Ok(()) })(); @@ -190,14 +265,18 @@ impl CellBuilder { } pub fn build(self) -> Cell { - Cell { + let cell = Cell { id: CellId(self.id), cell_id: self.cell_id, cell_type: self.cell_type.unwrap(), metadata: self.metadata.unwrap(), source: self.source.unwrap(), execution_count: self.execution_count, - } + outputs: self.outputs, + output_actions: self.output_actions, + }; + + cell } } @@ -259,6 +338,15 @@ impl Cells { } } +impl Clone for Cells { + fn clone(&self) -> Self { + self.iter().fold(Cells::default(), |mut cells, cell| { + cells.push_cell(cell.clone(), &()); + cells + }) + } +} + impl sum_tree::Item for Cell { type Summary = CellSummary; @@ -274,6 +362,12 @@ impl sum_tree::Item for Cell { #[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)] pub struct CellId(u64); +impl From for CellId { + fn from(id: u64) -> Self { + CellId(id) + } +} + impl From for CellId { fn from(excerpt_id: ExcerptId) -> Self { CellId(excerpt_id.to_proto()) @@ -304,23 +398,29 @@ impl Summary for CellSummary { fn add_summary(&mut self, summary: &Self, cx: &Self::Context) {} } +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum StreamOutputText { + Text(String), + MultiLineText(Vec), +} + // https://nbformat.readthedocs.io/en/latest/format_description.html#code-cell-outputs -// TODO: Better typing for `output_type` -#[derive(Deserialize)] -enum IpynbCodeOutput { +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "output_type")] +pub enum IpynbCodeOutput { + #[serde(alias = "stream")] Stream { - output_type: String, name: StreamOutputTarget, - // text: Rope, - text: String, + text: StreamOutputText, }, + #[serde(alias = "display_data")] DisplayData { - output_type: String, data: HashMap, metadata: HashMap>, }, + #[serde(alias = "execute_result")] ExecutionResult { - output_type: String, execution_count: usize, data: HashMap, metadata: HashMap>, @@ -328,7 +428,8 @@ enum IpynbCodeOutput { } // https://nbformat.readthedocs.io/en/latest/format_description.html#display-data -#[derive(Deserialize)] +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] pub enum MimeData { MultiLineText(Vec), B64EncodedMultiLineText(Vec), @@ -336,10 +437,60 @@ pub enum MimeData { } // TODO: Appropriate deserialize from string value -#[derive(Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub enum StreamOutputTarget { + #[serde(alias = "stdout")] Stdout, + #[serde(alias = "stderr")] Stderr, } -enum JupyterServerEvent {} +pub enum JupyterServerEvent {} + +#[derive(Clone, Debug)] +pub enum OutputHandler { + Print(Option), +} + +impl OutputHandler { + pub fn print(text: Option) -> OutputHandler { + use StreamOutputText::*; + match text { + Some(Text(text)) => OutputHandler::Print(Some(text.to_string())), + Some(MultiLineText(text)) => { + let output_text = text + .iter() + .map(|line| line.strip_suffix("\n").unwrap_or(line).to_string()) + .join("\n"); + OutputHandler::Print(Some(output_text)) + } + None => OutputHandler::Print(None), + } + } + + pub fn as_rope(&self) -> Option { + let OutputHandler::Print(Some(text)) = self else { + return None; + }; + let mut out = Rope::new(); + out.push(text.as_str()); + Some(out) + } + + pub fn to_output_buffer(self, cx: &mut ViewContext) -> Option> { + let OutputHandler::Print(Some(text)) = self else { + return None; + }; + Some(cx.new_model(|cx| Buffer::local(text, cx))) + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct KernelSpec { + pub argv: Option>, + pub display_name: String, + pub language: String, + pub interrup_mode: Option, + pub env: Option>, + pub metadata: Option>, +} diff --git a/crates/notebook/src/common.rs b/crates/notebook/src/common.rs index 04afefff923266..f20a7a0823a4ef 100644 --- a/crates/notebook/src/common.rs +++ b/crates/notebook/src/common.rs @@ -1,11 +1,7 @@ -use std::sync::Arc; +use log::error; -use gpui::AppContext; -use language::{BracketPair, BracketPairConfig, Language, LanguageConfig, LanguageMatcher}; -use regex::Regex; +use anyhow::anyhow; use serde::de::DeserializeOwned; -use tree_sitter_python; -use ui::{ActiveTheme, Context, ViewContext}; pub(crate) fn parse_value( val: serde_json::Value, @@ -13,10 +9,15 @@ pub(crate) fn parse_value( serde_json::from_value::(val).map_err(|err| E::custom(err.to_string())) } -// TODO: Figure out how to obtain a language w/ properly specified grammar -// without relying on private functions. -pub(crate) fn python_lang(cx: &AppContext) -> Arc { - let lang = languages::language("python", tree_sitter_python::language()); - lang.set_theme(cx.theme().syntax()); - lang +// TODO: For cleaning things up +pub(crate) fn forward_err_with<'f, E, F>(format: F) -> Box anyhow::Error + 'f> +where + E: Into, + F: FnOnce(E) -> String + 'f, +{ + Box::new(move |err| { + let err = anyhow!(format(err)); + error!("{:#?}", err); + err + }) } diff --git a/crates/notebook/src/editor.rs b/crates/notebook/src/editor.rs index acc93b2c5441c5..2556e46046ff56 100644 --- a/crates/notebook/src/editor.rs +++ b/crates/notebook/src/editor.rs @@ -1,22 +1,24 @@ //! Jupyter support for Zed. +use collections::HashMap; use editor::{ items::entry_label_color, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, MAX_TAB_TITLE_LEN, }; use gpui::{ - AnyView, AppContext, Context, EventEmitter, FocusHandle, FocusableView, HighlightStyle, Model, - ParentElement, Subscription, View, + AnyView, AppContext, Context, EventEmitter, FocusHandle, FocusableView, Model, ParentElement, + Subscription, View, }; -use itertools::Itertools; -use language::{self, Buffer, Capability, HighlightId}; +use language::{self, Buffer, Capability}; +use log::info; use project::{self, Project}; +use rope::Rope; use std::{ any::{Any, TypeId}, convert::AsRef, ops::Range, + sync::Arc, }; -use theme::{ActiveTheme, SyntaxTheme}; use ui::{ div, h_flex, FluentBuilder, InteractiveElement, IntoElement, Label, LabelCommon, Render, SharedString, Styled, ViewContext, VisualContext, @@ -27,7 +29,7 @@ use workspace::item::{ItemEvent, ItemHandle}; use crate::{ actions, - cell::{Cell, CellId}, + cell::{cell_tab_title, CellId}, Notebook, }; @@ -40,41 +42,72 @@ pub struct NotebookEditor { impl NotebookEditor { fn new(project: Model, notebook: Model, cx: &mut ViewContext) -> Self { - let cells = notebook - .read(cx) - .cells + let cells = notebook.read(cx).cells.clone(); + + let mut output_buffers_by_id: HashMap> = cells .iter() - .map(|cell| cell.clone()) - .collect_vec(); + .filter_map(|cell| { + let mut output_text = Rope::new(); + for action in cell.output_actions.as_ref()? { + output_text.append(action.as_rope()?); + } + let buffer = cx.new_model(|cx| { + let title = cell_tab_title( + cell.id.into(), + cell.cell_id.as_ref(), + &cell.cell_type, + true, + ); + let mut buffer = Buffer::local(output_text.to_string(), cx); + buffer.file_updated(Arc::from(title), cx); + buffer + }); + Some((cell.id, buffer)) + }) + .collect(); + + info!("{:#?}", output_buffers_by_id); let multi = cx.new_model(|model_cx| { let mut multi = MultiBuffer::new(0, Capability::ReadWrite); - for (cell, range) in cells - .into_iter() - .map(|cell| { + + let mut prev_excerpt_id = ExcerptId::min(); + for cell in cells.iter() { + let id: u64 = prev_excerpt_id.to_proto() + 1; + + // Handle source buffer + let range = ExcerptRange { + context: Range { + start: 0 as usize, + end: cell.source.read(model_cx).len() as _, + }, + primary: None, + }; + multi.insert_excerpts_with_ids_after( + prev_excerpt_id, + cell.source.clone(), + vec![(ExcerptId::from_proto(id), range)], + model_cx, + ); + prev_excerpt_id = ExcerptId::from_proto(id); + + // Handle output buffer if present + if let Some(output_buffer) = output_buffers_by_id.remove(&cell.id) { let range = ExcerptRange { context: Range { start: 0 as usize, - end: cell.source.read(model_cx).len() as _, + end: output_buffer.read(model_cx).len() as _, }, primary: None, }; - (cell, range) - }) - .collect_vec() - { - let id: u64 = cell.id.into(); - let prev_excerpt_id = if id == 1 { - ExcerptId::min() - } else { - ExcerptId::from_proto(id - 1) - }; - multi.insert_excerpts_with_ids_after( - prev_excerpt_id, - cell.source, - vec![(cell.id.into(), range)], - model_cx, - ); + multi.insert_excerpts_with_ids_after( + prev_excerpt_id, + output_buffer, + vec![(ExcerptId::from_proto(id + 1), range)], + model_cx, + ); + prev_excerpt_id = ExcerptId::from_proto(id + 1); + } } multi @@ -105,7 +138,12 @@ impl NotebookEditor { cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { cx.emit(event.clone()); - log::info!("Event: {:#?}", event); + match event { + EditorEvent::ScrollPositionChanged { local, autoscroll } => {} + _ => { + log::info!("Event: {:#?}", event); + } + } }) .detach(); @@ -119,103 +157,6 @@ impl NotebookEditor { this } - fn highlight_syntax( - &mut self, - only_for_excerpt_ids: Option>, - cx: &mut ViewContext, - ) { - let mut highlights_by_range = Vec::<(Range, HighlightId)>::default(); - - self.editor.update(cx, |editor, cx| { - editor.buffer().read_with(cx, |multi, cx| { - // This is somewhat inefficient but OK for now. - multi.for_each_buffer(|buffer_handle| { - if only_for_excerpt_ids.is_some() - && (!only_for_excerpt_ids.as_ref().unwrap().iter().any(|id| { - multi - .excerpts_for_buffer(buffer_handle, cx) - .iter() - .map(|(id, _)| id) - .contains(id) - })) - { - return; - } - buffer_handle.read_with(cx, |buffer, cx| { - highlights_by_range.extend(self.get_highlight_ids_for_buffer(buffer, cx)?); - Some(()) - }); - }); - }); - }); - - self.editor.update(cx, |editor, cx| { - let styles_by_range = NotebookEditor::get_highlight_styles_for_multi( - editor, - highlights_by_range, - &only_for_excerpt_ids, - cx, - ); - - for (rng, style) in styles_by_range { - editor.highlight_text::(vec![rng], style, cx); - } - }); - } - - fn get_highlight_ids_for_buffer( - &self, - buffer: &Buffer, - cx: &AppContext, - ) -> Option, HighlightId)>> { - let lang = self.notebook.read(cx).language.clone()?; - let highlights_by_range = lang - .highlight_text(buffer.as_rope(), 0..buffer.len()) - .into_iter() - .collect_vec(); - - Some(highlights_by_range) - } - - fn get_highlight_styles_for_multi( - editor: &Editor, - highlights_by_range: Vec<(Range, HighlightId)>, - only_for_excerpt_ids: &Option>, - cx: &ViewContext, - ) -> Vec<(Range, HighlightStyle)> { - let multi = editor.buffer().read(cx); - let syntax = cx.theme().syntax().as_ref().clone(); - - highlights_by_range - .iter() - .flat_map(|(range, h)| { - multi - .range_to_buffer_ranges(range.clone(), cx) - .iter() - .filter_map(|(_buffer, range, excerpt_id)| { - if only_for_excerpt_ids.is_some() - && (!only_for_excerpt_ids - .as_ref() - .unwrap() - .iter() - .any(|id| id == excerpt_id)) - { - return None; - } - let range = Range { - start: multi.snapshot(cx).anchor_before(range.start), - end: multi.snapshot(cx).anchor_before(range.end), - }; - let style = h.style(&syntax)?; - Some((range, style)) - }) - .collect_vec() - }) - .collect_vec() - } - - fn expand_output_cells(&mut self, cx: &mut ViewContext) {} - fn run_current_cell(&mut self, _: &actions::RunCurrentCell, cx: &mut ViewContext) { let (excerpt_id, buffer_handle, _range) = match self.editor.read(cx).active_excerpt(cx) { Some(data) => data, @@ -355,9 +296,7 @@ impl workspace::item::ProjectItem for NotebookEditor { where Self: Sized, { - let mut nb_editor = NotebookEditor::new(project, notebook, cx); - nb_editor.highlight_syntax(None, cx); - nb_editor + NotebookEditor::new(project, notebook, cx) } } diff --git a/crates/notebook/src/notebook.rs b/crates/notebook/src/notebook.rs index 85b85fd966e66e..00cec77efaff23 100644 --- a/crates/notebook/src/notebook.rs +++ b/crates/notebook/src/notebook.rs @@ -3,17 +3,19 @@ pub mod cell; mod common; pub mod editor; -use crate::cell::{CellId, Cells}; -use crate::common::{parse_value, python_lang}; +use crate::cell::{Cells, KernelSpec}; +use crate::common::{forward_err_with, parse_value}; +use anyhow::anyhow; use cell::CellBuilder; use collections::HashMap; -use gpui::{Context, ModelContext, WeakModel}; +use gpui::{AsyncAppContext, Context, WeakModel}; use language::Language; use log::{error, info}; use serde::de::{self, DeserializeSeed, Error, Visitor}; +use serde_json::Value; use std::{io::Read, num::NonZeroU64, sync::Arc}; -use project::{self, ProjectPath}; +use project::{self, Project, ProjectPath}; use worktree::File; // https://nbformat.readthedocs.io/en/latest/format_description.html#top-level-structure @@ -21,18 +23,82 @@ use worktree::File; pub struct Notebook { file: Option>, language: Option>, - metadata: Option>, + pub metadata: Option>, // TODO: Alias `nbformat` and `nbformat_minor` to include `_version` suffix for clarity - nbformat: usize, - nbformat_minor: usize, - cells: Cells, + pub nbformat: usize, + pub nbformat_minor: usize, + pub cells: Cells, +} + +impl Notebook { + fn kernel_spec(&self) -> Option { + self.metadata.clone().and_then(|metadata| { + Some(serde_json::from_value(metadata.get("kernel_spec")?.clone()).ok()?) + }) + } + + async fn try_set_source_languages<'cx>( + &mut self, + project: &WeakModel, + cx: &mut AsyncAppContext, + ) -> anyhow::Result>> { + let Some(kernel_spec) = (&self.metadata).as_ref().and_then(|metadata| { + log::info!("NotebookBuilder.metadata: {:#?}", metadata); + serde_json::from_value::(metadata.get("kernelspec")?.clone()).ok() + }) else { + return Err(anyhow::anyhow!("No kernel spec")); + }; + + log::info!("kernel_spec: {:#?}", kernel_spec); + + let cloned_project = project.clone(); + let language = cx + .spawn(|cx| async move { + let language = match kernel_spec.language.as_str() { + "python" => cloned_project.read_with(&cx, |project, cx| { + let languages = project.languages(); + log::info!("Available languages: {:#?}", languages.language_names()); + + languages.language_for_name("Python") + }), + _ => Err(anyhow::anyhow!("Failed to get language")), + }? + .await; + + language + }) + .await; + + self.language = language.ok().inspect(|lang| { + match (|| -> anyhow::Result<()> { + let handle = &project + .upgrade() + .ok_or_else(|| anyhow::anyhow!("Cannot upgrade project"))?; + + cx.update_model(handle, |project, cx| { + for cell in self.cells.iter() { + project.set_language_for_buffer(&cell.source, lang.clone(), cx) + } + }) + })() { + Ok(_) => log::info!("Successfully set languages for all source buffers"), + Err(err) => error!( + "Failed to set language for at least one source buffer: {:#?}", + err + ), + } + }); + match &self.language { + Some(lang) => Ok(Some(lang.clone())), + None => Ok(None), + } + } } -struct NotebookBuilder<'nbb, 'cx: 'nbb> { +struct NotebookBuilder<'cx> { project_handle: WeakModel, file: Option>, - language: Option>, - cx: &'nbb mut ModelContext<'cx, Notebook>, + cx: &'cx mut AsyncAppContext, metadata: Option>, // TODO: Alias `nbformat` and `nbformat_minor` to include `_version` suffix for clarity nbformat: Option, @@ -40,17 +106,15 @@ struct NotebookBuilder<'nbb, 'cx: 'nbb> { cells: Cells, } -impl<'nbb, 'cx: 'nbb> NotebookBuilder<'nbb, 'cx> { +impl<'cx> NotebookBuilder<'cx> { fn new( project_handle: WeakModel, file: Option>, - language: Option>, - cx: &'nbb mut ModelContext<'cx, Notebook>, - ) -> NotebookBuilder<'nbb, 'cx> { + cx: &'cx mut AsyncAppContext, + ) -> NotebookBuilder<'cx> { NotebookBuilder { project_handle, file, - language, cx, metadata: None, nbformat: None, @@ -59,24 +123,26 @@ impl<'nbb, 'cx: 'nbb> NotebookBuilder<'nbb, 'cx> { } } - fn build(self) -> Notebook { - Notebook { + async fn build(mut self) -> Notebook { + let mut notebook = Notebook { file: self.file, - language: self.language, + language: None, metadata: self.metadata, nbformat: self.nbformat.unwrap(), nbformat_minor: self.nbformat_minor.unwrap(), cells: self.cells, - } + }; + + notebook + .try_set_source_languages(&self.project_handle, &mut self.cx) + .await; + + notebook } } -impl<'nbb, 'cx, 'de> Visitor<'de> for NotebookBuilder<'nbb, 'cx> -where - 'cx: 'nbb, - 'nbb: 'de, -{ - type Value = NotebookBuilder<'nbb, 'cx>; +impl<'cx, 'de: 'cx> Visitor<'de> for NotebookBuilder<'cx> { + type Value = NotebookBuilder<'cx>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { write!( @@ -108,7 +174,7 @@ where let cell_builder = CellBuilder::new(&mut self.project_handle, &mut self.cx, id, item); - self.cells.push_cell(cell_builder.build(), &()); + self.cells.push_cell(cell_builder.build(), &()) } } _ => {} @@ -130,12 +196,8 @@ where } } -impl<'nbb, 'cx, 'de> DeserializeSeed<'de> for NotebookBuilder<'nbb, 'cx> -where - 'cx: 'nbb, - 'nbb: 'de, -{ - type Value = NotebookBuilder<'nbb, 'cx>; +impl<'cx, 'de: 'cx> DeserializeSeed<'de> for NotebookBuilder<'cx> { + type Value = NotebookBuilder<'cx>; fn deserialize(self, deserializer: D) -> Result where @@ -162,44 +224,61 @@ impl project::Item for Notebook { } info!("Detected `.ipynb` extension for path `{:#?}`", path); - let language = python_lang(app_cx); - - project_handle.update(app_cx, |project, cx| { - let Some(worktree) = project.worktree_for_id(path.worktree_id, cx) else { - return; - }; - project.start_language_servers(&worktree, language.clone(), cx); - }); - let project = project_handle.downgrade(); - let open_buffer_task = - project.update(app_cx, |project, cx| project.open_buffer(path.clone(), cx)); + let cloned_path = path.clone(); let task = app_cx.spawn(|mut cx| async move { - let buffer_handle = open_buffer_task?.await?; + let buffer_handle = project + .update(&mut cx, |project, cx| { + project.open_buffer(cloned_path.clone(), cx) + }) + .map_err(|err| anyhow::anyhow!("Failed to open file: {:#?}", err))? + .await + .inspect(|_| { + info!( + "Successfully opened notebook file from path `{:#?}`", + cloned_path + ) + })?; - info!("Successfully opened buffer"); + let project_clone = project.clone(); - cx.new_model(move |cx_model| { - let buffer = buffer_handle.read(cx_model); - let mut bytes = Vec::::with_capacity(buffer.len()); + let (bytes, file) = buffer_handle + .read_with(&cx, |buffer, cx| { + let mut bytes = Vec::::with_capacity(buffer.len()); + let file = (|| { + buffer + .bytes_in_range(0..buffer.len()) + .read_to_end(&mut bytes) + .inspect(|n_bytes_read| { + info!("Successfully read {n_bytes_read} bytes from notebook file") + }) + .ok()?; - let n_bytes_maybe_read = buffer - .bytes_in_range(0..buffer.len()) - .read_to_end(&mut bytes); + buffer.file().map(|file| file.clone()) + })(); - match n_bytes_maybe_read { - Ok(n_bytes) => info!("Successfully read {} bytes from notebook file", n_bytes), - Err(err) => error!("Failed to read from notebook file: {:#?}", err), - } + (bytes, file) + }) + .map_err(forward_err_with(|err| { + format!( + "Failed to read notebook from notebook file `{:#?}`: {:#?}", + cloned_path, err + ) + }))?; - let file = buffer.file().map(|file| file.clone()); - let mut deserializer = serde_json::Deserializer::from_slice(&bytes); - NotebookBuilder::new(project, file, Some(language), cx_model) - .deserialize(&mut deserializer) - .map(|builder| builder.build()) - .unwrap_or_default() - }) + let mut deserializer = serde_json::Deserializer::from_slice(&bytes); + let builder = NotebookBuilder::new(project_clone, file, &mut cx) + .deserialize(&mut deserializer) + .map_err(forward_err_with(|err| { + format!( + "Failed to deserialize notebook from path `{:#?}`: {:#?}", + cloned_path, err + ) + }))?; + + let notebook = builder.build().await; + cx.new_model(move |_| notebook) }); Some(task)