cell.rs

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