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::Play,
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 .into_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 { max_lines: 1024 },
181 multi_buffer,
182 None,
183 window,
184 cx,
185 );
186
187 let theme = ThemeSettings::get_global(cx);
188
189 let refinement = TextStyleRefinement {
190 font_family: Some(theme.buffer_font.family.clone()),
191 font_size: Some(theme.buffer_font_size(cx).into()),
192 color: Some(cx.theme().colors().editor_foreground),
193 background_color: Some(gpui::transparent_black()),
194 ..Default::default()
195 };
196
197 editor.set_text(text, window, cx);
198 editor.set_show_gutter(false, cx);
199 editor.set_text_style_refinement(refinement);
200
201 // editor.set_read_only(true);
202 editor
203 });
204
205 let buffer = buffer.clone();
206 let language_task = cx.spawn_in(window, async move |this, cx| {
207 let language = notebook_language.await;
208
209 buffer.update(cx, |buffer, cx| {
210 buffer.set_language(language.clone(), cx);
211 });
212 });
213
214 CodeCell {
215 id: id.clone(),
216 metadata: metadata.clone(),
217 execution_count: *execution_count,
218 source: source.join(""),
219 editor: editor_view,
220 outputs: convert_outputs(outputs, window, cx),
221 selected: false,
222 language_task,
223 cell_position: None,
224 }
225 })),
226 nbformat::v4::Cell::Raw {
227 id,
228 metadata,
229 source,
230 } => Cell::Raw(cx.new(|_| RawCell {
231 id: id.clone(),
232 metadata: metadata.clone(),
233 source: source.join(""),
234 selected: false,
235 cell_position: None,
236 })),
237 }
238 }
239}
240
241pub trait RenderableCell: Render {
242 const CELL_TYPE: CellType;
243
244 fn id(&self) -> &CellId;
245 fn cell_type(&self) -> CellType;
246 fn metadata(&self) -> &CellMetadata;
247 fn source(&self) -> &String;
248 fn selected(&self) -> bool;
249 fn set_selected(&mut self, selected: bool) -> &mut Self;
250 fn selected_bg_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
251 if self.selected() {
252 let mut color = cx.theme().colors().icon_accent;
253 color.fade_out(0.9);
254 color
255 } else {
256 // TODO: this is wrong
257 cx.theme().colors().tab_bar_background
258 }
259 }
260 fn control(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<CellControl> {
261 None
262 }
263
264 fn cell_position_spacer(
265 &self,
266 is_first: bool,
267 window: &mut Window,
268 cx: &mut Context<Self>,
269 ) -> Option<impl IntoElement> {
270 let cell_position = self.cell_position();
271
272 if (cell_position == Some(&CellPosition::First) && is_first)
273 || (cell_position == Some(&CellPosition::Last) && !is_first)
274 {
275 Some(div().flex().w_full().h(DynamicSpacing::Base12.px(cx)))
276 } else {
277 None
278 }
279 }
280
281 fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
282 let is_selected = self.selected();
283
284 div()
285 .relative()
286 .h_full()
287 .w(px(GUTTER_WIDTH))
288 .child(
289 div()
290 .w(px(GUTTER_WIDTH))
291 .flex()
292 .flex_none()
293 .justify_center()
294 .h_full()
295 .child(
296 div()
297 .flex_none()
298 .w(px(1.))
299 .h_full()
300 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
301 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
302 ),
303 )
304 .when_some(self.control(window, cx), |this, control| {
305 this.child(
306 div()
307 .absolute()
308 .top(px(CODE_BLOCK_INSET - 2.0))
309 .left_0()
310 .flex()
311 .flex_none()
312 .w(px(GUTTER_WIDTH))
313 .h(px(GUTTER_WIDTH + 12.0))
314 .items_center()
315 .justify_center()
316 .bg(cx.theme().colors().tab_bar_background)
317 .child(control.button),
318 )
319 })
320 }
321
322 fn cell_position(&self) -> Option<&CellPosition>;
323 fn set_cell_position(&mut self, position: CellPosition) -> &mut Self;
324}
325
326pub trait RunnableCell: RenderableCell {
327 fn execution_count(&self) -> Option<i32>;
328 fn set_execution_count(&mut self, count: i32) -> &mut Self;
329 fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) -> ();
330}
331
332pub struct MarkdownCell {
333 id: CellId,
334 metadata: CellMetadata,
335 image_cache: Entity<RetainAllImageCache>,
336 source: String,
337 parsed_markdown: Option<markdown_preview::markdown_elements::ParsedMarkdown>,
338 markdown_parsing_task: Task<()>,
339 selected: bool,
340 cell_position: Option<CellPosition>,
341 languages: Arc<LanguageRegistry>,
342}
343
344impl RenderableCell for MarkdownCell {
345 const CELL_TYPE: CellType = CellType::Markdown;
346
347 fn id(&self) -> &CellId {
348 &self.id
349 }
350
351 fn cell_type(&self) -> CellType {
352 CellType::Markdown
353 }
354
355 fn metadata(&self) -> &CellMetadata {
356 &self.metadata
357 }
358
359 fn source(&self) -> &String {
360 &self.source
361 }
362
363 fn selected(&self) -> bool {
364 self.selected
365 }
366
367 fn set_selected(&mut self, selected: bool) -> &mut Self {
368 self.selected = selected;
369 self
370 }
371
372 fn control(&self, _window: &mut Window, _: &mut Context<Self>) -> Option<CellControl> {
373 None
374 }
375
376 fn cell_position(&self) -> Option<&CellPosition> {
377 self.cell_position.as_ref()
378 }
379
380 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
381 self.cell_position = Some(cell_position);
382 self
383 }
384}
385
386impl Render for MarkdownCell {
387 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
388 let Some(parsed) = self.parsed_markdown.as_ref() else {
389 return div();
390 };
391
392 let mut markdown_render_context =
393 markdown_preview::markdown_renderer::RenderContext::new(None, window, cx);
394
395 v_flex()
396 .size_full()
397 // TODO: Move base cell render into trait impl so we don't have to repeat this
398 .children(self.cell_position_spacer(true, window, cx))
399 .child(
400 h_flex()
401 .w_full()
402 .pr_6()
403 .rounded_xs()
404 .items_start()
405 .gap(DynamicSpacing::Base08.rems(cx))
406 .bg(self.selected_bg_color(window, cx))
407 .child(self.gutter(window, cx))
408 .child(
409 v_flex()
410 .image_cache(self.image_cache.clone())
411 .size_full()
412 .flex_1()
413 .p_3()
414 .font_ui(cx)
415 .text_size(TextSize::Default.rems(cx))
416 .children(parsed.children.iter().map(|child| {
417 div().relative().child(div().relative().child(
418 render_markdown_block(child, &mut markdown_render_context),
419 ))
420 })),
421 ),
422 )
423 // TODO: Move base cell render into trait impl so we don't have to repeat this
424 .children(self.cell_position_spacer(false, window, cx))
425 }
426}
427
428pub struct CodeCell {
429 id: CellId,
430 metadata: CellMetadata,
431 execution_count: Option<i32>,
432 source: String,
433 editor: Entity<editor::Editor>,
434 outputs: Vec<Output>,
435 selected: bool,
436 cell_position: Option<CellPosition>,
437 language_task: Task<()>,
438}
439
440impl CodeCell {
441 pub fn is_dirty(&self, cx: &App) -> bool {
442 self.editor.read(cx).buffer().read(cx).is_dirty(cx)
443 }
444 pub fn has_outputs(&self) -> bool {
445 !self.outputs.is_empty()
446 }
447
448 pub fn clear_outputs(&mut self) {
449 self.outputs.clear();
450 }
451
452 fn output_control(&self) -> Option<CellControlType> {
453 if self.has_outputs() {
454 Some(CellControlType::ClearCell)
455 } else {
456 None
457 }
458 }
459
460 pub fn gutter_output(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
461 let is_selected = self.selected();
462
463 div()
464 .relative()
465 .h_full()
466 .w(px(GUTTER_WIDTH))
467 .child(
468 div()
469 .w(px(GUTTER_WIDTH))
470 .flex()
471 .flex_none()
472 .justify_center()
473 .h_full()
474 .child(
475 div()
476 .flex_none()
477 .w(px(1.))
478 .h_full()
479 .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
480 .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
481 ),
482 )
483 .when(self.has_outputs(), |this| {
484 this.child(
485 div()
486 .absolute()
487 .top(px(CODE_BLOCK_INSET - 2.0))
488 .left_0()
489 .flex()
490 .flex_none()
491 .w(px(GUTTER_WIDTH))
492 .h(px(GUTTER_WIDTH + 12.0))
493 .items_center()
494 .justify_center()
495 .bg(cx.theme().colors().tab_bar_background)
496 .child(IconButton::new("control", IconName::Ellipsis)),
497 )
498 })
499 }
500}
501
502impl RenderableCell for CodeCell {
503 const CELL_TYPE: CellType = CellType::Code;
504
505 fn id(&self) -> &CellId {
506 &self.id
507 }
508
509 fn cell_type(&self) -> CellType {
510 CellType::Code
511 }
512
513 fn metadata(&self) -> &CellMetadata {
514 &self.metadata
515 }
516
517 fn source(&self) -> &String {
518 &self.source
519 }
520
521 fn control(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<CellControl> {
522 let cell_control = if self.has_outputs() {
523 CellControl::new("rerun-cell", CellControlType::RerunCell)
524 } else {
525 CellControl::new("run-cell", CellControlType::RunCell)
526 .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx)))
527 };
528
529 Some(cell_control)
530 }
531
532 fn selected(&self) -> bool {
533 self.selected
534 }
535
536 fn set_selected(&mut self, selected: bool) -> &mut Self {
537 self.selected = selected;
538 self
539 }
540
541 fn cell_position(&self) -> Option<&CellPosition> {
542 self.cell_position.as_ref()
543 }
544
545 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
546 self.cell_position = Some(cell_position);
547 self
548 }
549}
550
551impl RunnableCell for CodeCell {
552 fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) {
553 println!("Running code cell: {}", self.id);
554 }
555
556 fn execution_count(&self) -> Option<i32> {
557 self.execution_count
558 .and_then(|count| if count > 0 { Some(count) } else { None })
559 }
560
561 fn set_execution_count(&mut self, count: i32) -> &mut Self {
562 self.execution_count = Some(count);
563 self
564 }
565}
566
567impl Render for CodeCell {
568 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
569 v_flex()
570 .size_full()
571 // TODO: Move base cell render into trait impl so we don't have to repeat this
572 .children(self.cell_position_spacer(true, window, cx))
573 // Editor portion
574 .child(
575 h_flex()
576 .w_full()
577 .pr_6()
578 .rounded_xs()
579 .items_start()
580 .gap(DynamicSpacing::Base08.rems(cx))
581 .bg(self.selected_bg_color(window, cx))
582 .child(self.gutter(window, cx))
583 .child(
584 div().py_1p5().w_full().child(
585 div()
586 .flex()
587 .size_full()
588 .flex_1()
589 .py_3()
590 .px_5()
591 .rounded_lg()
592 .border_1()
593 .border_color(cx.theme().colors().border)
594 .bg(cx.theme().colors().editor_background)
595 .child(div().w_full().child(self.editor.clone())),
596 ),
597 ),
598 )
599 // Output portion
600 .child(
601 h_flex()
602 .w_full()
603 .pr_6()
604 .rounded_xs()
605 .items_start()
606 .gap(DynamicSpacing::Base08.rems(cx))
607 .bg(self.selected_bg_color(window, cx))
608 .child(self.gutter_output(window, cx))
609 .child(
610 div().py_1p5().w_full().child(
611 div()
612 .flex()
613 .size_full()
614 .flex_1()
615 .py_3()
616 .px_5()
617 .rounded_lg()
618 .border_1()
619 // .border_color(cx.theme().colors().border)
620 // .bg(cx.theme().colors().editor_background)
621 .child(div().w_full().children(self.outputs.iter().map(
622 |output| {
623 let content = match output {
624 Output::Plain { content, .. } => {
625 Some(content.clone().into_any_element())
626 }
627 Output::Markdown { content, .. } => {
628 Some(content.clone().into_any_element())
629 }
630 Output::Stream { content, .. } => {
631 Some(content.clone().into_any_element())
632 }
633 Output::Image { content, .. } => {
634 Some(content.clone().into_any_element())
635 }
636 Output::Message(message) => Some(
637 div().child(message.clone()).into_any_element(),
638 ),
639 Output::Table { content, .. } => {
640 Some(content.clone().into_any_element())
641 }
642 Output::ErrorOutput(error_view) => {
643 error_view.render(window, cx)
644 }
645 Output::ClearOutputWaitMarker => None,
646 };
647
648 div()
649 // .w_full()
650 // .mt_3()
651 // .p_3()
652 // .rounded_sm()
653 // .bg(cx.theme().colors().editor_background)
654 // .border(px(1.))
655 // .border_color(cx.theme().colors().border)
656 // .shadow_sm()
657 .children(content)
658 },
659 ))),
660 ),
661 ),
662 )
663 // TODO: Move base cell render into trait impl so we don't have to repeat this
664 .children(self.cell_position_spacer(false, window, cx))
665 }
666}
667
668pub struct RawCell {
669 id: CellId,
670 metadata: CellMetadata,
671 source: String,
672 selected: bool,
673 cell_position: Option<CellPosition>,
674}
675
676impl RenderableCell for RawCell {
677 const CELL_TYPE: CellType = CellType::Raw;
678
679 fn id(&self) -> &CellId {
680 &self.id
681 }
682
683 fn cell_type(&self) -> CellType {
684 CellType::Raw
685 }
686
687 fn metadata(&self) -> &CellMetadata {
688 &self.metadata
689 }
690
691 fn source(&self) -> &String {
692 &self.source
693 }
694
695 fn selected(&self) -> bool {
696 self.selected
697 }
698
699 fn set_selected(&mut self, selected: bool) -> &mut Self {
700 self.selected = selected;
701 self
702 }
703
704 fn cell_position(&self) -> Option<&CellPosition> {
705 self.cell_position.as_ref()
706 }
707
708 fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
709 self.cell_position = Some(cell_position);
710 self
711 }
712}
713
714impl Render for RawCell {
715 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
716 v_flex()
717 .size_full()
718 // TODO: Move base cell render into trait impl so we don't have to repeat this
719 .children(self.cell_position_spacer(true, window, cx))
720 .child(
721 h_flex()
722 .w_full()
723 .pr_2()
724 .rounded_xs()
725 .items_start()
726 .gap(DynamicSpacing::Base08.rems(cx))
727 .bg(self.selected_bg_color(window, cx))
728 .child(self.gutter(window, cx))
729 .child(
730 div()
731 .flex()
732 .size_full()
733 .flex_1()
734 .p_3()
735 .font_ui(cx)
736 .text_size(TextSize::Default.rems(cx))
737 .child(self.source.clone()),
738 ),
739 )
740 // TODO: Move base cell render into trait impl so we don't have to repeat this
741 .children(self.cell_position_spacer(false, window, cx))
742 }
743}