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