Compare commits

...

6 Commits

2 changed files with 374 additions and 68 deletions

View File

@@ -1,9 +1,14 @@
#![allow(unused, dead_code)]
use std::sync::Arc;
use std::{
collections::HashMap,
default,
ops::{Deref, DerefMut},
sync::Arc,
};
use editor::{Editor, EditorMode, MultiBuffer};
use futures::future::Shared;
use gpui::{prelude::*, App, Entity, Hsla, Task, TextStyleRefinement};
use gpui::{prelude::*, App, ClickEvent, Entity, Hsla, MouseDownEvent, Task, TextStyleRefinement};
use language::{Buffer, Language, LanguageRegistry};
use markdown_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block};
use nbformat::v4::{CellId, CellMetadata, CellType};
@@ -75,6 +80,22 @@ impl Clickable for CellControl {
}
}
pub(crate) fn default_cell_metadata() -> CellMetadata {
CellMetadata {
id: None,
collapsed: None,
scrolled: None,
deletable: None,
editable: None,
format: None,
name: None,
tags: None,
jupyter: None,
execution: None,
additional: HashMap::new(),
}
}
/// A notebook cell
#[derive(Clone)]
pub enum Cell {
@@ -146,6 +167,11 @@ impl Cell {
})
};
let editor = cx.new(|cx| Editor::multi_line(window, cx));
editor.update(cx, |this, cx| {
this.set_text(source.clone(), window, cx);
});
MarkdownCell {
markdown_parsing_task,
languages: languages.clone(),
@@ -155,6 +181,10 @@ impl Cell {
parsed_markdown: None,
selected: false,
cell_position: None,
editing: false,
editor,
on_click: None,
on_secondary_mouse_down: None,
}
});
@@ -336,6 +366,69 @@ pub struct MarkdownCell {
selected: bool,
cell_position: Option<CellPosition>,
languages: Arc<LanguageRegistry>,
editing: bool,
editor: Entity<Editor>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>>,
}
impl MarkdownCell {
pub fn new(
id: CellId,
languages: Arc<LanguageRegistry>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let selected = false;
let cell_position = None;
let editor = cx.new(|cx| Editor::multi_line(window, cx));
MarkdownCell {
id,
metadata: default_cell_metadata(),
source: "".to_string(),
parsed_markdown: None,
markdown_parsing_task: Task::ready(()),
selected,
cell_position,
languages,
editing: false,
editor,
on_click: None,
on_secondary_mouse_down: None,
}
}
pub fn set_text(mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |this, cx| this.set_text(text, window, cx));
}
pub fn is_editing(&self) -> bool {
self.editing
}
pub fn editing(&mut self, editing: bool) -> &mut Self {
self.editing = editing;
self
}
pub fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Box::new(handler));
self
}
pub fn on_secondary_mouse_down(
mut self,
handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_secondary_mouse_down = Some(Box::new(handler));
self
}
}
impl RenderableCell for MarkdownCell {
@@ -382,9 +475,10 @@ impl RenderableCell for MarkdownCell {
impl Render for MarkdownCell {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let Some(parsed) = self.parsed_markdown.as_ref() else {
return div();
};
let parsed = self.parsed_markdown.as_ref();
let source_lines = self.source.lines();
let source_line_count = source_lines.count();
let mut markdown_render_context =
markdown_preview::markdown_renderer::RenderContext::new(None, window, cx);
@@ -409,12 +503,36 @@ impl Render for MarkdownCell {
.p_3()
.font_ui(cx)
.text_size(TextSize::Default.rems(cx))
//
.children(parsed.children.iter().map(|child| {
div().relative().child(div().relative().child(
render_markdown_block(child, &mut markdown_render_context),
))
})),
.when(!self.is_editing(), |this| {
this.map(|this| {
if let Some(parsed) = parsed {
this.children(parsed.children.iter().map(|child| {
div().relative().child(div().relative().child(
render_markdown_block(
child,
&mut markdown_render_context,
),
))
}))
} else {
this.child(
Label::new(
"Empty markdown block. Double click to edit.",
)
.color(Color::Placeholder),
)
}
})
})
.when(self.is_editing(), |this| {
this.child(
div()
.flex_1()
.min_h(window.line_height() * px(source_line_count as f32))
.w_full()
.child(self.editor.clone()),
)
}),
),
)
// TODO: Move base cell render into trait impl so we don't have to repeat this

View File

@@ -9,8 +9,8 @@ use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag};
use futures::future::Shared;
use futures::FutureExt;
use gpui::{
actions, list, prelude::*, AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable,
ListScrollEvent, ListState, Point, Task,
actions, list, prelude::*, AnyElement, App, ClickEvent, Entity, EventEmitter, FocusHandle,
Focusable, ListScrollEvent, ListState, Point, Task, WeakEntity,
};
use language::{Language, LanguageRegistry};
use project::{Project, ProjectEntryId, ProjectPath};
@@ -20,10 +20,10 @@ use workspace::searchable::SearchableItemHandle;
use workspace::{Item, ItemHandle, ProjectItem, ToolbarItemLocation};
use workspace::{ToolbarItemEvent, ToolbarItemView};
use super::{Cell, CellPosition, RenderableCell};
use super::{Cell, CellPosition, CodeCell, MarkdownCell, RawCell, RenderableCell};
use nbformat::v4::CellId;
use nbformat::v4::Metadata as NotebookMetadata;
use nbformat::v4::{CellId, CellType};
actions!(
notebook,
@@ -35,6 +35,8 @@ actions!(
MoveCellDown,
AddMarkdownBlock,
AddCodeBlock,
ToggleEditMode,
EditCell,
]
);
@@ -45,6 +47,7 @@ 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;
const MINIMUM_NOTEBOOK_VER: f32 = 4.1;
pub fn init(cx: &mut App) {
if cx.has_flag::<NotebookFeatureFlag>() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() {
@@ -64,6 +67,36 @@ pub fn init(cx: &mut App) {
.detach();
}
pub fn update_cell_list(
cell_count: usize,
notebook_handle: WeakEntity<NotebookEditor>,
cx: &mut Context<NotebookEditor>,
) -> ListState {
ListState::new(
cell_count,
gpui::ListAlignment::Top,
px(1000.),
move |ix, window, cx| {
notebook_handle
.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, window, cx)
.into_any_element()
})
})
})
.unwrap_or_else(|| div().into_any())
},
)
}
pub struct NotebookEditor {
languages: Arc<LanguageRegistry>,
project: Entity<Project>,
@@ -75,6 +108,7 @@ pub struct NotebookEditor {
cell_list: ListState,
selected_cell_index: usize,
cell_order: Vec<CellId>,
cell_map: HashMap<CellId, Cell>,
}
@@ -117,29 +151,7 @@ impl NotebookEditor {
let cell_count = cell_order.len();
let this = cx.entity();
let cell_list = ListState::new(
cell_count,
gpui::ListAlignment::Top,
px(1000.),
move |ix, window, cx| {
notebook_handle
.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, window, cx)
.into_any_element()
})
})
})
.unwrap_or_else(|| div().into_any())
},
);
let cell_list = update_cell_list(cell_count, notebook_handle, cx);
Self {
project,
@@ -154,6 +166,12 @@ impl NotebookEditor {
}
}
fn has_executable_cells(&self) -> bool {
self.cell_map
.values()
.any(|cell| matches!(cell, Cell::Code(_)))
}
fn has_outputs(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
self.cell_map.values().any(|cell| {
if let Cell::Code(code_cell) = cell {
@@ -182,20 +200,89 @@ impl NotebookEditor {
println!("Open notebook triggered");
}
fn swap_cells(
&mut self,
index1: usize,
index2: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
if index1 < self.cell_order.len() && index2 < self.cell_order.len() {
let cell_count = self.cell_count();
let notebook_handle = cx.entity().downgrade();
self.cell_order.swap(index1, index2);
self.cell_list = update_cell_list(cell_count, notebook_handle, cx);
cx.notify();
}
}
fn move_cell_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
println!("Move cell up triggered");
if self.selected_cell_index > 0 {
let current_index = self.selected_cell_index;
let new_index = current_index - 1;
self.swap_cells(current_index, new_index, window, cx);
self.selected_cell_index = new_index;
self.cell_list.scroll_to_reveal_item(new_index);
}
}
fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context<Self>) {
println!("Move cell down triggered");
if self.selected_cell_index < self.cell_order.len() - 1 {
let current_index = self.selected_cell_index;
let new_index = current_index + 1;
self.swap_cells(current_index, new_index, window, cx);
self.selected_cell_index = new_index;
self.cell_list.scroll_to_reveal_item(new_index);
}
}
fn add_cell(&mut self, cell_type: CellType, window: &mut Window, cx: &mut Context<Self>) {
let new_cell_id = CellId::new(&uuid::Uuid::new_v4().to_string()).unwrap();
let insert_index = self.selected_cell_index.saturating_add(1);
let languages = self.languages.clone();
let cell = match cell_type {
CellType::Markdown => {
let markdown_cell = cx.new(|cx| {
MarkdownCell::new(new_cell_id.clone(), languages.clone(), window, cx)
});
Cell::Markdown(markdown_cell)
}
_ => return, // CellType::Code => {
// let code_cell =
// cx.new(|_| CodeCell::new(new_cell_id.clone(), self.languages.clone()));
// Cell::Code(code_cell)
// }
// CellType::Raw => {
// let raw_cell = cx.new(|_| RawCell::new(new_cell_id.clone()));
// Cell::Raw(raw_cell)
// }
};
self.cell_order.insert(insert_index, new_cell_id.clone());
self.cell_map.insert(new_cell_id.clone(), cell);
let notebook_handle = cx.entity().downgrade();
self.cell_list = update_cell_list(self.cell_count(), notebook_handle, cx);
self.set_selected_index(insert_index, true, window, cx);
self.cell_list.scroll_to_reveal_item(insert_index);
cx.notify();
}
fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
println!("Add markdown block triggered");
self.add_cell(CellType::Markdown, window, cx);
}
fn add_code_block(&mut self, window: &mut Window, cx: &mut Context<Self>) {
println!("Add code block triggered");
self.add_cell(CellType::Code, window, cx);
}
fn cell_count(&self) -> usize {
@@ -284,6 +371,45 @@ impl NotebookEditor {
}
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(cell_id) = self.cell_order.get(self.selected_cell_index).cloned() {
if let Some(Cell::Markdown(markdown_cell)) = self.cell_map.get_mut(&cell_id) {
markdown_cell.update(cx, |cell, _| {
println!("Confirming cell, {}", cell.id());
cell.editing(true);
});
}
}
cx.notify();
}
fn edit_cell(&mut self, index: usize, cx: &mut Context<Self>) {
if let Some(cell_id) = self.cell_order.get(index).cloned() {
if let Some(Cell::Markdown(markdown_cell)) = self.cell_map.get_mut(&cell_id) {
markdown_cell.update(cx, |cell, _| {
println!("Editing cell, {}", cell.id());
cell.editing(true);
});
}
}
}
fn toggle_edit_mode(
&mut self,
_: &ToggleEditMode,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(cell_id) = self.cell_order.get(self.selected_cell_index).cloned() {
if let Some(Cell::Markdown(markdown_cell)) = self.cell_map.get_mut(&cell_id) {
markdown_cell.update(cx, |cell, _| {
cell.editing(!cell.is_editing());
});
}
}
cx.notify();
}
fn jump_to_cell(&mut self, index: usize, _window: &mut Window, _cx: &mut Context<Self>) {
self.cell_list.scroll_to_reveal_item(index);
}
@@ -317,8 +443,17 @@ impl NotebookEditor {
cx: &mut Context<Self>,
) -> impl IntoElement {
let has_outputs = self.has_outputs(window, cx);
let selected_cell_id = &self.cell_order[self.selected_cell_index];
let selected_is_editing =
self.cell_map
.get(selected_cell_id)
.map_or(false, |cell| match cell {
Cell::Markdown(markdown_cell) => markdown_cell.read(cx).is_editing(),
_ => false,
});
v_flex()
.id("notebook-controls")
.max_w(px(CONTROL_SIZE + 4.0))
.items_center()
.gap(DynamicSpacing::Base16.rems(cx))
@@ -338,12 +473,16 @@ impl NotebookEditor {
window,
cx,
)
.disabled(!self.has_executable_cells())
.tooltip(move |window, cx| {
Tooltip::for_action("Execute all cells", &RunAll, window, cx)
})
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(RunAll), cx);
}),
.on_click(cx.listener(
move |this, _, window, cx| {
window.focus(&this.focus_handle);
window.dispatch_action(Box::new(RunAll), cx);
},
)),
)
.child(
Self::render_notebook_control(
@@ -361,9 +500,12 @@ impl NotebookEditor {
cx,
)
})
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(ClearOutputs), cx);
}),
.on_click(cx.listener(
move |this, _, window, cx| {
window.focus(&this.focus_handle);
window.dispatch_action(Box::new(ClearOutputs), cx);
},
)),
),
)
.child(
@@ -378,9 +520,12 @@ impl NotebookEditor {
.tooltip(move |window, cx| {
Tooltip::for_action("Move cell up", &MoveCellUp, window, cx)
})
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(MoveCellUp), cx);
}),
.on_click(cx.listener(
move |this, _, window, cx| {
window.focus(&this.focus_handle);
window.dispatch_action(Box::new(MoveCellUp), cx);
},
)),
)
.child(
Self::render_notebook_control(
@@ -392,9 +537,12 @@ impl NotebookEditor {
.tooltip(move |window, cx| {
Tooltip::for_action("Move cell down", &MoveCellDown, window, cx)
})
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(MoveCellDown), cx);
}),
.on_click(cx.listener(
move |this, _, window, cx| {
window.focus(&this.focus_handle);
window.dispatch_action(Box::new(MoveCellDown), cx);
},
)),
),
)
.child(
@@ -414,9 +562,12 @@ impl NotebookEditor {
cx,
)
})
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(AddMarkdownBlock), cx);
}),
.on_click(cx.listener(
move |this, _, window, cx| {
window.focus(&this.focus_handle);
window.dispatch_action(Box::new(AddMarkdownBlock), cx);
},
)),
)
.child(
Self::render_notebook_control(
@@ -425,28 +576,53 @@ impl NotebookEditor {
window,
cx,
)
.disabled(true)
.tooltip(move |window, cx| {
Tooltip::for_action("Add code block", &AddCodeBlock, window, cx)
})
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(AddCodeBlock), cx);
}),
.on_click(cx.listener(
move |this, _, window, cx| {
window.focus(&this.focus_handle);
window.dispatch_action(Box::new(AddCodeBlock), cx);
},
)),
),
)
.child(
Self::button_group(window, cx).child(
Self::render_notebook_control(
"edit-cell",
if selected_is_editing {
IconName::Eye
} else {
IconName::Pencil
},
window,
cx,
)
.tooltip(move |window, cx| {
Tooltip::for_action("Edit Cell", &EditCell, window, cx)
})
.on_click(cx.listener(
move |this, _, window, cx| {
window.focus(&this.focus_handle);
window.dispatch_action(Box::new(ToggleEditMode), cx);
},
)),
),
),
)
.child(
v_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.items_center()
.child(Self::render_notebook_control(
"more-menu",
IconName::Ellipsis,
window,
cx,
))
.child(
Self::render_notebook_control("more-menu", IconName::Ellipsis, window, cx)
.disabled(true),
)
.child(
Self::button_group(window, cx)
.child(IconButton::new("repl", IconName::ReplNeutral)),
.child(IconButton::new("repl", IconName::ReplNeutral).disabled(true)),
),
)
}
@@ -504,6 +680,14 @@ impl Render for NotebookEditor {
.on_action(cx.listener(|this, &OpenNotebook, window, cx| {
this.open_notebook(&OpenNotebook, window, cx)
}))
.on_action(cx.listener(|this, &EditCell, window, cx| {
let selected_cell_index = this.selected_cell_index.clone();
this.edit_cell(selected_cell_index.clone(), cx)
}))
.on_action(cx.listener(|this, &ToggleEditMode, window, cx| {
this.toggle_edit_mode(&ToggleEditMode, window, cx)
}))
.on_action(
cx.listener(|this, &ClearOutputs, window, cx| this.clear_outputs(window, cx)),
)
@@ -590,7 +774,11 @@ impl project::ProjectItem for NotebookItem {
}
// Bad notebooks and notebooks v4.0 and below are not supported
Err(e) => {
anyhow::bail!("Failed to parse notebook: {:?}", e);
anyhow::bail!(
"Unsupported notebook version. This notebook requires version {} or later. Error details: {}",
MINIMUM_NOTEBOOK_VER,
e
);
}
};