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