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