Initial Notebook UI structure (#19756)
This is the start of a notebook UI for Zed. `🔔 Note: This won't be useable yet when it is merged! Read below. 🔔` This is going to be behind a feature flag so that we can merge this initial PR and then make follow up PRs. Release notes will be produced in a future PR. Minimum checklist for merging this: * [x] All functionality behind the `notebooks` feature flag (with env var opt out) * [x] Open notebook files in the workspace * [x] Remove the "Open Notebook" button from title bar * [x] Incorporate text style refinements for cell editors * [x] Rely on `nbformat` crate for parsing the notebook into our in-memory format * [x] Move notebook to a `gpui::List` * [x] Hook up output rendering Release Notes: - N/A --------- Co-authored-by: Nate Butler <iamnbutler@gmail.com> Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
This commit is contained in:
parent
9d12308d06
commit
6ea4662326
13 changed files with 1478 additions and 9 deletions
45
Cargo.lock
generated
45
Cargo.lock
generated
|
@ -1586,7 +1586,7 @@ dependencies = [
|
|||
"bitflags 2.6.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"proc-macro2",
|
||||
|
@ -5584,7 +5584,7 @@ dependencies = [
|
|||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.10",
|
||||
"socket2 0.5.7",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
|
@ -6154,6 +6154,20 @@ dependencies = [
|
|||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-serde"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a444fb3f87ee6885eb316028cc998c7d84811663ef95d78c419419423d5a054"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
|
@ -6474,7 +6488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -7137,6 +7151,21 @@ dependencies = [
|
|||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nbformat"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146074ad45cab20f5d98ccded164826158471f21d04f96e40b9872529e10979d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"jupyter-serde",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.8.0"
|
||||
|
@ -9579,6 +9608,7 @@ dependencies = [
|
|||
"command_palette_hooks",
|
||||
"editor",
|
||||
"env_logger 0.11.5",
|
||||
"feature_flags",
|
||||
"futures 0.3.30",
|
||||
"gpui",
|
||||
"http_client",
|
||||
|
@ -9588,7 +9618,9 @@ dependencies = [
|
|||
"languages",
|
||||
"log",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"nbformat",
|
||||
"project",
|
||||
"runtimelib",
|
||||
"schemars",
|
||||
|
@ -9927,9 +9959,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "runtimelib"
|
||||
version = "0.15.0"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7d76d28b882a7b889ebb04e79bc2b160b3061821ea596ff0f4a838fc7a76db0"
|
||||
checksum = "263588fe9593333c4bfde258c9021fc64e766ea434e070c6b67c7100536d6499"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-dispatcher",
|
||||
|
@ -9941,6 +9973,7 @@ dependencies = [
|
|||
"dirs 5.0.1",
|
||||
"futures 0.3.30",
|
||||
"glob",
|
||||
"jupyter-serde",
|
||||
"rand 0.8.5",
|
||||
"ring 0.17.8",
|
||||
"serde",
|
||||
|
@ -14126,7 +14159,7 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -371,6 +371,7 @@ linkify = "0.10.0"
|
|||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nbformat = "0.3.1"
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
|
@ -402,7 +403,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
|
|||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.15", default-features = false, features = [
|
||||
runtimelib = { version = "0.16.0", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
|
|
7
assets/icons/list_x.svg
Normal file
7
assets/icons/list_x.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.33333 8H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.6667 4H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.6667 12H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.6667 6.66663L11 9.33329" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 6.66663L13.6667 9.33329" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 579 B |
|
@ -59,6 +59,12 @@ impl FeatureFlag for ZedPro {
|
|||
const NAME: &'static str = "zed-pro";
|
||||
}
|
||||
|
||||
pub struct NotebookFeatureFlag;
|
||||
|
||||
impl FeatureFlag for NotebookFeatureFlag {
|
||||
const NAME: &'static str = "notebooks";
|
||||
}
|
||||
|
||||
pub struct AutoCommand {}
|
||||
impl FeatureFlag for AutoCommand {
|
||||
const NAME: &'static str = "auto-command";
|
||||
|
|
|
@ -21,13 +21,16 @@ client.workspace = true
|
|||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
image.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
nbformat.workspace = true
|
||||
project.workspace = true
|
||||
runtimelib.workspace = true
|
||||
schemars.workspace = true
|
||||
|
|
4
crates/repl/src/notebook.rs
Normal file
4
crates/repl/src/notebook.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
mod cell;
|
||||
mod notebook_ui;
|
||||
pub use cell::*;
|
||||
pub use notebook_ui::*;
|
733
crates/repl/src/notebook/cell.rs
Normal file
733
crates/repl/src/notebook/cell.rs
Normal file
|
@ -0,0 +1,733 @@
|
|||
#![allow(unused, dead_code)]
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{Editor, EditorMode, MultiBuffer};
|
||||
use futures::future::Shared;
|
||||
use gpui::{prelude::*, AppContext, Hsla, Task, TextStyleRefinement, View};
|
||||
use language::{Buffer, Language, LanguageRegistry};
|
||||
use markdown_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block};
|
||||
use nbformat::v4::{CellId, CellMetadata, CellType};
|
||||
use settings::Settings as _;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, IconButtonShape};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
notebook::{CODE_BLOCK_INSET, GUTTER_WIDTH},
|
||||
outputs::{plain::TerminalOutput, user_error::ErrorView, Output},
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd)]
|
||||
pub enum CellPosition {
|
||||
First,
|
||||
Middle,
|
||||
Last,
|
||||
}
|
||||
|
||||
pub enum CellControlType {
|
||||
RunCell,
|
||||
RerunCell,
|
||||
ClearCell,
|
||||
CellOptions,
|
||||
CollapseCell,
|
||||
ExpandCell,
|
||||
}
|
||||
|
||||
impl CellControlType {
|
||||
fn icon_name(&self) -> IconName {
|
||||
match self {
|
||||
CellControlType::RunCell => IconName::Play,
|
||||
CellControlType::RerunCell => IconName::ArrowCircle,
|
||||
CellControlType::ClearCell => IconName::ListX,
|
||||
CellControlType::CellOptions => IconName::Ellipsis,
|
||||
CellControlType::CollapseCell => IconName::ChevronDown,
|
||||
CellControlType::ExpandCell => IconName::ChevronRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CellControl {
|
||||
button: IconButton,
|
||||
}
|
||||
|
||||
impl CellControl {
|
||||
fn new(id: impl Into<SharedString>, control_type: CellControlType) -> Self {
|
||||
let icon_name = control_type.icon_name();
|
||||
let id = id.into();
|
||||
let button = IconButton::new(id, icon_name)
|
||||
.icon_size(IconSize::Small)
|
||||
.shape(IconButtonShape::Square);
|
||||
Self { button }
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for CellControl {
|
||||
fn on_click(self, handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
let button = self.button.on_click(handler);
|
||||
Self { button }
|
||||
}
|
||||
|
||||
fn cursor_style(self, _cursor_style: gpui::CursorStyle) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A notebook cell
|
||||
#[derive(Clone)]
|
||||
pub enum Cell {
|
||||
Code(View<CodeCell>),
|
||||
Markdown(View<MarkdownCell>),
|
||||
Raw(View<RawCell>),
|
||||
}
|
||||
|
||||
fn convert_outputs(outputs: &Vec<nbformat::v4::Output>, cx: &mut WindowContext) -> Vec<Output> {
|
||||
outputs
|
||||
.into_iter()
|
||||
.map(|output| match output {
|
||||
nbformat::v4::Output::Stream { text, .. } => Output::Stream {
|
||||
content: cx.new_view(|cx| TerminalOutput::from(&text.0, cx)),
|
||||
},
|
||||
nbformat::v4::Output::DisplayData(display_data) => {
|
||||
Output::new(&display_data.data, None, cx)
|
||||
}
|
||||
nbformat::v4::Output::ExecuteResult(execute_result) => {
|
||||
Output::new(&execute_result.data, None, cx)
|
||||
}
|
||||
nbformat::v4::Output::Error(error) => Output::ErrorOutput(ErrorView {
|
||||
ename: error.ename.clone(),
|
||||
evalue: error.evalue.clone(),
|
||||
traceback: cx.new_view(|cx| TerminalOutput::from(&error.traceback.join("\n"), cx)),
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
pub fn load(
|
||||
cell: &nbformat::v4::Cell,
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
notebook_language: Shared<Task<Option<Arc<Language>>>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
match cell {
|
||||
nbformat::v4::Cell::Markdown {
|
||||
id,
|
||||
metadata,
|
||||
source,
|
||||
attachments: _,
|
||||
} => {
|
||||
let source = source.join("");
|
||||
|
||||
let view = cx.new_view(|cx| {
|
||||
let markdown_parsing_task = {
|
||||
let languages = languages.clone();
|
||||
let source = source.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let parsed_markdown = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
parse_markdown(&source, None, Some(languages)).await
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |cell: &mut MarkdownCell, _| {
|
||||
cell.parsed_markdown = Some(parsed_markdown);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
};
|
||||
|
||||
MarkdownCell {
|
||||
markdown_parsing_task,
|
||||
languages: languages.clone(),
|
||||
id: id.clone(),
|
||||
metadata: metadata.clone(),
|
||||
source: source.clone(),
|
||||
parsed_markdown: None,
|
||||
selected: false,
|
||||
cell_position: None,
|
||||
}
|
||||
});
|
||||
|
||||
Cell::Markdown(view)
|
||||
}
|
||||
nbformat::v4::Cell::Code {
|
||||
id,
|
||||
metadata,
|
||||
execution_count,
|
||||
source,
|
||||
outputs,
|
||||
} => Cell::Code(cx.new_view(|cx| {
|
||||
let text = source.join("");
|
||||
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
|
||||
let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||||
|
||||
let editor_view = cx.new_view(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::AutoHeight { max_lines: 1024 },
|
||||
multi_buffer,
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
let theme = ThemeSettings::get_global(cx);
|
||||
|
||||
let refinement = TextStyleRefinement {
|
||||
font_family: Some(theme.buffer_font.family.clone()),
|
||||
font_size: Some(theme.buffer_font_size.into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(gpui::transparent_black()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
editor.set_text(text, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_text_style_refinement(refinement);
|
||||
|
||||
// editor.set_read_only(true);
|
||||
editor
|
||||
});
|
||||
|
||||
let buffer = buffer.clone();
|
||||
let language_task = cx.spawn(|this, mut cx| async move {
|
||||
let language = notebook_language.await;
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(language.clone(), cx);
|
||||
});
|
||||
});
|
||||
|
||||
CodeCell {
|
||||
id: id.clone(),
|
||||
metadata: metadata.clone(),
|
||||
execution_count: *execution_count,
|
||||
source: source.join(""),
|
||||
editor: editor_view,
|
||||
outputs: convert_outputs(outputs, cx),
|
||||
selected: false,
|
||||
language_task,
|
||||
cell_position: None,
|
||||
}
|
||||
})),
|
||||
nbformat::v4::Cell::Raw {
|
||||
id,
|
||||
metadata,
|
||||
source,
|
||||
} => Cell::Raw(cx.new_view(|_| RawCell {
|
||||
id: id.clone(),
|
||||
metadata: metadata.clone(),
|
||||
source: source.join(""),
|
||||
selected: false,
|
||||
cell_position: None,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RenderableCell: Render {
|
||||
const CELL_TYPE: CellType;
|
||||
|
||||
fn id(&self) -> &CellId;
|
||||
fn cell_type(&self) -> CellType;
|
||||
fn metadata(&self) -> &CellMetadata;
|
||||
fn source(&self) -> &String;
|
||||
fn selected(&self) -> bool;
|
||||
fn set_selected(&mut self, selected: bool) -> &mut Self;
|
||||
fn selected_bg_color(&self, cx: &ViewContext<Self>) -> Hsla {
|
||||
if self.selected() {
|
||||
let mut color = cx.theme().colors().icon_accent;
|
||||
color.fade_out(0.9);
|
||||
color
|
||||
} else {
|
||||
// TODO: this is wrong
|
||||
cx.theme().colors().tab_bar_background
|
||||
}
|
||||
}
|
||||
fn control(&self, _cx: &ViewContext<Self>) -> Option<CellControl> {
|
||||
None
|
||||
}
|
||||
|
||||
fn cell_position_spacer(
|
||||
&self,
|
||||
is_first: bool,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
let cell_position = self.cell_position();
|
||||
|
||||
if (cell_position == Some(&CellPosition::First) && is_first)
|
||||
|| (cell_position == Some(&CellPosition::Last) && !is_first)
|
||||
{
|
||||
Some(div().flex().w_full().h(Spacing::XLarge.px(cx)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn gutter(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
let is_selected = self.selected();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.h_full()
|
||||
.w(px(GUTTER_WIDTH))
|
||||
.child(
|
||||
div()
|
||||
.w(px(GUTTER_WIDTH))
|
||||
.flex()
|
||||
.flex_none()
|
||||
.justify_center()
|
||||
.h_full()
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(px(1.))
|
||||
.h_full()
|
||||
.when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
|
||||
.when(!is_selected, |this| this.bg(cx.theme().colors().border)),
|
||||
),
|
||||
)
|
||||
.when_some(self.control(cx), |this, control| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(CODE_BLOCK_INSET - 2.0))
|
||||
.left_0()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.w(px(GUTTER_WIDTH))
|
||||
.h(px(GUTTER_WIDTH + 12.0))
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
.child(control.button),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn cell_position(&self) -> Option<&CellPosition>;
|
||||
fn set_cell_position(&mut self, position: CellPosition) -> &mut Self;
|
||||
}
|
||||
|
||||
pub trait RunnableCell: RenderableCell {
|
||||
fn execution_count(&self) -> Option<i32>;
|
||||
fn set_execution_count(&mut self, count: i32) -> &mut Self;
|
||||
fn run(&mut self, cx: &mut ViewContext<Self>) -> ();
|
||||
}
|
||||
|
||||
pub struct MarkdownCell {
|
||||
id: CellId,
|
||||
metadata: CellMetadata,
|
||||
source: String,
|
||||
parsed_markdown: Option<markdown_preview::markdown_elements::ParsedMarkdown>,
|
||||
markdown_parsing_task: Task<()>,
|
||||
selected: bool,
|
||||
cell_position: Option<CellPosition>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
impl RenderableCell for MarkdownCell {
|
||||
const CELL_TYPE: CellType = CellType::Markdown;
|
||||
|
||||
fn id(&self) -> &CellId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn cell_type(&self) -> CellType {
|
||||
CellType::Markdown
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &CellMetadata {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
fn source(&self) -> &String {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn set_selected(&mut self, selected: bool) -> &mut Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn control(&self, _: &ViewContext<Self>) -> Option<CellControl> {
|
||||
None
|
||||
}
|
||||
|
||||
fn cell_position(&self) -> Option<&CellPosition> {
|
||||
self.cell_position.as_ref()
|
||||
}
|
||||
|
||||
fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
|
||||
self.cell_position = Some(cell_position);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MarkdownCell {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Some(parsed) = self.parsed_markdown.as_ref() else {
|
||||
return div();
|
||||
};
|
||||
|
||||
let mut markdown_render_context =
|
||||
markdown_preview::markdown_renderer::RenderContext::new(None, cx);
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
// TODO: Move base cell render into trait impl so we don't have to repeat this
|
||||
.children(self.cell_position_spacer(true, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pr_6()
|
||||
.rounded_sm()
|
||||
.items_start()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.bg(self.selected_bg_color(cx))
|
||||
.child(self.gutter(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.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),
|
||||
))
|
||||
})),
|
||||
),
|
||||
)
|
||||
// TODO: Move base cell render into trait impl so we don't have to repeat this
|
||||
.children(self.cell_position_spacer(false, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeCell {
|
||||
id: CellId,
|
||||
metadata: CellMetadata,
|
||||
execution_count: Option<i32>,
|
||||
source: String,
|
||||
editor: View<editor::Editor>,
|
||||
outputs: Vec<Output>,
|
||||
selected: bool,
|
||||
cell_position: Option<CellPosition>,
|
||||
language_task: Task<()>,
|
||||
}
|
||||
|
||||
impl CodeCell {
|
||||
pub fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
self.editor.read(cx).buffer().read(cx).is_dirty(cx)
|
||||
}
|
||||
pub fn has_outputs(&self) -> bool {
|
||||
!self.outputs.is_empty()
|
||||
}
|
||||
|
||||
pub fn clear_outputs(&mut self) {
|
||||
self.outputs.clear();
|
||||
}
|
||||
|
||||
fn output_control(&self) -> Option<CellControlType> {
|
||||
if self.has_outputs() {
|
||||
Some(CellControlType::ClearCell)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gutter_output(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
let is_selected = self.selected();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.h_full()
|
||||
.w(px(GUTTER_WIDTH))
|
||||
.child(
|
||||
div()
|
||||
.w(px(GUTTER_WIDTH))
|
||||
.flex()
|
||||
.flex_none()
|
||||
.justify_center()
|
||||
.h_full()
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(px(1.))
|
||||
.h_full()
|
||||
.when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
|
||||
.when(!is_selected, |this| this.bg(cx.theme().colors().border)),
|
||||
),
|
||||
)
|
||||
.when(self.has_outputs(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(CODE_BLOCK_INSET - 2.0))
|
||||
.left_0()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.w(px(GUTTER_WIDTH))
|
||||
.h(px(GUTTER_WIDTH + 12.0))
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
.child(IconButton::new("control", IconName::Ellipsis)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderableCell for CodeCell {
|
||||
const CELL_TYPE: CellType = CellType::Code;
|
||||
|
||||
fn id(&self) -> &CellId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn cell_type(&self) -> CellType {
|
||||
CellType::Code
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &CellMetadata {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
fn source(&self) -> &String {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn control(&self, cx: &ViewContext<Self>) -> Option<CellControl> {
|
||||
let cell_control = if self.has_outputs() {
|
||||
CellControl::new("rerun-cell", CellControlType::RerunCell)
|
||||
} else {
|
||||
CellControl::new("run-cell", CellControlType::RunCell)
|
||||
.on_click(cx.listener(move |this, _, cx| this.run(cx)))
|
||||
};
|
||||
|
||||
Some(cell_control)
|
||||
}
|
||||
|
||||
fn selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn set_selected(&mut self, selected: bool) -> &mut Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn cell_position(&self) -> Option<&CellPosition> {
|
||||
self.cell_position.as_ref()
|
||||
}
|
||||
|
||||
fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
|
||||
self.cell_position = Some(cell_position);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RunnableCell for CodeCell {
|
||||
fn run(&mut self, cx: &mut ViewContext<Self>) {
|
||||
println!("Running code cell: {}", self.id);
|
||||
}
|
||||
|
||||
fn execution_count(&self) -> Option<i32> {
|
||||
self.execution_count
|
||||
.and_then(|count| if count > 0 { Some(count) } else { None })
|
||||
}
|
||||
|
||||
fn set_execution_count(&mut self, count: i32) -> &mut Self {
|
||||
self.execution_count = Some(count);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CodeCell {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let lines = self.source.lines().count();
|
||||
let height = lines as f32 * cx.line_height();
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
// TODO: Move base cell render into trait impl so we don't have to repeat this
|
||||
.children(self.cell_position_spacer(true, cx))
|
||||
// Editor portion
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pr_6()
|
||||
.rounded_sm()
|
||||
.items_start()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.bg(self.selected_bg_color(cx))
|
||||
.child(self.gutter(cx))
|
||||
.child(
|
||||
div().py_1p5().w_full().child(
|
||||
div()
|
||||
.flex()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.py_3()
|
||||
.px_5()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(div().h(height).w_full().child(self.editor.clone())),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Output portion
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pr_6()
|
||||
.rounded_sm()
|
||||
.items_start()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.bg(self.selected_bg_color(cx))
|
||||
.child(self.gutter_output(cx))
|
||||
.child(
|
||||
div().py_1p5().w_full().child(
|
||||
div()
|
||||
.flex()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.py_3()
|
||||
.px_5()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
// .border_color(cx.theme().colors().border)
|
||||
// .bg(cx.theme().colors().editor_background)
|
||||
.child(div().w_full().children(self.outputs.iter().map(
|
||||
|output| {
|
||||
let content = match output {
|
||||
Output::Plain { content, .. } => {
|
||||
Some(content.clone().into_any_element())
|
||||
}
|
||||
Output::Markdown { content, .. } => {
|
||||
Some(content.clone().into_any_element())
|
||||
}
|
||||
Output::Stream { content, .. } => {
|
||||
Some(content.clone().into_any_element())
|
||||
}
|
||||
Output::Image { content, .. } => {
|
||||
Some(content.clone().into_any_element())
|
||||
}
|
||||
Output::Message(message) => Some(
|
||||
div().child(message.clone()).into_any_element(),
|
||||
),
|
||||
Output::Table { content, .. } => {
|
||||
Some(content.clone().into_any_element())
|
||||
}
|
||||
Output::ErrorOutput(error_view) => {
|
||||
error_view.render(cx)
|
||||
}
|
||||
Output::ClearOutputWaitMarker => None,
|
||||
};
|
||||
|
||||
div()
|
||||
// .w_full()
|
||||
// .mt_3()
|
||||
// .p_3()
|
||||
// .rounded_md()
|
||||
// .bg(cx.theme().colors().editor_background)
|
||||
// .border(px(1.))
|
||||
// .border_color(cx.theme().colors().border)
|
||||
// .shadow_sm()
|
||||
.children(content)
|
||||
},
|
||||
))),
|
||||
),
|
||||
),
|
||||
)
|
||||
// TODO: Move base cell render into trait impl so we don't have to repeat this
|
||||
.children(self.cell_position_spacer(false, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RawCell {
|
||||
id: CellId,
|
||||
metadata: CellMetadata,
|
||||
source: String,
|
||||
selected: bool,
|
||||
cell_position: Option<CellPosition>,
|
||||
}
|
||||
|
||||
impl RenderableCell for RawCell {
|
||||
const CELL_TYPE: CellType = CellType::Raw;
|
||||
|
||||
fn id(&self) -> &CellId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn cell_type(&self) -> CellType {
|
||||
CellType::Raw
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &CellMetadata {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
fn source(&self) -> &String {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn set_selected(&mut self, selected: bool) -> &mut Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn cell_position(&self) -> Option<&CellPosition> {
|
||||
self.cell_position.as_ref()
|
||||
}
|
||||
|
||||
fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
|
||||
self.cell_position = Some(cell_position);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RawCell {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
// TODO: Move base cell render into trait impl so we don't have to repeat this
|
||||
.children(self.cell_position_spacer(true, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pr_2()
|
||||
.rounded_sm()
|
||||
.items_start()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.bg(self.selected_bg_color(cx))
|
||||
.child(self.gutter(cx))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.p_3()
|
||||
.font_ui(cx)
|
||||
.text_size(TextSize::Default.rems(cx))
|
||||
.child(self.source.clone()),
|
||||
),
|
||||
)
|
||||
// TODO: Move base cell render into trait impl so we don't have to repeat this
|
||||
.children(self.cell_position_spacer(false, cx))
|
||||
}
|
||||
}
|
672
crates/repl/src/notebook/notebook_ui.rs
Normal file
672
crates/repl/src/notebook/notebook_ui.rs
Normal file
|
@ -0,0 +1,672 @@
|
|||
#![allow(unused, dead_code)]
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use client::proto::ViewId;
|
||||
use collections::HashMap;
|
||||
use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag};
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions, list, prelude::*, AppContext, EventEmitter, FocusHandle, FocusableView,
|
||||
ListScrollEvent, ListState, Model, Task,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use workspace::item::ItemEvent;
|
||||
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;
|
||||
|
||||
pub(crate) const DEFAULT_NOTEBOOK_FORMAT: i32 = 4;
|
||||
pub(crate) const DEFAULT_NOTEBOOK_FORMAT_MINOR: i32 = 0;
|
||||
|
||||
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>,
|
||||
|
||||
focus_handle: FocusHandle,
|
||||
project: Model<Project>,
|
||||
path: ProjectPath,
|
||||
|
||||
remote_id: Option<ViewId>,
|
||||
cell_list: ListState,
|
||||
|
||||
metadata: NotebookMetadata,
|
||||
nbformat: i32,
|
||||
nbformat_minor: i32,
|
||||
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 notebook = notebook_item.read(cx).notebook.clone();
|
||||
|
||||
let languages = project.read(cx).languages().clone();
|
||||
|
||||
let metadata = notebook.metadata;
|
||||
let nbformat = notebook.nbformat;
|
||||
let nbformat_minor = notebook.nbformat_minor;
|
||||
|
||||
let language_name = metadata
|
||||
.language_info
|
||||
.as_ref()
|
||||
.map(|l| l.name.clone())
|
||||
.or(metadata
|
||||
.kernelspec
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.language.clone()));
|
||||
|
||||
let notebook_language = if let Some(language_name) = language_name {
|
||||
cx.spawn(|_, _| {
|
||||
let languages = languages.clone();
|
||||
async move { languages.language_for_name(&language_name).await.ok() }
|
||||
})
|
||||
.shared()
|
||||
} else {
|
||||
Task::ready(None).shared()
|
||||
};
|
||||
|
||||
let languages = project.read(cx).languages().clone();
|
||||
let notebook_language = cx
|
||||
.spawn(|_, _| {
|
||||
// todo: pull from notebook metadata
|
||||
const TODO: &'static str = "Python";
|
||||
let languages = languages.clone();
|
||||
async move { languages.language_for_name(TODO).await.ok() }
|
||||
})
|
||||
.shared();
|
||||
|
||||
let mut cell_order = vec![];
|
||||
let mut cell_map = HashMap::default();
|
||||
|
||||
for (index, cell) in notebook.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 cell_order_for_list = cell_order.clone();
|
||||
let cell_map_for_list = cell_map.clone();
|
||||
|
||||
let cell_list = ListState::new(
|
||||
cell_count,
|
||||
gpui::ListAlignment::Top,
|
||||
// TODO: This is a totally random number,
|
||||
// not sure what this should be
|
||||
px(3000.),
|
||||
move |ix, cx| {
|
||||
let cell_order_for_list = cell_order_for_list.clone();
|
||||
let cell_id = cell_order_for_list[ix].clone();
|
||||
if let Some(view) = view.upgrade() {
|
||||
let cell_id = cell_id.clone();
|
||||
if let Some(cell) = cell_map_for_list.clone().get(&cell_id) {
|
||||
view.update(cx, |view, cx| {
|
||||
view.render_cell(ix, cell, cx).into_any_element()
|
||||
})
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
languages: languages.clone(),
|
||||
focus_handle,
|
||||
project,
|
||||
path: notebook_item.read(cx).project_path.clone(),
|
||||
remote_id: None,
|
||||
cell_list,
|
||||
selected_cell_index: 0,
|
||||
metadata,
|
||||
nbformat,
|
||||
nbformat_minor,
|
||||
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 is_dirty(&self, cx: &AppContext) -> bool {
|
||||
self.cell_map.values().any(|cell| {
|
||||
if let Cell::Code(code_cell) = cell {
|
||||
code_cell.read(cx).is_dirty(cx)
|
||||
} 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(Spacing::Small.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(Spacing::XXLarge.rems(cx))
|
||||
.justify_between()
|
||||
.flex_none()
|
||||
.h_full()
|
||||
.py(Spacing::XLarge.px(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap(Spacing::Large.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(Spacing::Large.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(Spacing::XLarge.px(cx))
|
||||
.gap(Spacing::XLarge.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()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotebookItem {
|
||||
path: PathBuf,
|
||||
project_path: ProjectPath,
|
||||
notebook: nbformat::v4::Notebook,
|
||||
}
|
||||
|
||||
impl project::Item 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();
|
||||
|
||||
if path.path.extension().unwrap_or_default() == "ipynb" {
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let abs_path = project
|
||||
.read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to find the absolute path"))?;
|
||||
|
||||
let file_content = std::fs::read_to_string(abs_path.clone())?;
|
||||
let notebook = nbformat::parse_notebook(&file_content);
|
||||
|
||||
let notebook = match notebook {
|
||||
Ok(nbformat::Notebook::V4(notebook)) => notebook,
|
||||
Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
|
||||
// todo!(): Decide if we want to mutate the notebook by including Cell IDs
|
||||
// and any other conversions
|
||||
let notebook = nbformat::upgrade_legacy_notebook(legacy_notebook)?;
|
||||
notebook
|
||||
}
|
||||
Err(e) => {
|
||||
anyhow::bail!("Failed to parse notebook: {:?}", e);
|
||||
}
|
||||
};
|
||||
|
||||
cx.new_model(|_| NotebookItem {
|
||||
path: abs_path,
|
||||
project_path: path,
|
||||
notebook,
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||
Some(self.project_path.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for NotebookEditor {}
|
||||
|
||||
// pub struct NotebookControls {
|
||||
// pane_focused: bool,
|
||||
// active_item: Option<Box<dyn ItemHandle>>,
|
||||
// // subscription: Option<Subscription>,
|
||||
// }
|
||||
|
||||
// impl NotebookControls {
|
||||
// pub fn new() -> Self {
|
||||
// Self {
|
||||
// pane_focused: false,
|
||||
// active_item: Default::default(),
|
||||
// // subscription: Default::default(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl EventEmitter<ToolbarItemEvent> for NotebookControls {}
|
||||
|
||||
// impl Render for NotebookControls {
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// div().child("notebook controls")
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl ToolbarItemView for NotebookControls {
|
||||
// fn set_active_pane_item(
|
||||
// &mut self,
|
||||
// active_pane_item: Option<&dyn workspace::ItemHandle>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> workspace::ToolbarItemLocation {
|
||||
// cx.notify();
|
||||
// self.active_item = None;
|
||||
|
||||
// let Some(item) = active_pane_item else {
|
||||
// return ToolbarItemLocation::Hidden;
|
||||
// };
|
||||
|
||||
// ToolbarItemLocation::PrimaryLeft
|
||||
// }
|
||||
|
||||
// fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
// self.pane_focused = pane_focused;
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Item for NotebookEditor {
|
||||
type Event = ();
|
||||
|
||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||
let path = self.path.path.clone();
|
||||
|
||||
path.file_stem()
|
||||
.map(|stem| stem.to_string_lossy().into_owned())
|
||||
.map(SharedString::from)
|
||||
}
|
||||
|
||||
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
|
||||
Some(IconName::Book.into())
|
||||
}
|
||||
|
||||
fn show_toolbar(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
// self.is_dirty(cx)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement this to allow us to persist to the database, etc:
|
||||
// impl SerializableItem for NotebookEditor {}
|
||||
|
||||
impl ProjectItem for NotebookEditor {
|
||||
type Item = NotebookItem;
|
||||
|
||||
fn for_project_item(
|
||||
project: Model<Project>,
|
||||
item: Model<Self::Item>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::new(project, item, cx)
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@ use table::TableView;
|
|||
pub mod plain;
|
||||
use plain::TerminalOutput;
|
||||
|
||||
mod user_error;
|
||||
pub(crate) mod user_error;
|
||||
use user_error::ErrorView;
|
||||
use workspace::Workspace;
|
||||
|
||||
|
@ -201,7 +201,7 @@ impl Output {
|
|||
)
|
||||
}
|
||||
|
||||
fn render(
|
||||
pub fn render(
|
||||
&self,
|
||||
|
||||
workspace: WeakView<Workspace>,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mod components;
|
||||
mod jupyter_settings;
|
||||
mod kernels;
|
||||
pub mod notebook;
|
||||
mod outputs;
|
||||
mod repl_editor;
|
||||
mod repl_sessions_ui;
|
||||
|
|
|
@ -212,6 +212,7 @@ pub enum IconName {
|
|||
LineHeight,
|
||||
Link,
|
||||
ListTree,
|
||||
ListX,
|
||||
MagnifyingGlass,
|
||||
MailOpen,
|
||||
Maximize,
|
||||
|
@ -291,6 +292,12 @@ pub enum IconName {
|
|||
ZedXCopilot,
|
||||
}
|
||||
|
||||
impl From<IconName> for Icon {
|
||||
fn from(icon: IconName) -> Self {
|
||||
Icon::new(icon)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Icon {
|
||||
path: SharedString,
|
||||
|
|
|
@ -418,6 +418,7 @@ fn main() {
|
|||
app_state.languages.set_theme(cx.theme().clone());
|
||||
editor::init(cx);
|
||||
image_viewer::init(cx);
|
||||
repl::notebook::init(cx);
|
||||
diagnostics::init(cx);
|
||||
|
||||
audio::init(Assets, cx);
|
||||
|
|
|
@ -3505,6 +3505,7 @@ mod tests {
|
|||
app_state.client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
repl::notebook::init(cx);
|
||||
tasks_ui::init(cx);
|
||||
initialize_workspace(app_state.clone(), prompt_builder, cx);
|
||||
search::init(cx);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue