Skip to repository content1325 lines · 46.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:06:57.136Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
cell.rs
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