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