cell.rs

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