Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:06:19.229Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

cell.rs

1325 lines · 46.1 KB · rust
1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use editor::{Editor, EditorMode, MultiBuffer, SizingBehavior};
5use futures::future::Shared;
6use gpui::{
7    App, Entity, EventEmitter, Focusable, Hsla, InteractiveElement, RetainAllImageCache,
8    StatefulInteractiveElement, Task, prelude::*,
9};
10use language::{Buffer, Language, LanguageRegistry};
11use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
12use nbformat::v4::{CellId, CellMetadata, CellType};
13use runtimelib::{JupyterMessage, JupyterMessageContent};
14use settings::Settings as _;
15use ui::{CommonAnimationExt, IconButtonShape, prelude::*};
16use util::ResultExt;
17use zed_actions::notebook::InterruptKernel;
18
19use crate::{
20    notebook::{CODE_BLOCK_INSET, GUTTER_WIDTH},
21    outputs::{Output, plain, plain::TerminalOutput, user_error::ErrorView},
22    repl_settings::ReplSettings,
23};
24
25#[derive(Copy, Clone, PartialEq, PartialOrd)]
26pub enum CellPosition {
27    First,
28    Middle,
29    Last,
30}
31
32pub enum CellControlType {
33    RunCell,
34    RerunCell,
35    StopCell,
36    ClearCell,
37    CellOptions,
38    CollapseCell,
39    ExpandCell,
40}
41
42pub enum CellEvent {
43    Run(CellId),
44    FocusedIn(CellId),
45}
46
47pub enum MarkdownCellEvent {
48    FinishedEditing,
49    Run(CellId),
50}
51
52impl CellControlType {
53    fn icon_name(&self) -> IconName {
54        match self {
55            CellControlType::RunCell => IconName::PlayFilled,
56            CellControlType::RerunCell => IconName::ArrowCircle,
57            CellControlType::StopCell => IconName::Stop,
58            CellControlType::ClearCell => IconName::ListX,
59            CellControlType::CellOptions => IconName::Ellipsis,
60            CellControlType::CollapseCell => IconName::ChevronDown,
61            CellControlType::ExpandCell => IconName::ChevronRight,
62        }
63    }
64    fn id(&self) -> &'static str {
65        match self {
66            CellControlType::RunCell => "CellControlType::RunCell",
67            CellControlType::RerunCell => "CellControlType::RerunCell",
68            CellControlType::StopCell => "CellControlType::StopCell",
69            CellControlType::ClearCell => "CellControlType::ClearCell",
70            CellControlType::CellOptions => "CellControlType::CellOptions",
71            CellControlType::CollapseCell => "CellControlType::CollapseCelln",
72            CellControlType::ExpandCell => "CellControlType::ExpandCell",
73        }
74    }
75}
76
77pub struct CellControl {
78    button: IconButton,
79}
80
81impl CellControl {
82    fn new(id: impl Into<SharedString>, control_type: CellControlType) -> Self {
83        let icon_name = control_type.icon_name();
84        let id = id.into();
85        let button = IconButton::new(id, icon_name)
86            .icon_size(IconSize::Small)
87            .shape(IconButtonShape::Square);
88        Self { button }
89    }
90}
91
92impl Clickable for CellControl {
93    fn on_click(
94        self,
95        handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
96    ) -> Self {
97        let button = self.button.on_click(handler);
98        Self { button }
99    }
100
101    fn cursor_style(self, _cursor_style: gpui::CursorStyle) -> Self {
102        self
103    }
104}
105
106/// A notebook cell
107#[derive(Clone)]
108pub enum Cell {
109    Code(Entity<CodeCell>),
110    Markdown(Entity<MarkdownCell>),
111    Raw(Entity<RawCell>),
112}
113
114pub(crate) enum MovementDirection {
115    Start,
116    End,
117}
118
119fn convert_outputs(
120    outputs: &Vec<nbformat::v4::Output>,
121    window: &mut Window,
122    cx: &mut App,
123) -> Vec<Output> {
124    outputs
125        .iter()
126        .map(|output| match output {
127            nbformat::v4::Output::Stream { text, .. } => Output::Stream {
128                content: cx.new(|cx| TerminalOutput::from(&text.0, window, cx)),
129            },
130            nbformat::v4::Output::DisplayData(display_data) => {
131                Output::new(&display_data.data, None, window, cx)
132            }
133            nbformat::v4::Output::ExecuteResult(execute_result) => {
134                Output::new(&execute_result.data, None, window, cx)
135            }
136            nbformat::v4::Output::Error(error) => Output::ErrorOutput(ErrorView {
137                ename: error.ename.clone(),
138                evalue: error.evalue.clone(),
139                traceback: cx
140                    .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
141            }),
142        })
143        .collect()
144}
145
146impl Cell {
147    pub fn id(&self, cx: &App) -> CellId {
148        match self {
149            Cell::Code(code_cell) => code_cell.read(cx).id().clone(),
150            Cell::Markdown(markdown_cell) => markdown_cell.read(cx).id().clone(),
151            Cell::Raw(raw_cell) => raw_cell.read(cx).id().clone(),
152        }
153    }
154
155    pub fn current_source(&self, cx: &App) -> String {
156        match self {
157            Cell::Code(code_cell) => code_cell.read(cx).current_source(cx),
158            Cell::Markdown(markdown_cell) => markdown_cell.read(cx).current_source(cx),
159            Cell::Raw(raw_cell) => raw_cell.read(cx).source.clone(),
160        }
161    }
162
163    pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
164        match self {
165            Cell::Code(code_cell) => code_cell.read(cx).to_nbformat_cell(cx),
166            Cell::Markdown(markdown_cell) => markdown_cell.read(cx).to_nbformat_cell(cx),
167            Cell::Raw(raw_cell) => raw_cell.read(cx).to_nbformat_cell(),
168        }
169    }
170
171    pub fn is_dirty(&self, cx: &App) -> bool {
172        match self {
173            Cell::Code(code_cell) => code_cell.read(cx).is_dirty(cx),
174            Cell::Markdown(markdown_cell) => markdown_cell.read(cx).is_dirty(cx),
175            Cell::Raw(_) => false,
176        }
177    }
178
179    pub fn load(
180        cell: &nbformat::v4::Cell,
181        languages: &Arc<LanguageRegistry>,
182        notebook_language: Shared<Task<Option<Arc<Language>>>>,
183        window: &mut Window,
184        cx: &mut App,
185    ) -> Self {
186        match cell {
187            nbformat::v4::Cell::Markdown {
188                id,
189                metadata,
190                source,
191                ..
192            } => {
193                let source = source.concat();
194
195                let entity = cx.new(|cx| {
196                    MarkdownCell::new(
197                        id.clone(),
198                        metadata.clone(),
199                        source,
200                        languages.clone(),
201                        window,
202                        cx,
203                    )
204                });
205
206                Cell::Markdown(entity)
207            }
208            nbformat::v4::Cell::Code {
209                id,
210                metadata,
211                execution_count,
212                source,
213                outputs,
214            } => {
215                let text = source.concat();
216                let outputs = convert_outputs(outputs, window, cx);
217
218                Cell::Code(cx.new(|cx| {
219                    CodeCell::new(
220                        CellSource::Existing {
221                            execution_count: *execution_count,
222                            outputs,
223                        },
224                        id.clone(),
225                        metadata.clone(),
226                        text,
227                        notebook_language,
228                        window,
229                        cx,
230                    )
231                }))
232            }
233            nbformat::v4::Cell::Raw {
234                id,
235                metadata,
236                source,
237            } => Cell::Raw(cx.new(|_| RawCell {
238                id: id.clone(),
239                metadata: metadata.clone(),
240                source: source.concat(),
241                selected: false,
242                cell_position: None,
243            })),
244        }
245    }
246
247    pub(crate) fn move_to(&self, direction: MovementDirection, window: &mut Window, cx: &mut App) {
248        fn move_in_editor(
249            editor: &Entity<Editor>,
250            direction: MovementDirection,
251            window: &mut Window,
252            cx: &mut App,
253        ) {
254            editor.update(cx, |editor, cx| {
255                match direction {
256                    MovementDirection::Start => {
257                        editor.move_to_beginning(&Default::default(), window, cx);
258                    }
259                    MovementDirection::End => {
260                        editor.move_to_end(&Default::default(), window, cx);
261                    }
262                }
263                editor.focus_handle(cx).focus(window, cx);
264            })
265        }
266
267        match self {
268            Cell::Code(cell) => {
269                cell.update(cx, |cell, cx| {
270                    move_in_editor(&cell.editor, direction, window, cx)
271                });
272            }
273            Cell::Markdown(cell) => {
274                cell.update(cx, |cell, cx| {
275                    cell.set_editing(true);
276                    move_in_editor(&cell.editor, direction, window, cx);
277
278                    cx.notify();
279                });
280            }
281            _ => {}
282        }
283    }
284
285    pub(crate) fn editor<'a>(&'a self, cx: &'a App) -> Option<&'a Entity<Editor>> {
286        match self {
287            Cell::Code(cell) => Some(cell.read(cx).editor()),
288            Cell::Markdown(cell) => Some(cell.read(cx).editor()),
289            _ => None,
290        }
291    }
292}
293
294pub trait RenderableCell: Render {
295    const CELL_TYPE: CellType;
296
297    fn id(&self) -> &CellId;
298    fn cell_type(&self) -> CellType;
299    fn metadata(&self) -> &CellMetadata;
300    fn source(&self) -> &String;
301    fn selected(&self) -> bool;
302    fn set_selected(&mut self, selected: bool) -> &mut Self;
303    fn selected_bg_color(&self, _window: &mut Window, cx: &mut Context<Self>) -> Hsla {
304        if self.selected() {
305            let mut color = cx.theme().colors().element_hover;
306            color.fade_out(0.5);
307            color
308        } else {
309            // Not sure if this is correct, previous was TODO: this is wrong
310            gpui::transparent_black()
311        }
312    }
313    fn control(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<CellControl> {
314        None
315    }
316
317    fn cell_position_spacer(
318        &self,
319        is_first: bool,
320        _window: &mut Window,
321        cx: &mut Context<Self>,
322    ) -> Option<impl IntoElement> {
323        let cell_position = self.cell_position();
324
325        if (cell_position == Some(&CellPosition::First) && is_first)
326            || (cell_position == Some(&CellPosition::Last) && !is_first)
327        {
328            Some(div().flex().w_full().h(DynamicSpacing::Base12.px(cx)))
329        } else {
330            None
331        }
332    }
333
334    fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
335        let is_selected = self.selected();
336
337        div()
338            .relative()
339            .h_full()
340            .w(px(GUTTER_WIDTH))
341            .child(
342                div()
343                    .w(px(GUTTER_WIDTH))
344                    .flex()
345                    .flex_none()
346                    .justify_center()
347                    .h_full()
348                    .child(
349                        div()
350                            .flex_none()
351                            .w(px(1.))
352                            .h_full()
353                            .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
354                            .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
355                    ),
356            )
357            .when_some(self.control(window, cx), |this, control| {
358                this.child(
359                    div()
360                        .absolute()
361                        .top(px(CODE_BLOCK_INSET - 2.0))
362                        .left_0()
363                        .flex()
364                        .flex_none()
365                        .w(px(GUTTER_WIDTH))
366                        .h(px(GUTTER_WIDTH + 12.0))
367                        .items_center()
368                        .justify_center()
369                        .bg(cx.theme().colors().tab_bar_background)
370                        .child(control.button),
371                )
372            })
373    }
374
375    fn cell_position(&self) -> Option<&CellPosition>;
376    fn set_cell_position(&mut self, position: CellPosition) -> &mut Self;
377}
378
379pub trait RunnableCell: RenderableCell {
380    fn execution_count(&self) -> Option<i32>;
381    fn set_execution_count(&mut self, count: i32) -> &mut Self;
382    fn run(&mut self, window: &mut Window, cx: &mut Context<Self>) -> ();
383}
384
385pub struct MarkdownCell {
386    id: CellId,
387    metadata: CellMetadata,
388    image_cache: Entity<RetainAllImageCache>,
389    source: String,
390    editor: Entity<Editor>,
391    markdown: Entity<Markdown>,
392    editing: bool,
393    selected: bool,
394    cell_position: Option<CellPosition>,
395    _editor_subscription: gpui::Subscription,
396}
397
398impl EventEmitter<MarkdownCellEvent> for MarkdownCell {}
399
400impl MarkdownCell {
401    pub fn new(
402        id: CellId,
403        metadata: CellMetadata,
404        source: String,
405        languages: Arc<LanguageRegistry>,
406        window: &mut Window,
407        cx: &mut Context<Self>,
408    ) -> Self {
409        let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
410        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
411
412        let markdown_language = languages.language_for_name("Markdown");
413        cx.spawn_in(window, async move |_this, cx| {
414            if let Some(markdown) = markdown_language.await.log_err() {
415                buffer.update(cx, |buffer, cx| {
416                    buffer.set_language(Some(markdown), cx);
417                });
418            }
419        })
420        .detach();
421
422        let editor = cx.new(|cx| {
423            let mut editor = Editor::new(
424                EditorMode::Full {
425                    scale_ui_elements_with_buffer_font_size: false,
426                    show_active_line_background: false,
427                    sizing_behavior: SizingBehavior::SizeByContent,
428                },
429                multi_buffer,
430                None,
431                window,
432                cx,
433            );
434
435            editor.set_show_gutter(false, cx);
436            editor.set_use_modal_editing(true);
437            editor.disable_mouse_wheel_zoom();
438            editor.disable_scrollbars_and_minimap(window, cx);
439            editor
440        });
441
442        let markdown = cx.new(|cx| Markdown::new(source.clone().into(), None, None, cx));
443
444        let editor_subscription =
445            cx.subscribe(&editor, move |this, _editor, event, cx| match event {
446                editor::EditorEvent::Blurred => {
447                    if this.editing {
448                        this.editing = false;
449                        cx.emit(MarkdownCellEvent::FinishedEditing);
450                        cx.notify();
451                    }
452                }
453                _ => {}
454            });
455
456        let start_editing = source.is_empty();
457        Self {
458            id,
459            metadata,
460            image_cache: RetainAllImageCache::new(cx),
461            source,
462            editor,
463            markdown,
464            editing: start_editing,
465            selected: false,
466            cell_position: None,
467            _editor_subscription: editor_subscription,
468        }
469    }
470
471    pub fn editor(&self) -> &Entity<Editor> {
472        &self.editor
473    }
474
475    pub fn current_source(&self, cx: &App) -> String {
476        let editor = self.editor.read(cx);
477        let buffer = editor.buffer().read(cx);
478        buffer
479            .as_singleton()
480            .map(|b| b.read(cx).text())
481            .unwrap_or_default()
482    }
483
484    pub fn is_dirty(&self, cx: &App) -> bool {
485        self.editor.read(cx).buffer().read(cx).is_dirty(cx)
486    }
487
488    pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
489        let source = self.current_source(cx);
490        let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
491
492        nbformat::v4::Cell::Markdown {
493            id: self.id.clone(),
494            metadata: self.metadata.clone(),
495            source: source_lines,
496            attachments: None,
497        }
498    }
499
500    pub fn is_editing(&self) -> bool {
501        self.editing
502    }
503
504    pub fn set_editing(&mut self, editing: bool) {
505        self.editing = editing;
506    }
507
508    pub fn reparse_markdown(&mut self, cx: &mut Context<Self>) {
509        let editor = self.editor.read(cx);
510        let buffer = editor.buffer().read(cx);
511        let source = buffer
512            .as_singleton()
513            .map(|b| b.read(cx).text())
514            .unwrap_or_default();
515
516        self.source = source.clone();
517        self.markdown.update(cx, |markdown, cx| {
518            markdown.reset(source.into(), cx);
519        });
520    }
521
522    /// Called when user presses Shift+Enter or Ctrl+Enter while editing.
523    /// Finishes editing and signals to move to the next cell.
524    pub fn run(&mut self, cx: &mut Context<Self>) {
525        if self.editing {
526            self.editing = false;
527            cx.emit(MarkdownCellEvent::FinishedEditing);
528            cx.emit(MarkdownCellEvent::Run(self.id.clone()));
529            cx.notify();
530        }
531    }
532}
533
534impl RenderableCell for MarkdownCell {
535    const CELL_TYPE: CellType = CellType::Markdown;
536
537    fn id(&self) -> &CellId {
538        &self.id
539    }
540
541    fn cell_type(&self) -> CellType {
542        CellType::Markdown
543    }
544
545    fn metadata(&self) -> &CellMetadata {
546        &self.metadata
547    }
548
549    fn source(&self) -> &String {
550        &self.source
551    }
552
553    fn selected(&self) -> bool {
554        self.selected
555    }
556
557    fn set_selected(&mut self, selected: bool) -> &mut Self {
558        self.selected = selected;
559        self
560    }
561
562    fn control(&self, _window: &mut Window, _: &mut Context<Self>) -> Option<CellControl> {
563        None
564    }
565
566    fn cell_position(&self) -> Option<&CellPosition> {
567        self.cell_position.as_ref()
568    }
569
570    fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
571        self.cell_position = Some(cell_position);
572        self
573    }
574}
575
576impl Render for MarkdownCell {
577    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
578        // If editing, show the editor
579        if self.editing {
580            return v_flex()
581                .size_full()
582                .children(self.cell_position_spacer(true, window, cx))
583                .child(
584                    h_flex()
585                        .w_full()
586                        .pr_6()
587                        .rounded_xs()
588                        .items_start()
589                        .gap(DynamicSpacing::Base08.rems(cx))
590                        .bg(self.selected_bg_color(window, cx))
591                        .child(self.gutter(window, cx))
592                        .child(
593                            div()
594                                .flex_1()
595                                .p_3()
596                                .bg(cx.theme().colors().editor_background)
597                                .rounded_sm()
598                                .child(self.editor.clone())
599                                .on_mouse_down(
600                                    gpui::MouseButton::Left,
601                                    cx.listener(|_this, _event, _window, _cx| {
602                                        // Prevent the click from propagating
603                                    }),
604                                ),
605                        ),
606                )
607                .children(self.cell_position_spacer(false, window, cx));
608        }
609
610        // Preview mode - show rendered markdown
611
612        let style = MarkdownStyle::themed(MarkdownFont::Preview, window, cx);
613
614        v_flex()
615            .size_full()
616            .children(self.cell_position_spacer(true, window, cx))
617            .child(
618                h_flex()
619                    .w_full()
620                    .pr_6()
621                    .rounded_xs()
622                    .items_start()
623                    .gap(DynamicSpacing::Base08.rems(cx))
624                    .bg(self.selected_bg_color(window, cx))
625                    .child(self.gutter(window, cx))
626                    .child(
627                        v_flex()
628                            .image_cache(self.image_cache.clone())
629                            .id("markdown-content")
630                            .size_full()
631                            .flex_1()
632                            .p_3()
633                            .font_ui(cx)
634                            .text_size(TextSize::Default.rems(cx))
635                            .cursor_pointer()
636                            .on_click(cx.listener(|this, _event, window, cx| {
637                                this.editing = true;
638                                window.focus(&this.editor.focus_handle(cx), cx);
639                                cx.notify();
640                            }))
641                            .child(MarkdownElement::new(self.markdown.clone(), style)),
642                    ),
643            )
644            .children(self.cell_position_spacer(false, window, cx))
645    }
646}
647
648pub struct CodeCell {
649    id: CellId,
650    metadata: CellMetadata,
651    execution_count: Option<i32>,
652    source: String,
653    editor: Entity<editor::Editor>,
654    outputs: Vec<Output>,
655    selected: bool,
656    cell_position: Option<CellPosition>,
657    _language_task: Task<()>,
658    execution_start_time: Option<Instant>,
659    execution_duration: Option<Duration>,
660    is_executing: bool,
661}
662
663impl EventEmitter<CellEvent> for CodeCell {}
664
665pub(super) enum CellSource {
666    /// Crate a new empty cell
667    None,
668    /// Backed by an existing notebook cell
669    Existing {
670        execution_count: Option<i32>,
671        outputs: Vec<Output>,
672    },
673}
674
675impl CellSource {
676    fn into_outputs(self) -> (Option<i32>, Vec<Output>) {
677        match self {
678            CellSource::Existing {
679                execution_count,
680                outputs,
681            } => (execution_count, outputs),
682            CellSource::None => Default::default(),
683        }
684    }
685}
686
687impl CodeCell {
688    pub(super) fn new(
689        cell_source: CellSource,
690        id: CellId,
691        metadata: CellMetadata,
692        source: String,
693        notebook_language: Shared<Task<Option<Arc<Language>>>>,
694        window: &mut Window,
695        cx: &mut Context<Self>,
696    ) -> Self {
697        let buffer = cx.new(|cx| Buffer::local(source.clone(), cx));
698        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
699
700        let editor = cx.new(|cx| {
701            let mut editor = Editor::new(
702                EditorMode::Full {
703                    scale_ui_elements_with_buffer_font_size: false,
704                    show_active_line_background: false,
705                    sizing_behavior: SizingBehavior::SizeByContent,
706                },
707                multi_buffer,
708                None,
709                window,
710                cx,
711            );
712
713            editor.disable_mouse_wheel_zoom();
714            editor.disable_scrollbars_and_minimap(window, cx);
715            editor.set_text(source.clone(), window, cx);
716            editor.set_show_gutter(false, cx);
717            editor.set_use_modal_editing(true);
718            editor
719        });
720
721        let language_task = cx.spawn_in(window, async move |_this, cx| {
722            let language = notebook_language.await;
723            buffer.update(cx, |buffer, cx| {
724                buffer.set_language(language.clone(), cx);
725            });
726        });
727
728        let (execution_count, outputs) = cell_source.into_outputs();
729
730        Self {
731            id,
732            metadata,
733            execution_count,
734            source,
735            editor,
736            outputs,
737            selected: false,
738            cell_position: None,
739            execution_start_time: None,
740            execution_duration: None,
741            is_executing: false,
742            _language_task: language_task,
743        }
744    }
745
746    pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut Context<Self>) {
747        self.editor.update(cx, |editor, cx| {
748            editor.buffer().update(cx, |buffer, cx| {
749                if let Some(buffer) = buffer.as_singleton() {
750                    buffer.update(cx, |buffer, cx| {
751                        buffer.set_language(language, cx);
752                    });
753                }
754            });
755        });
756    }
757
758    pub fn editor(&self) -> &Entity<editor::Editor> {
759        &self.editor
760    }
761
762    pub fn current_source(&self, cx: &App) -> String {
763        let editor = self.editor.read(cx);
764        let buffer = editor.buffer().read(cx);
765        buffer
766            .as_singleton()
767            .map(|b| b.read(cx).text())
768            .unwrap_or_default()
769    }
770
771    pub fn is_dirty(&self, cx: &App) -> bool {
772        self.editor.read(cx).buffer().read(cx).is_dirty(cx)
773    }
774
775    pub fn to_nbformat_cell(&self, cx: &App) -> nbformat::v4::Cell {
776        let source = self.current_source(cx);
777        let source_lines: Vec<String> = source.lines().map(|l| format!("{}\n", l)).collect();
778
779        let outputs = self.outputs_to_nbformat(cx);
780
781        nbformat::v4::Cell::Code {
782            id: self.id.clone(),
783            metadata: self.metadata.clone(),
784            execution_count: self.execution_count,
785            source: source_lines,
786            outputs,
787        }
788    }
789
790    fn outputs_to_nbformat(&self, cx: &App) -> Vec<nbformat::v4::Output> {
791        self.outputs
792            .iter()
793            .filter_map(|output| output.to_nbformat(cx))
794            .collect()
795    }
796
797    pub fn has_outputs(&self) -> bool {
798        !self.outputs.is_empty()
799    }
800
801    pub fn clear_outputs(&mut self) {
802        self.outputs.clear();
803        self.execution_duration = None;
804    }
805
806    pub fn start_execution(&mut self) {
807        self.execution_start_time = Some(Instant::now());
808        self.execution_duration = None;
809        self.is_executing = true;
810    }
811
812    pub fn finish_execution(&mut self) {
813        if let Some(start_time) = self.execution_start_time.take() {
814            self.execution_duration = Some(start_time.elapsed());
815        }
816        self.is_executing = false;
817    }
818
819    pub fn is_executing(&self) -> bool {
820        self.is_executing
821    }
822
823    /// Displays a kernel-level failure (e.g. the kernel failed to launch because
824    /// Python is not installed) as an error output on this cell, so the user gets
825    /// feedback instead of a spinner that never resolves.
826    pub fn show_kernel_error(
827        &mut self,
828        error_message: &str,
829        window: &mut Window,
830        cx: &mut Context<Self>,
831    ) {
832        self.outputs.push(Output::ErrorOutput(ErrorView {
833            ename: "Kernel Error".to_string(),
834            evalue: "cell could not be executed".to_string(),
835            traceback: cx.new(|cx| TerminalOutput::from(error_message, window, cx)),
836        }));
837        self.execution_start_time = None;
838        self.is_executing = false;
839        cx.notify();
840    }
841
842    pub fn execution_duration(&self) -> Option<Duration> {
843        self.execution_duration
844    }
845
846    fn format_duration(duration: Duration) -> String {
847        let total_secs = duration.as_secs_f64();
848        if total_secs < 1.0 {
849            format!("{:.0}ms", duration.as_millis())
850        } else if total_secs < 60.0 {
851            format!("{:.1}s", total_secs)
852        } else {
853            let minutes = (total_secs / 60.0).floor() as u64;
854            let secs = total_secs % 60.0;
855            format!("{}m {:.1}s", minutes, secs)
856        }
857    }
858
859    pub fn handle_message(
860        &mut self,
861        message: &JupyterMessage,
862        window: &mut Window,
863        cx: &mut Context<Self>,
864    ) {
865        match &message.content {
866            JupyterMessageContent::StreamContent(stream) => {
867                self.outputs.push(Output::Stream {
868                    content: cx.new(|cx| TerminalOutput::from(&stream.text, window, cx)),
869                });
870            }
871            JupyterMessageContent::DisplayData(display_data) => {
872                self.outputs
873                    .push(Output::new(&display_data.data, None, window, cx));
874            }
875            JupyterMessageContent::ExecuteResult(execute_result) => {
876                self.outputs
877                    .push(Output::new(&execute_result.data, None, window, cx));
878            }
879            JupyterMessageContent::ExecuteInput(input) => {
880                self.execution_count = serde_json::to_value(&input.execution_count)
881                    .ok()
882                    .and_then(|v| v.as_i64())
883                    .map(|v| v as i32);
884            }
885            JupyterMessageContent::ExecuteReply(_) => {
886                self.finish_execution();
887            }
888            JupyterMessageContent::ErrorOutput(error) => {
889                self.outputs.push(Output::ErrorOutput(ErrorView {
890                    ename: error.ename.clone(),
891                    evalue: error.evalue.clone(),
892                    traceback: cx
893                        .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)),
894                }));
895            }
896            _ => {}
897        }
898        cx.notify();
899    }
900
901    pub fn gutter_output(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
902        let is_selected = self.selected();
903
904        div()
905            .relative()
906            .h_full()
907            .w(px(GUTTER_WIDTH))
908            .child(
909                div()
910                    .w(px(GUTTER_WIDTH))
911                    .flex()
912                    .flex_none()
913                    .justify_center()
914                    .h_full()
915                    .child(
916                        div()
917                            .flex_none()
918                            .w(px(1.))
919                            .h_full()
920                            .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
921                            .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
922                    ),
923            )
924            .when(self.has_outputs(), |this| {
925                this.child(
926                    div()
927                        .absolute()
928                        .top(px(CODE_BLOCK_INSET - 2.0))
929                        .left_0()
930                        .flex()
931                        .flex_none()
932                        .w(px(GUTTER_WIDTH))
933                        .h(px(GUTTER_WIDTH + 12.0))
934                        .items_center()
935                        .justify_center()
936                        .bg(cx.theme().colors().tab_bar_background)
937                        .child(IconButton::new("control", IconName::Ellipsis)),
938                )
939            })
940    }
941}
942
943impl RenderableCell for CodeCell {
944    const CELL_TYPE: CellType = CellType::Code;
945
946    fn id(&self) -> &CellId {
947        &self.id
948    }
949
950    fn cell_type(&self) -> CellType {
951        CellType::Code
952    }
953
954    fn metadata(&self) -> &CellMetadata {
955        &self.metadata
956    }
957
958    fn source(&self) -> &String {
959        &self.source
960    }
961
962    fn control(&self, _window: &mut Window, cx: &mut Context<Self>) -> Option<CellControl> {
963        let control_type = if self.is_executing {
964            CellControlType::StopCell
965        } else if self.has_outputs() {
966            CellControlType::RerunCell
967        } else {
968            CellControlType::RunCell
969        };
970
971        Some(
972            CellControl::new(control_type.id(), control_type).on_click(cx.listener(
973                move |this, _, window, cx| {
974                    if this.is_executing {
975                        window.dispatch_action(Box::new(InterruptKernel), cx);
976                    } else {
977                        this.run(window, cx);
978                    }
979                },
980            )),
981        )
982    }
983
984    fn selected(&self) -> bool {
985        self.selected
986    }
987
988    fn set_selected(&mut self, selected: bool) -> &mut Self {
989        self.selected = selected;
990        self
991    }
992
993    fn cell_position(&self) -> Option<&CellPosition> {
994        self.cell_position.as_ref()
995    }
996
997    fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
998        self.cell_position = Some(cell_position);
999        self
1000    }
1001
1002    fn gutter(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1003        let is_selected = self.selected();
1004        let execution_count = self.execution_count;
1005
1006        div()
1007            .relative()
1008            .h_full()
1009            .w(px(GUTTER_WIDTH))
1010            .child(
1011                div()
1012                    .w(px(GUTTER_WIDTH))
1013                    .flex()
1014                    .flex_none()
1015                    .justify_center()
1016                    .h_full()
1017                    .child(
1018                        div()
1019                            .flex_none()
1020                            .w(px(1.))
1021                            .h_full()
1022                            .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent))
1023                            .when(!is_selected, |this| this.bg(cx.theme().colors().border)),
1024                    ),
1025            )
1026            .when_some(self.control(window, cx), |this, control| {
1027                this.child(
1028                    div()
1029                        .absolute()
1030                        .top(px(CODE_BLOCK_INSET - 2.0))
1031                        .left_0()
1032                        .flex()
1033                        .flex_col()
1034                        .w(px(GUTTER_WIDTH))
1035                        .items_center()
1036                        .justify_center()
1037                        .bg(cx.theme().colors().tab_bar_background)
1038                        .child(control.button)
1039                        .when_some(execution_count, |this, count| {
1040                            this.child(
1041                                div()
1042                                    .mt_1()
1043                                    .text_xs()
1044                                    .text_color(cx.theme().colors().text_muted)
1045                                    .child(format!("{}", count)),
1046                            )
1047                        }),
1048                )
1049            })
1050    }
1051}
1052
1053impl RunnableCell for CodeCell {
1054    fn run(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1055        cx.emit(CellEvent::Run(self.id.clone()));
1056    }
1057
1058    fn execution_count(&self) -> Option<i32> {
1059        self.execution_count
1060            .and_then(|count| if count > 0 { Some(count) } else { None })
1061    }
1062
1063    fn set_execution_count(&mut self, count: i32) -> &mut Self {
1064        self.execution_count = Some(count);
1065        self
1066    }
1067}
1068
1069impl Render for CodeCell {
1070    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1071        let output_max_height = ReplSettings::get_global(cx).output_max_height_lines;
1072        let output_max_height = if output_max_height > 0 {
1073            Some(window.line_height() * output_max_height as f32)
1074        } else {
1075            None
1076        };
1077        let output_max_width =
1078            plain::max_width_for_columns(ReplSettings::get_global(cx).max_columns, window, cx);
1079        // get the language from the editor's buffer
1080        let language_name = self
1081            .editor
1082            .read(cx)
1083            .buffer()
1084            .read(cx)
1085            .as_singleton()
1086            .and_then(|buffer| buffer.read(cx).language())
1087            .map(|lang| lang.name().to_string());
1088
1089        v_flex()
1090            .size_full()
1091            // TODO: Move base cell render into trait impl so we don't have to repeat this
1092            .children(self.cell_position_spacer(true, window, cx))
1093            // Editor portion
1094            .child(
1095                h_flex()
1096                    .w_full()
1097                    .pr_6()
1098                    .rounded_xs()
1099                    .items_start()
1100                    .gap(DynamicSpacing::Base08.rems(cx))
1101                    .bg(self.selected_bg_color(window, cx))
1102                    .child(self.gutter(window, cx))
1103                    .child(
1104                        div().py_1p5().w_full().child(
1105                            div()
1106                                .relative()
1107                                .flex()
1108                                .size_full()
1109                                .flex_1()
1110                                .py_3()
1111                                .px_5()
1112                                .rounded_lg()
1113                                .border_1()
1114                                .border_color(cx.theme().colors().border)
1115                                .bg(cx.theme().colors().editor_background)
1116                                .child(div().w_full().child(self.editor.clone()))
1117                                // lang badge in top-right corner
1118                                .when_some(language_name, |this, name| {
1119                                    this.child(
1120                                        div()
1121                                            .absolute()
1122                                            .top_1()
1123                                            .right_2()
1124                                            .px_2()
1125                                            .py_0p5()
1126                                            .rounded_md()
1127                                            .bg(cx.theme().colors().element_background.opacity(0.7))
1128                                            .text_xs()
1129                                            .text_color(cx.theme().colors().text_muted)
1130                                            .child(name),
1131                                    )
1132                                }),
1133                        ),
1134                    ),
1135            )
1136            .when(
1137                self.has_outputs() || self.execution_duration.is_some() || self.is_executing,
1138                |this| {
1139                    let execution_time_label = self.execution_duration.map(Self::format_duration);
1140                    let is_executing = self.is_executing;
1141                    this.child(
1142                        h_flex()
1143                            .w_full()
1144                            .pr_6()
1145                            .rounded_xs()
1146                            .items_start()
1147                            .gap(DynamicSpacing::Base08.rems(cx))
1148                            .bg(self.selected_bg_color(window, cx))
1149                            .child(self.gutter_output(window, cx))
1150                            .child(
1151                                div().py_1p5().w_full().child(
1152                                    v_flex()
1153                                        .size_full()
1154                                        .flex_1()
1155                                        .py_3()
1156                                        .px_5()
1157                                        .rounded_lg()
1158                                        .border_1()
1159                                        // execution status/time at the TOP
1160                                        .when(
1161                                            is_executing || execution_time_label.is_some(),
1162                                            |this| {
1163                                                let time_element = if is_executing {
1164                                                    h_flex()
1165                                                        .gap_1()
1166                                                        .items_center()
1167                                                        .child(
1168                                                            Icon::new(IconName::ArrowCircle)
1169                                                                .size(IconSize::XSmall)
1170                                                                .color(Color::Warning)
1171                                                                .with_rotate_animation(2)
1172                                                                .into_any_element(),
1173                                                        )
1174                                                        .child(
1175                                                            div()
1176                                                                .text_xs()
1177                                                                .text_color(
1178                                                                    cx.theme().colors().text_muted,
1179                                                                )
1180                                                                .child("Running..."),
1181                                                        )
1182                                                        .into_any_element()
1183                                                } else if let Some(duration_text) =
1184                                                    execution_time_label.clone()
1185                                                {
1186                                                    h_flex()
1187                                                        .gap_1()
1188                                                        .items_center()
1189                                                        .child(
1190                                                            Icon::new(IconName::Check)
1191                                                                .size(IconSize::XSmall)
1192                                                                .color(Color::Success),
1193                                                        )
1194                                                        .child(
1195                                                            div()
1196                                                                .text_xs()
1197                                                                .text_color(
1198                                                                    cx.theme().colors().text_muted,
1199                                                                )
1200                                                                .child(duration_text),
1201                                                        )
1202                                                        .into_any_element()
1203                                                } else {
1204                                                    div().into_any_element()
1205                                                };
1206                                                this.child(div().mb_2().child(time_element))
1207                                            },
1208                                        )
1209                                        // output at bottom
1210                                        .child(
1211                                            div()
1212                                                .id((
1213                                                    ElementId::from(self.id.to_string()),
1214                                                    "output-scroll",
1215                                                ))
1216                                                .w_full()
1217                                                .when_some(output_max_width, |div, max_width| {
1218                                                    div.max_w(max_width).overflow_x_scroll()
1219                                                })
1220                                                .when_some(output_max_height, |div, max_height| {
1221                                                    div.max_h(max_height).overflow_y_scroll()
1222                                                })
1223                                                .children(self.outputs.iter().map(|output| {
1224                                                    div().children(output.content(window, cx))
1225                                                })),
1226                                        ),
1227                                ),
1228                            ),
1229                    )
1230                },
1231            )
1232            // TODO: Move base cell render into trait impl so we don't have to repeat this
1233            .children(self.cell_position_spacer(false, window, cx))
1234    }
1235}
1236
1237pub struct RawCell {
1238    id: CellId,
1239    metadata: CellMetadata,
1240    source: String,
1241    selected: bool,
1242    cell_position: Option<CellPosition>,
1243}
1244
1245impl RawCell {
1246    pub fn to_nbformat_cell(&self) -> nbformat::v4::Cell {
1247        let source_lines: Vec<String> = self.source.lines().map(|l| format!("{}\n", l)).collect();
1248
1249        nbformat::v4::Cell::Raw {
1250            id: self.id.clone(),
1251            metadata: self.metadata.clone(),
1252            source: source_lines,
1253        }
1254    }
1255}
1256
1257impl RenderableCell for RawCell {
1258    const CELL_TYPE: CellType = CellType::Raw;
1259
1260    fn id(&self) -> &CellId {
1261        &self.id
1262    }
1263
1264    fn cell_type(&self) -> CellType {
1265        CellType::Raw
1266    }
1267
1268    fn metadata(&self) -> &CellMetadata {
1269        &self.metadata
1270    }
1271
1272    fn source(&self) -> &String {
1273        &self.source
1274    }
1275
1276    fn selected(&self) -> bool {
1277        self.selected
1278    }
1279
1280    fn set_selected(&mut self, selected: bool) -> &mut Self {
1281        self.selected = selected;
1282        self
1283    }
1284
1285    fn cell_position(&self) -> Option<&CellPosition> {
1286        self.cell_position.as_ref()
1287    }
1288
1289    fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self {
1290        self.cell_position = Some(cell_position);
1291        self
1292    }
1293}
1294
1295impl Render for RawCell {
1296    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1297        v_flex()
1298            .size_full()
1299            // TODO: Move base cell render into trait impl so we don't have to repeat this
1300            .children(self.cell_position_spacer(true, window, cx))
1301            .child(
1302                h_flex()
1303                    .w_full()
1304                    .pr_2()
1305                    .rounded_xs()
1306                    .items_start()
1307                    .gap(DynamicSpacing::Base08.rems(cx))
1308                    .bg(self.selected_bg_color(window, cx))
1309                    .child(self.gutter(window, cx))
1310                    .child(
1311                        div()
1312                            .flex()
1313                            .size_full()
1314                            .flex_1()
1315                            .p_3()
1316                            .font_ui(cx)
1317                            .text_size(TextSize::Default.rems(cx))
1318                            .child(self.source.clone()),
1319                    ),
1320            )
1321            // TODO: Move base cell render into trait impl so we don't have to repeat this
1322            .children(self.cell_position_spacer(false, window, cx))
1323    }
1324}
1325
Served at tenant.openagents/omega Member data and write actions are omitted.