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