cell.rs

  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}