1#![allow(unused, dead_code)]
2use std::sync::Arc;
3
4use editor::{Editor, EditorMode, MultiBuffer};
5use futures::future::Shared;
6use gpui::{
7 App, Entity, Hsla, RetainAllImageCache, Task, TextStyleRefinement, image_cache, prelude::*,
8};
9use language::{Buffer, Language, LanguageRegistry};
10use markdown_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block};
11use nbformat::v4::{CellId, CellMetadata, CellType};
12use settings::Settings as _;
13use theme::ThemeSettings;
14use ui::{IconButtonShape, prelude::*};
15use util::ResultExt;
16
17use crate::{
18 notebook::{CODE_BLOCK_INSET, GUTTER_WIDTH},
19 outputs::{Output, plain::TerminalOutput, user_error::ErrorView},
20};
21
22#[derive(Copy, Clone, PartialEq, PartialOrd)]
23pub enum CellPosition {
24 First,
25 Middle,
26 Last,
27}
28
29pub enum CellControlType {
30 RunCell,
31 RerunCell,
32 ClearCell,
33 CellOptions,
34 CollapseCell,
35 ExpandCell,
36}
37
38impl CellControlType {
39 fn icon_name(&self) -> IconName {
40 match self {
41 CellControlType::RunCell => IconName::PlayFilled,
42 CellControlType::RerunCell => IconName::ArrowCircle,
43 CellControlType::ClearCell => IconName::ListX,
44 CellControlType::CellOptions => IconName::Ellipsis,
45 CellControlType::CollapseCell => IconName::ChevronDown,
46 CellControlType::ExpandCell => IconName::ChevronRight,
47 }
48 }
49}
50
51pub struct CellControl {
52 button: IconButton,
53}
54
55impl CellControl {
56 fn new(id: impl Into<SharedString>, control_type: CellControlType) -> Self {
57 let icon_name = control_type.icon_name();
58 let id = id.into();
59 let button = IconButton::new(id, icon_name)
60 .icon_size(IconSize::Small)
61 .shape(IconButtonShape::Square);
62 Self { button }
63 }
64}
65
66impl Clickable for CellControl {
67 fn on_click(
68 self,
69 handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
70 ) -> Self {
71 let button = self.button.on_click(handler);
72 Self { button }
73 }
74
75 fn cursor_style(self, _cursor_style: gpui::CursorStyle) -> Self {
76 self
77 }
78}
79
80/// A notebook cell
81#[derive(Clone)]
82pub enum Cell {
83 Code(Entity<CodeCell>),
84 Markdown(Entity<MarkdownCell>),
85 Raw(Entity<RawCell>),
86}
87
88fn convert_outputs(
89 outputs: &Vec<nbformat::v4::Output>,
90 window: &mut Window,
91 cx: &mut App,
92) -> Vec<Output> {
93 outputs
94 .iter()
95 .map(|output| match output {
96 nbformat::v4::Output::Stream { text, .. } => Output::Stream {
97 content: cx.new(|cx| TerminalOutput::from(&text.0, window, cx)),
98 },
99 nbformat::v4::Output::DisplayData(display_data) => {
100 Output::new(&display_data.data, None, window, cx)
101 }
102 nbformat::v4::Output::ExecuteResult(execute_result) => {
103 Output::new(&execute_result.data, None, window, cx)
104 }
105 nbformat::v4::Output::Error(error) => Output::ErrorOutput(ErrorView {
106 ename: error.ename.clone(),
107 evalue: error.evalue.clone(),
108 traceback: cx
109 .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
110 }),
111 })
112 .collect()
113}
114
115impl Cell {
116 pub fn load(
117 cell: &nbformat::v4::Cell,
118 languages: &Arc<LanguageRegistry>,
119 notebook_language: Shared<Task<Option<Arc<Language>>>>,
120 window: &mut Window,
121 cx: &mut App,
122 ) -> Self {
123 match cell {
124 nbformat::v4::Cell::Markdown {
125 id,
126 metadata,
127 source,
128 ..
129 } => {
130 let source = source.join("");
131
132 let entity = cx.new(|cx| {
133 let markdown_parsing_task = {
134 let languages = languages.clone();
135 let source = source.clone();
136
137 cx.spawn_in(window, async move |this, cx| {
138 let parsed_markdown = cx
139 .background_spawn(async move {
140 parse_markdown(&source, None, Some(languages)).await
141 })
142 .await;
143
144 this.update(cx, |cell: &mut MarkdownCell, _| {
145 cell.parsed_markdown = Some(parsed_markdown);
146 })
147 .log_err();
148 })
149 };
150
151 MarkdownCell {
152 markdown_parsing_task,
153 image_cache: RetainAllImageCache::new(cx),
154 languages: languages.clone(),
155 id: id.clone(),
156 metadata: metadata.clone(),
157 source: source.clone(),
158 parsed_markdown: None,
159 selected: false,
160 cell_position: None,
161 }
162 });
163
164 Cell::Markdown(entity)
165 }
166 nbformat::v4::Cell::Code {
167 id,
168 metadata,
169 execution_count,
170 source,
171 outputs,
172 } => Cell::Code(cx.new(|cx| {
173 let text = source.join("");
174
175 let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
176 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
177
178 let editor_view = cx.new(|cx| {
179 let mut editor = Editor::new(
180 EditorMode::AutoHeight {
181 min_lines: 1,
182 max_lines: Some(1024),
183 },
184 multi_buffer,
185 None,
186 window,
187 cx,
188 );
189
190 let theme = ThemeSettings::get_global(cx);
191
192 let refinement = TextStyleRefinement {
193 font_family: Some(theme.buffer_font.family.clone()),
194 font_size: Some(theme.buffer_font_size(cx).into()),
195 color: Some(cx.theme().colors().editor_foreground),
196 background_color: Some(gpui::transparent_black()),
197 ..Default::default()
198 };
199
200 editor.set_text(text, window, cx);
201 editor.set_show_gutter(false, cx);
202 editor.set_text_style_refinement(refinement);
203
204 // editor.set_read_only(true);
205 editor
206 });
207
208 let buffer = buffer.clone();
209 let language_task = cx.spawn_in(window, async move |this, cx| {
210 let language = notebook_language.await;
211
212 buffer.update(cx, |buffer, cx| {
213 buffer.set_language(language.clone(), cx);
214 });
215 });
216
217 CodeCell {
218 id: id.clone(),
219 metadata: metadata.clone(),
220 execution_count: *execution_count,
221 source: source.join(""),
222 editor: editor_view,
223 outputs: convert_outputs(outputs, window, cx),
224 selected: false,
225 language_task,
226 cell_position: None,
227 }
228 })),
229 nbformat::v4::Cell::Raw {
230 id,
231 metadata,
232 source,
233 } => Cell::Raw(cx.new(|_| RawCell {
234 id: id.clone(),
235 metadata: metadata.clone(),
236 source: source.join(""),
237 selected: false,
238 cell_position: None,
239 })),
240 }
241 }
242}
243
244pub trait RenderableCell: Render {
245 const CELL_TYPE: CellType;
246
247 fn id(&self) -> &CellId;
248 fn cell_type(&self) -> CellType;
249 fn metadata(&self) -> &CellMetadata;
250 fn source(&self) -> &String;
251 fn selected(&self) -> bool;
252 fn set_selected(&mut self, selected: bool) -> &mut Self;
253 fn selected_bg_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
254 if self.selected() {
255 let mut color = cx.theme().colors().icon_accent;
256 color.fade_out(0.9);
257 color
258 } else {
259 // TODO: this is wrong
260 cx.theme().colors().tab_bar_background
261 }
262 }
263 fn control(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<CellControl> {
264 None
265 }
266
267 fn cell_position_spacer(
268 &self,
269 is_first: bool,
270 window: &mut Window,
271 cx: &mut Context<Self>,
272 ) -> Option<impl IntoElement> {
273 let cell_position = self.cell_position();
274
275 if (cell_position == Some(&CellPosition::First) && is_first)
276 || (cell_position == Some(&CellPosition::Last) && !is_first)
277 {
278 Some(div().flex().w_full().h(DynamicSpacing::Base12.px(cx)))
279 } else {
280 None
281 }
282 }
283
284 fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
285 let is_selected = self.selected();
286
287 div()
288 .relative()
289 .h_full()
290 .w(px(GUTTER_WIDTH))
291 .child(
292 div()
293 .w(px(GUTTER_WIDTH))
294 .flex()
295 .flex_none()
296 .justify_center()
297 .h_full()
298 .child(
299 div()
300 .flex_none()
301 .w(px(1.))
302 .h_full()
303 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
304 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
305 ),
306 )
307 .when_some(self.control(window, cx), |this, control| {
308 this.child(
309 div()
310 .absolute()
311 .top(px(CODE_BLOCK_INSET - 2.0))
312 .left_0()
313 .flex()
314 .flex_none()
315 .w(px(GUTTER_WIDTH))
316 .h(px(GUTTER_WIDTH + 12.0))
317 .items_center()
318 .justify_center()
319 .bg(cx.theme().colors().tab_bar_background)
320 .child(control.button),
321 )
322 })
323 }
324
325 fn cell_position(&self) -> Option<&CellPosition>;
326 fn set_cell_position(&mut self, position: CellPosition) -> &mut Self;
327}
328
329pub trait RunnableCell: RenderableCell {
330 fn execution_count(&self) -> Option<i32>;
331 fn set_execution_count(&mut self, count: i32) -> &mut Self;
332 fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) -> ();
333}
334
335pub struct MarkdownCell {
336 id: CellId,
337 metadata: CellMetadata,
338 image_cache: Entity<RetainAllImageCache>,
339 source: String,
340 parsed_markdown: Option<markdown_preview::markdown_elements::ParsedMarkdown>,
341 markdown_parsing_task: Task<()>,
342 selected: bool,
343 cell_position: Option<CellPosition>,
344 languages: Arc<LanguageRegistry>,
345}
346
347impl RenderableCell for MarkdownCell {
348 const CELL_TYPE: CellType = CellType::Markdown;
349
350 fn id(&self) -> &CellId {
351 &self.id
352 }
353
354 fn cell_type(&self) -> CellType {
355 CellType::Markdown
356 }
357
358 fn metadata(&self) -> &CellMetadata {
359 &self.metadata
360 }
361
362 fn source(&self) -> &String {
363 &self.source
364 }
365
366 fn selected(&self) -> bool {
367 self.selected
368 }
369
370 fn set_selected(&mut self, selected: bool) -> &mut Self {
371 self.selected = selected;
372 self
373 }
374
375 fn control(&self, _window: &mut Window, _: &mut Context<Self>) -> Option<CellControl> {
376 None
377 }
378
379 fn cell_position(&self) -> Option<&CellPosition> {
380 self.cell_position.as_ref()
381 }
382
383 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
384 self.cell_position = Some(cell_position);
385 self
386 }
387}
388
389impl Render for MarkdownCell {
390 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
391 let Some(parsed) = self.parsed_markdown.as_ref() else {
392 return div();
393 };
394
395 let mut markdown_render_context =
396 markdown_preview::markdown_renderer::RenderContext::new(None, window, cx);
397
398 v_flex()
399 .size_full()
400 // TODO: Move base cell render into trait impl so we don't have to repeat this
401 .children(self.cell_position_spacer(true, window, cx))
402 .child(
403 h_flex()
404 .w_full()
405 .pr_6()
406 .rounded_xs()
407 .items_start()
408 .gap(DynamicSpacing::Base08.rems(cx))
409 .bg(self.selected_bg_color(window, cx))
410 .child(self.gutter(window, cx))
411 .child(
412 v_flex()
413 .image_cache(self.image_cache.clone())
414 .size_full()
415 .flex_1()
416 .p_3()
417 .font_ui(cx)
418 .text_size(TextSize::Default.rems(cx))
419 .children(parsed.children.iter().map(|child| {
420 div().relative().child(div().relative().child(
421 render_markdown_block(child, &mut markdown_render_context),
422 ))
423 })),
424 ),
425 )
426 // TODO: Move base cell render into trait impl so we don't have to repeat this
427 .children(self.cell_position_spacer(false, window, cx))
428 }
429}
430
431pub struct CodeCell {
432 id: CellId,
433 metadata: CellMetadata,
434 execution_count: Option<i32>,
435 source: String,
436 editor: Entity<editor::Editor>,
437 outputs: Vec<Output>,
438 selected: bool,
439 cell_position: Option<CellPosition>,
440 language_task: Task<()>,
441}
442
443impl CodeCell {
444 pub fn is_dirty(&self, cx: &App) -> bool {
445 self.editor.read(cx).buffer().read(cx).is_dirty(cx)
446 }
447 pub fn has_outputs(&self) -> bool {
448 !self.outputs.is_empty()
449 }
450
451 pub fn clear_outputs(&mut self) {
452 self.outputs.clear();
453 }
454
455 fn output_control(&self) -> Option<CellControlType> {
456 if self.has_outputs() {
457 Some(CellControlType::ClearCell)
458 } else {
459 None
460 }
461 }
462
463 pub fn gutter_output(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
464 let is_selected = self.selected();
465
466 div()
467 .relative()
468 .h_full()
469 .w(px(GUTTER_WIDTH))
470 .child(
471 div()
472 .w(px(GUTTER_WIDTH))
473 .flex()
474 .flex_none()
475 .justify_center()
476 .h_full()
477 .child(
478 div()
479 .flex_none()
480 .w(px(1.))
481 .h_full()
482 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
483 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
484 ),
485 )
486 .when(self.has_outputs(), |this| {
487 this.child(
488 div()
489 .absolute()
490 .top(px(CODE_BLOCK_INSET - 2.0))
491 .left_0()
492 .flex()
493 .flex_none()
494 .w(px(GUTTER_WIDTH))
495 .h(px(GUTTER_WIDTH + 12.0))
496 .items_center()
497 .justify_center()
498 .bg(cx.theme().colors().tab_bar_background)
499 .child(IconButton::new("control", IconName::Ellipsis)),
500 )
501 })
502 }
503}
504
505impl RenderableCell for CodeCell {
506 const CELL_TYPE: CellType = CellType::Code;
507
508 fn id(&self) -> &CellId {
509 &self.id
510 }
511
512 fn cell_type(&self) -> CellType {
513 CellType::Code
514 }
515
516 fn metadata(&self) -> &CellMetadata {
517 &self.metadata
518 }
519
520 fn source(&self) -> &String {
521 &self.source
522 }
523
524 fn control(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<CellControl> {
525 let cell_control = if self.has_outputs() {
526 CellControl::new("rerun-cell", CellControlType::RerunCell)
527 } else {
528 CellControl::new("run-cell", CellControlType::RunCell)
529 .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx)))
530 };
531
532 Some(cell_control)
533 }
534
535 fn selected(&self) -> bool {
536 self.selected
537 }
538
539 fn set_selected(&mut self, selected: bool) -> &mut Self {
540 self.selected = selected;
541 self
542 }
543
544 fn cell_position(&self) -> Option<&CellPosition> {
545 self.cell_position.as_ref()
546 }
547
548 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
549 self.cell_position = Some(cell_position);
550 self
551 }
552}
553
554impl RunnableCell for CodeCell {
555 fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) {
556 println!("Running code cell: {}", self.id);
557 }
558
559 fn execution_count(&self) -> Option<i32> {
560 self.execution_count
561 .and_then(|count| if count > 0 { Some(count) } else { None })
562 }
563
564 fn set_execution_count(&mut self, count: i32) -> &mut Self {
565 self.execution_count = Some(count);
566 self
567 }
568}
569
570impl Render for CodeCell {
571 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
572 v_flex()
573 .size_full()
574 // TODO: Move base cell render into trait impl so we don't have to repeat this
575 .children(self.cell_position_spacer(true, window, cx))
576 // Editor portion
577 .child(
578 h_flex()
579 .w_full()
580 .pr_6()
581 .rounded_xs()
582 .items_start()
583 .gap(DynamicSpacing::Base08.rems(cx))
584 .bg(self.selected_bg_color(window, cx))
585 .child(self.gutter(window, cx))
586 .child(
587 div().py_1p5().w_full().child(
588 div()
589 .flex()
590 .size_full()
591 .flex_1()
592 .py_3()
593 .px_5()
594 .rounded_lg()
595 .border_1()
596 .border_color(cx.theme().colors().border)
597 .bg(cx.theme().colors().editor_background)
598 .child(div().w_full().child(self.editor.clone())),
599 ),
600 ),
601 )
602 // Output portion
603 .child(
604 h_flex()
605 .w_full()
606 .pr_6()
607 .rounded_xs()
608 .items_start()
609 .gap(DynamicSpacing::Base08.rems(cx))
610 .bg(self.selected_bg_color(window, cx))
611 .child(self.gutter_output(window, cx))
612 .child(
613 div().py_1p5().w_full().child(
614 div()
615 .flex()
616 .size_full()
617 .flex_1()
618 .py_3()
619 .px_5()
620 .rounded_lg()
621 .border_1()
622 // .border_color(cx.theme().colors().border)
623 // .bg(cx.theme().colors().editor_background)
624 .child(div().w_full().children(self.outputs.iter().map(
625 |output| {
626 let content = match output {
627 Output::Plain { content, .. } => {
628 Some(content.clone().into_any_element())
629 }
630 Output::Markdown { content, .. } => {
631 Some(content.clone().into_any_element())
632 }
633 Output::Stream { content, .. } => {
634 Some(content.clone().into_any_element())
635 }
636 Output::Image { content, .. } => {
637 Some(content.clone().into_any_element())
638 }
639 Output::Message(message) => Some(
640 div().child(message.clone()).into_any_element(),
641 ),
642 Output::Table { content, .. } => {
643 Some(content.clone().into_any_element())
644 }
645 Output::ErrorOutput(error_view) => {
646 error_view.render(window, cx)
647 }
648 Output::ClearOutputWaitMarker => None,
649 };
650
651 div()
652 // .w_full()
653 // .mt_3()
654 // .p_3()
655 // .rounded_sm()
656 // .bg(cx.theme().colors().editor_background)
657 // .border(px(1.))
658 // .border_color(cx.theme().colors().border)
659 // .shadow_xs()
660 .children(content)
661 },
662 ))),
663 ),
664 ),
665 )
666 // TODO: Move base cell render into trait impl so we don't have to repeat this
667 .children(self.cell_position_spacer(false, window, cx))
668 }
669}
670
671pub struct RawCell {
672 id: CellId,
673 metadata: CellMetadata,
674 source: String,
675 selected: bool,
676 cell_position: Option<CellPosition>,
677}
678
679impl RenderableCell for RawCell {
680 const CELL_TYPE: CellType = CellType::Raw;
681
682 fn id(&self) -> &CellId {
683 &self.id
684 }
685
686 fn cell_type(&self) -> CellType {
687 CellType::Raw
688 }
689
690 fn metadata(&self) -> &CellMetadata {
691 &self.metadata
692 }
693
694 fn source(&self) -> &String {
695 &self.source
696 }
697
698 fn selected(&self) -> bool {
699 self.selected
700 }
701
702 fn set_selected(&mut self, selected: bool) -> &mut Self {
703 self.selected = selected;
704 self
705 }
706
707 fn cell_position(&self) -> Option<&CellPosition> {
708 self.cell_position.as_ref()
709 }
710
711 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
712 self.cell_position = Some(cell_position);
713 self
714 }
715}
716
717impl Render for RawCell {
718 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
719 v_flex()
720 .size_full()
721 // TODO: Move base cell render into trait impl so we don't have to repeat this
722 .children(self.cell_position_spacer(true, window, cx))
723 .child(
724 h_flex()
725 .w_full()
726 .pr_2()
727 .rounded_xs()
728 .items_start()
729 .gap(DynamicSpacing::Base08.rems(cx))
730 .bg(self.selected_bg_color(window, cx))
731 .child(self.gutter(window, cx))
732 .child(
733 div()
734 .flex()
735 .size_full()
736 .flex_1()
737 .p_3()
738 .font_ui(cx)
739 .text_size(TextSize::Default.rems(cx))
740 .child(self.source.clone()),
741 ),
742 )
743 // TODO: Move base cell render into trait impl so we don't have to repeat this
744 .children(self.cell_position_spacer(false, window, cx))
745 }
746}