#![allow(unused, dead_code)]use std::future::Future;use std::{path::PathBuf, sync::Arc}; use anyhow::{Context as _, Result};use client::proto::ViewId;use collections::HashMap;use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag};use futures::future::Shared;use futures::FutureExt;use gpui::{ actions, list, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListScrollEvent, ListState, Model, Point, Task, View,};use language::{Language, LanguageRegistry};use project::{Project, ProjectEntryId, ProjectPath};use ui::{prelude::*, Tooltip};use workspace::item::{ItemEvent, TabContentParams};use workspace::searchable::SearchableItemHandle;use workspace::{Item, ItemHandle, ProjectItem, ToolbarItemLocation};use workspace::{ToolbarItemEvent, ToolbarItemView}; use super::{Cell, CellPosition, RenderableCell}; use nbformat::v4::CellId;use nbformat::v4::Metadata as NotebookMetadata; actions!( notebook, [ OpenNotebook, RunAll, ClearOutputs, MoveCellUp, MoveCellDown, AddMarkdownBlock, AddCodeBlock, ]); pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0;pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0;pub(crate) const MEDIUM_SPACING_SIZE: f32 = 12.0;pub(crate) const LARGE_SPACING_SIZE: f32 = 16.0;pub(crate) const GUTTER_WIDTH: f32 = 19.0;pub(crate) const CODE_BLOCK_INSET: f32 = MEDIUM_SPACING_SIZE;pub(crate) const CONTROL_SIZE: f32 = 20.0; pub fn init(cx: &mut AppContext) { if cx.has_flag::<NotebookFeatureFlag>() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() { workspace::register_project_item::<NotebookEditor>(cx); } cx.observe_flag::<NotebookFeatureFlag, _>({ move |is_enabled, cx| { if is_enabled { workspace::register_project_item::<NotebookEditor>(cx); } else { // todo: there is no way to unregister a project item, so if the feature flag // gets turned off they need to restart Zed. } } }) .detach();} pub struct NotebookEditor { languages: Arc<LanguageRegistry>, project: Model<Project>, focus_handle: FocusHandle, notebook_item: Model<NotebookItem>, remote_id: Option<ViewId>, cell_list: ListState, selected_cell_index: usize, cell_order: Vec<CellId>, cell_map: HashMap<CellId, Cell>,} impl NotebookEditor { pub fn new( project: Model<Project>, notebook_item: Model<NotebookItem>, cx: &mut ViewContext<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let languages = project.read(cx).languages().clone(); let language_name = notebook_item.read(cx).language_name(); let notebook_language = notebook_item.read(cx).notebook_language(); let notebook_language = cx.spawn(|_, _| notebook_language).shared(); let mut cell_order = vec![]; // Vec<CellId> let mut cell_map = HashMap::default(); // HashMap<CellId, Cell> for (index, cell) in notebook_item .read(cx) .notebook .clone() .cells .iter() .enumerate() { let cell_id = cell.id(); cell_order.push(cell_id.clone()); cell_map.insert( cell_id.clone(), Cell::load(cell, &languages, notebook_language.clone(), cx), ); } let view = cx.view().downgrade(); let cell_count = cell_order.len(); let this = cx.view(); let cell_list = ListState::new( cell_count, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { view.upgrade() .and_then(|notebook_handle| { notebook_handle.update(cx, |notebook, cx| { notebook .cell_order .get(ix) .and_then(|cell_id| notebook.cell_map.get(cell_id)) .map(|cell| notebook.render_cell(ix, cell, cx).into_any_element()) }) }) .unwrap_or_else(|| div().into_any()) }, ); Self { project, languages: languages.clone(), focus_handle, notebook_item, remote_id: None, cell_list, selected_cell_index: 0, cell_order: cell_order.clone(), cell_map: cell_map.clone(), } } fn has_outputs(&self, cx: &ViewContext<Self>) -> bool { self.cell_map.values().any(|cell| { if let Cell::Code(code_cell) = cell { code_cell.read(cx).has_outputs() } else { false } }) } fn clear_outputs(&mut self, cx: &mut ViewContext<Self>) { for cell in self.cell_map.values() { if let Cell::Code(code_cell) = cell { code_cell.update(cx, |cell, _cx| { cell.clear_outputs(); }); } } } fn run_cells(&mut self, cx: &mut ViewContext<Self>) { println!("Cells would all run here, if that was implemented!"); } fn open_notebook(&mut self, _: &OpenNotebook, _cx: &mut ViewContext<Self>) { println!("Open notebook triggered"); } fn move_cell_up(&mut self, cx: &mut ViewContext<Self>) { println!("Move cell up triggered"); } fn move_cell_down(&mut self, cx: &mut ViewContext<Self>) { println!("Move cell down triggered"); } fn add_markdown_block(&mut self, cx: &mut ViewContext<Self>) { println!("Add markdown block triggered"); } fn add_code_block(&mut self, cx: &mut ViewContext<Self>) { println!("Add code block triggered"); } fn cell_count(&self) -> usize { self.cell_map.len() } fn selected_index(&self) -> usize { self.selected_cell_index } pub fn set_selected_index( &mut self, index: usize, jump_to_index: bool, cx: &mut ViewContext<Self>, ) { // let previous_index = self.selected_cell_index; self.selected_cell_index = index; let current_index = self.selected_cell_index; // in the future we may have some `on_cell_change` event that we want to fire here if jump_to_index { self.jump_to_cell(current_index, cx); } } pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) { let count = self.cell_count(); if count > 0 { let index = self.selected_index(); let ix = if index == count - 1 { count - 1 } else { index + 1 }; self.set_selected_index(ix, true, cx); cx.notify(); } } pub fn select_previous(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) { let count = self.cell_count(); if count > 0 { let index = self.selected_index(); let ix = if index == 0 { 0 } else { index - 1 }; self.set_selected_index(ix, true, cx); cx.notify(); } } pub fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) { let count = self.cell_count(); if count > 0 { self.set_selected_index(0, true, cx); cx.notify(); } } pub fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) { let count = self.cell_count(); if count > 0 { self.set_selected_index(count - 1, true, cx); cx.notify(); } } fn jump_to_cell(&mut self, index: usize, _cx: &mut ViewContext<Self>) { self.cell_list.scroll_to_reveal_item(index); } fn button_group(cx: &ViewContext<Self>) -> Div { v_flex() .gap(DynamicSpacing::Base04.rems(cx)) .items_center() .w(px(CONTROL_SIZE + 4.0)) .overflow_hidden() .rounded(px(5.)) .bg(cx.theme().colors().title_bar_background) .p_px() .border_1() .border_color(cx.theme().colors().border) } fn render_notebook_control( id: impl Into<SharedString>, icon: IconName, _cx: &ViewContext<Self>, ) -> IconButton { let id: ElementId = ElementId::Name(id.into()); IconButton::new(id, icon).width(px(CONTROL_SIZE).into()) } fn render_notebook_controls(&self, cx: &ViewContext<Self>) -> impl IntoElement { let has_outputs = self.has_outputs(cx); v_flex() .max_w(px(CONTROL_SIZE + 4.0)) .items_center() .gap(DynamicSpacing::Base16.rems(cx)) .justify_between() .flex_none() .h_full() .py(DynamicSpacing::Base12.px(cx)) .child( v_flex() .gap(DynamicSpacing::Base08.rems(cx)) .child( Self::button_group(cx) .child( Self::render_notebook_control("run-all-cells", IconName::Play, cx) .tooltip(move |cx| { Tooltip::for_action("Execute all cells", &RunAll, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(RunAll)); }), ) .child( Self::render_notebook_control( "clear-all-outputs", IconName::ListX, cx, ) .disabled(!has_outputs) .tooltip(move |cx| { Tooltip::for_action("Clear all outputs", &ClearOutputs, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(ClearOutputs)); }), ), ) .child( Self::button_group(cx) .child( Self::render_notebook_control( "move-cell-up", IconName::ArrowUp, cx, ) .tooltip(move |cx| { Tooltip::for_action("Move cell up", &MoveCellUp, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(MoveCellUp)); }), ) .child( Self::render_notebook_control( "move-cell-down", IconName::ArrowDown, cx, ) .tooltip(move |cx| { Tooltip::for_action("Move cell down", &MoveCellDown, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(MoveCellDown)); }), ), ) .child( Self::button_group(cx) .child( Self::render_notebook_control( "new-markdown-cell", IconName::Plus, cx, ) .tooltip(move |cx| { Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(AddMarkdownBlock)); }), ) .child( Self::render_notebook_control("new-code-cell", IconName::Code, cx) .tooltip(move |cx| { Tooltip::for_action("Add code block", &AddCodeBlock, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(AddCodeBlock)); }), ), ), ) .child( v_flex() .gap(DynamicSpacing::Base08.rems(cx)) .items_center() .child(Self::render_notebook_control( "more-menu", IconName::Ellipsis, cx, )) .child( Self::button_group(cx) .child(IconButton::new("repl", IconName::ReplNeutral)), ), ) } fn cell_position(&self, index: usize) -> CellPosition { match index { 0 => CellPosition::First, index if index == self.cell_count() - 1 => CellPosition::Last, _ => CellPosition::Middle, } } fn render_cell( &self, index: usize, cell: &Cell, cx: &mut ViewContext<Self>, ) -> impl IntoElement { let cell_position = self.cell_position(index); let is_selected = index == self.selected_cell_index; match cell { Cell::Code(cell) => { cell.update(cx, |cell, _cx| { cell.set_selected(is_selected) .set_cell_position(cell_position); }); cell.clone().into_any_element() } Cell::Markdown(cell) => { cell.update(cx, |cell, _cx| { cell.set_selected(is_selected) .set_cell_position(cell_position); }); cell.clone().into_any_element() } Cell::Raw(cell) => { cell.update(cx, |cell, _cx| { cell.set_selected(is_selected) .set_cell_position(cell_position); }); cell.clone().into_any_element() } } }} impl Render for NotebookEditor { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { div() .key_context("notebook") .track_focus(&self.focus_handle) .on_action(cx.listener(|this, &OpenNotebook, cx| this.open_notebook(&OpenNotebook, cx))) .on_action(cx.listener(|this, &ClearOutputs, cx| this.clear_outputs(cx))) .on_action(cx.listener(|this, &RunAll, cx| this.run_cells(cx))) .on_action(cx.listener(|this, &MoveCellUp, cx| this.move_cell_up(cx))) .on_action(cx.listener(|this, &MoveCellDown, cx| this.move_cell_down(cx))) .on_action(cx.listener(|this, &AddMarkdownBlock, cx| this.add_markdown_block(cx))) .on_action(cx.listener(|this, &AddCodeBlock, cx| this.add_code_block(cx))) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_last)) .flex() .items_start() .size_full() .overflow_hidden() .px(DynamicSpacing::Base12.px(cx)) .gap(DynamicSpacing::Base12.px(cx)) .bg(cx.theme().colors().tab_bar_background) .child( v_flex() .id("notebook-cells") .flex_1() .size_full() .overflow_y_scroll() .child(list(self.cell_list.clone()).size_full()), ) .child(self.render_notebook_controls(cx)) }} impl FocusableView for NotebookEditor { fn focus_handle(&self, _: &AppContext) -> FocusHandle { self.focus_handle.clone() }} // Intended to be a NotebookBufferpub struct NotebookItem { path: PathBuf, project_path: ProjectPath, languages: Arc<LanguageRegistry>, // Raw notebook data notebook: nbformat::v4::Notebook, // Store our version of the notebook in memory (cell_order, cell_map) id: ProjectEntryId,} impl project::ProjectItem for NotebookItem { fn try_open( project: &Model<Project>, path: &ProjectPath, cx: &mut AppContext, ) -> Option<Task<gpui::Result<Model<Self>>>> { let path = path.clone(); let project = project.clone();Lines 1–500 of 757. Load more
#![allow(unused, dead_code)] use std::future::Future; use std::{path::PathBuf, sync::Arc}; use anyhow::{Context as _, Result}; use client::proto::ViewId; use collections::HashMap; use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag}; use futures::future::Shared; use futures::FutureExt; use gpui::{ actions, list, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListScrollEvent, ListState, Model, Point, Task, View, }; use language::{Language, LanguageRegistry}; use project::{Project, ProjectEntryId, ProjectPath}; use ui::{prelude::*, Tooltip}; use workspace::item::{ItemEvent, TabContentParams}; use workspace::searchable::SearchableItemHandle; use workspace::{Item, ItemHandle, ProjectItem, ToolbarItemLocation}; use workspace::{ToolbarItemEvent, ToolbarItemView}; use super::{Cell, CellPosition, RenderableCell}; use nbformat::v4::CellId; use nbformat::v4::Metadata as NotebookMetadata; actions!( notebook, [ OpenNotebook, RunAll, ClearOutputs, MoveCellUp, MoveCellDown, AddMarkdownBlock, AddCodeBlock, ] ); pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0; pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0; pub(crate) const MEDIUM_SPACING_SIZE: f32 = 12.0; pub(crate) const LARGE_SPACING_SIZE: f32 = 16.0; pub(crate) const GUTTER_WIDTH: f32 = 19.0; pub(crate) const CODE_BLOCK_INSET: f32 = MEDIUM_SPACING_SIZE; pub(crate) const CONTROL_SIZE: f32 = 20.0; pub fn init(cx: &mut AppContext) { if cx.has_flag::<NotebookFeatureFlag>() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() { workspace::register_project_item::<NotebookEditor>(cx); } cx.observe_flag::<NotebookFeatureFlag, _>({ move |is_enabled, cx| { if is_enabled { workspace::register_project_item::<NotebookEditor>(cx); } else { // todo: there is no way to unregister a project item, so if the feature flag // gets turned off they need to restart Zed. } } }) .detach(); } pub struct NotebookEditor { languages: Arc<LanguageRegistry>, project: Model<Project>, focus_handle: FocusHandle, notebook_item: Model<NotebookItem>, remote_id: Option<ViewId>, cell_list: ListState, selected_cell_index: usize, cell_order: Vec<CellId>, cell_map: HashMap<CellId, Cell>, } impl NotebookEditor { pub fn new( project: Model<Project>, notebook_item: Model<NotebookItem>, cx: &mut ViewContext<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let languages = project.read(cx).languages().clone(); let language_name = notebook_item.read(cx).language_name(); let notebook_language = notebook_item.read(cx).notebook_language(); let notebook_language = cx.spawn(|_, _| notebook_language).shared(); let mut cell_order = vec![]; // Vec<CellId> let mut cell_map = HashMap::default(); // HashMap<CellId, Cell> for (index, cell) in notebook_item .read(cx) .notebook .clone() .cells .iter() .enumerate() { let cell_id = cell.id(); cell_order.push(cell_id.clone()); cell_map.insert( cell_id.clone(), Cell::load(cell, &languages, notebook_language.clone(), cx), ); } let view = cx.view().downgrade(); let cell_count = cell_order.len(); let this = cx.view(); let cell_list = ListState::new( cell_count, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { view.upgrade() .and_then(|notebook_handle| { notebook_handle.update(cx, |notebook, cx| { notebook .cell_order .get(ix) .and_then(|cell_id| notebook.cell_map.get(cell_id)) .map(|cell| notebook.render_cell(ix, cell, cx).into_any_element()) }) }) .unwrap_or_else(|| div().into_any()) }, ); Self { project, languages: languages.clone(), focus_handle, notebook_item, remote_id: None, cell_list, selected_cell_index: 0, cell_order: cell_order.clone(), cell_map: cell_map.clone(), } } fn has_outputs(&self, cx: &ViewContext<Self>) -> bool { self.cell_map.values().any(|cell| { if let Cell::Code(code_cell) = cell { code_cell.read(cx).has_outputs() } else { false } }) } fn clear_outputs(&mut self, cx: &mut ViewContext<Self>) { for cell in self.cell_map.values() { if let Cell::Code(code_cell) = cell { code_cell.update(cx, |cell, _cx| { cell.clear_outputs(); }); } } } fn run_cells(&mut self, cx: &mut ViewContext<Self>) { println!("Cells would all run here, if that was implemented!"); } fn open_notebook(&mut self, _: &OpenNotebook, _cx: &mut ViewContext<Self>) { println!("Open notebook triggered"); } fn move_cell_up(&mut self, cx: &mut ViewContext<Self>) { println!("Move cell up triggered"); } fn move_cell_down(&mut self, cx: &mut ViewContext<Self>) { println!("Move cell down triggered"); } fn add_markdown_block(&mut self, cx: &mut ViewContext<Self>) { println!("Add markdown block triggered"); } fn add_code_block(&mut self, cx: &mut ViewContext<Self>) { println!("Add code block triggered"); } fn cell_count(&self) -> usize { self.cell_map.len() } fn selected_index(&self) -> usize { self.selected_cell_index } pub fn set_selected_index( &mut self, index: usize, jump_to_index: bool, cx: &mut ViewContext<Self>, ) { // let previous_index = self.selected_cell_index; self.selected_cell_index = index; let current_index = self.selected_cell_index; // in the future we may have some `on_cell_change` event that we want to fire here if jump_to_index { self.jump_to_cell(current_index, cx); } } pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) { let count = self.cell_count(); if count > 0 { let index = self.selected_index(); let ix = if index == count - 1 { count - 1 } else { index + 1 }; self.set_selected_index(ix, true, cx); cx.notify(); } } pub fn select_previous(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) { let count = self.cell_count(); if count > 0 { let index = self.selected_index(); let ix = if index == 0 { 0 } else { index - 1 }; self.set_selected_index(ix, true, cx); cx.notify(); } } pub fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) { let count = self.cell_count(); if count > 0 { self.set_selected_index(0, true, cx); cx.notify(); } } pub fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) { let count = self.cell_count(); if count > 0 { self.set_selected_index(count - 1, true, cx); cx.notify(); } } fn jump_to_cell(&mut self, index: usize, _cx: &mut ViewContext<Self>) { self.cell_list.scroll_to_reveal_item(index); } fn button_group(cx: &ViewContext<Self>) -> Div { v_flex() .gap(DynamicSpacing::Base04.rems(cx)) .items_center() .w(px(CONTROL_SIZE + 4.0)) .overflow_hidden() .rounded(px(5.)) .bg(cx.theme().colors().title_bar_background) .p_px() .border_1() .border_color(cx.theme().colors().border) } fn render_notebook_control( id: impl Into<SharedString>, icon: IconName, _cx: &ViewContext<Self>, ) -> IconButton { let id: ElementId = ElementId::Name(id.into()); IconButton::new(id, icon).width(px(CONTROL_SIZE).into()) } fn render_notebook_controls(&self, cx: &ViewContext<Self>) -> impl IntoElement { let has_outputs = self.has_outputs(cx); v_flex() .max_w(px(CONTROL_SIZE + 4.0)) .items_center() .gap(DynamicSpacing::Base16.rems(cx)) .justify_between() .flex_none() .h_full() .py(DynamicSpacing::Base12.px(cx)) .child( v_flex() .gap(DynamicSpacing::Base08.rems(cx)) .child( Self::button_group(cx) .child( Self::render_notebook_control("run-all-cells", IconName::Play, cx) .tooltip(move |cx| { Tooltip::for_action("Execute all cells", &RunAll, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(RunAll)); }), ) .child( Self::render_notebook_control( "clear-all-outputs", IconName::ListX, cx, ) .disabled(!has_outputs) .tooltip(move |cx| { Tooltip::for_action("Clear all outputs", &ClearOutputs, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(ClearOutputs)); }), ), ) .child( Self::button_group(cx) .child( Self::render_notebook_control( "move-cell-up", IconName::ArrowUp, cx, ) .tooltip(move |cx| { Tooltip::for_action("Move cell up", &MoveCellUp, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(MoveCellUp)); }), ) .child( Self::render_notebook_control( "move-cell-down", IconName::ArrowDown, cx, ) .tooltip(move |cx| { Tooltip::for_action("Move cell down", &MoveCellDown, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(MoveCellDown)); }), ), ) .child( Self::button_group(cx) .child( Self::render_notebook_control( "new-markdown-cell", IconName::Plus, cx, ) .tooltip(move |cx| { Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(AddMarkdownBlock)); }), ) .child( Self::render_notebook_control("new-code-cell", IconName::Code, cx) .tooltip(move |cx| { Tooltip::for_action("Add code block", &AddCodeBlock, cx) }) .on_click(|_, cx| { cx.dispatch_action(Box::new(AddCodeBlock)); }), ), ), ) .child( v_flex() .gap(DynamicSpacing::Base08.rems(cx)) .items_center() .child(Self::render_notebook_control( "more-menu", IconName::Ellipsis, cx, )) .child( Self::button_group(cx) .child(IconButton::new("repl", IconName::ReplNeutral)), ), ) } fn cell_position(&self, index: usize) -> CellPosition { match index { 0 => CellPosition::First, index if index == self.cell_count() - 1 => CellPosition::Last, _ => CellPosition::Middle, } } fn render_cell( &self, index: usize, cell: &Cell, cx: &mut ViewContext<Self>, ) -> impl IntoElement { let cell_position = self.cell_position(index); let is_selected = index == self.selected_cell_index; match cell { Cell::Code(cell) => { cell.update(cx, |cell, _cx| { cell.set_selected(is_selected) .set_cell_position(cell_position); }); cell.clone().into_any_element() } Cell::Markdown(cell) => { cell.update(cx, |cell, _cx| { cell.set_selected(is_selected) .set_cell_position(cell_position); }); cell.clone().into_any_element() } Cell::Raw(cell) => { cell.update(cx, |cell, _cx| { cell.set_selected(is_selected) .set_cell_position(cell_position); }); cell.clone().into_any_element() } } } } impl Render for NotebookEditor { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { div() .key_context("notebook") .track_focus(&self.focus_handle) .on_action(cx.listener(|this, &OpenNotebook, cx| this.open_notebook(&OpenNotebook, cx))) .on_action(cx.listener(|this, &ClearOutputs, cx| this.clear_outputs(cx))) .on_action(cx.listener(|this, &RunAll, cx| this.run_cells(cx))) .on_action(cx.listener(|this, &MoveCellUp, cx| this.move_cell_up(cx))) .on_action(cx.listener(|this, &MoveCellDown, cx| this.move_cell_down(cx))) .on_action(cx.listener(|this, &AddMarkdownBlock, cx| this.add_markdown_block(cx))) .on_action(cx.listener(|this, &AddCodeBlock, cx| this.add_code_block(cx))) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_last)) .flex() .items_start() .size_full() .overflow_hidden() .px(DynamicSpacing::Base12.px(cx)) .gap(DynamicSpacing::Base12.px(cx)) .bg(cx.theme().colors().tab_bar_background) .child( v_flex() .id("notebook-cells") .flex_1() .size_full() .overflow_y_scroll() .child(list(self.cell_list.clone()).size_full()), ) .child(self.render_notebook_controls(cx)) } } impl FocusableView for NotebookEditor { fn focus_handle(&self, _: &AppContext) -> FocusHandle { self.focus_handle.clone() } } // Intended to be a NotebookBuffer pub struct NotebookItem { path: PathBuf, project_path: ProjectPath, languages: Arc<LanguageRegistry>, // Raw notebook data notebook: nbformat::v4::Notebook, // Store our version of the notebook in memory (cell_order, cell_map) id: ProjectEntryId, } impl project::ProjectItem for NotebookItem { fn try_open( project: &Model<Project>, path: &ProjectPath, cx: &mut AppContext, ) -> Option<Task<gpui::Result<Model<Self>>>> { let path = path.clone(); let project = project.clone();