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