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