cell.rs

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