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