session.rs

  1use crate::components::KernelListItem;
  2use crate::kernels::RemoteRunningKernel;
  3use crate::setup_editor_session_actions;
  4use crate::{
  5    KernelStatus,
  6    kernels::{Kernel, KernelSession, KernelSpecification, NativeRunningKernel},
  7    outputs::{
  8        ExecutionStatus, ExecutionView, ExecutionViewFinishedEmpty, ExecutionViewFinishedSmall,
  9    },
 10    repl_settings::ReplSettings,
 11};
 12use anyhow::Context as _;
 13use collections::{HashMap, HashSet};
 14use editor::SelectionEffects;
 15use editor::{
 16    Anchor, AnchorRangeExt as _, Editor, Inlay, MultiBuffer, ToOffset, ToPoint,
 17    display_map::{
 18        BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId,
 19        RenderBlock,
 20    },
 21    scroll::Autoscroll,
 22};
 23use project::InlayId;
 24
 25/// Marker types
 26enum ReplExecutedRange {}
 27
 28use futures::FutureExt as _;
 29use gpui::{
 30    Context, Entity, EventEmitter, Render, Subscription, Task, WeakEntity, Window, div, prelude::*,
 31};
 32use language::Point;
 33use project::Fs;
 34use runtimelib::{
 35    ExecuteRequest, ExecutionState, InterruptRequest, JupyterMessage, JupyterMessageContent,
 36    ShutdownRequest,
 37};
 38use settings::Settings as _;
 39use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration};
 40use theme::ActiveTheme;
 41use ui::{IconButtonShape, Tooltip, prelude::*};
 42use util::ResultExt as _;
 43
 44pub struct Session {
 45    fs: Arc<dyn Fs>,
 46    editor: WeakEntity<Editor>,
 47    pub kernel: Kernel,
 48    pub kernel_specification: KernelSpecification,
 49
 50    blocks: HashMap<String, EditorBlock>,
 51    result_inlays: HashMap<String, (InlayId, Range<Anchor>, usize)>,
 52    next_inlay_id: usize,
 53
 54    _subscriptions: Vec<Subscription>,
 55}
 56
 57struct EditorBlock {
 58    code_range: Range<Anchor>,
 59    invalidation_anchor: Anchor,
 60    block_id: CustomBlockId,
 61    execution_view: Entity<ExecutionView>,
 62}
 63
 64type CloseBlockFn =
 65    Arc<dyn for<'a> Fn(CustomBlockId, &'a mut Window, &mut App) + Send + Sync + 'static>;
 66
 67impl EditorBlock {
 68    fn new(
 69        editor: WeakEntity<Editor>,
 70        code_range: Range<Anchor>,
 71        status: ExecutionStatus,
 72        on_close: CloseBlockFn,
 73        cx: &mut Context<Session>,
 74    ) -> anyhow::Result<Self> {
 75        let editor = editor.upgrade().context("editor is not open")?;
 76        let workspace = editor.read(cx).workspace().context("workspace dropped")?;
 77
 78        let execution_view = cx.new(|cx| ExecutionView::new(status, workspace.downgrade(), cx));
 79
 80        let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| {
 81            let buffer = editor.buffer().clone();
 82            let buffer_snapshot = buffer.read(cx).snapshot(cx);
 83            let end_point = code_range.end.to_point(&buffer_snapshot);
 84            let next_row_start = end_point + Point::new(1, 0);
 85            if next_row_start > buffer_snapshot.max_point() {
 86                buffer.update(cx, |buffer, cx| {
 87                    buffer.edit(
 88                        [(
 89                            buffer_snapshot.max_point()..buffer_snapshot.max_point(),
 90                            "\n",
 91                        )],
 92                        None,
 93                        cx,
 94                    )
 95                });
 96            }
 97
 98            // Re-read snapshot after potential buffer edit and create a fresh anchor for
 99            // block placement. Using anchor_before (Bias::Left) ensures the anchor stays
100            // at the end of the code line regardless of whether we inserted a newline.
101            let buffer_snapshot = buffer.read(cx).snapshot(cx);
102            let block_placement_anchor = buffer_snapshot.anchor_before(end_point);
103            let invalidation_anchor = buffer_snapshot.anchor_before(next_row_start);
104            let block = BlockProperties {
105                placement: BlockPlacement::Below(block_placement_anchor),
106                // Take up at least one height for status, allow the editor to determine the real height based on the content from render
107                height: Some(1),
108                style: BlockStyle::Sticky,
109                render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
110                priority: 0,
111            };
112
113            let block_id = editor.insert_blocks([block], None, cx)[0];
114            (block_id, invalidation_anchor)
115        });
116
117        anyhow::Ok(Self {
118            code_range,
119            invalidation_anchor,
120            block_id,
121            execution_view,
122        })
123    }
124
125    fn handle_message(
126        &mut self,
127        message: &JupyterMessage,
128        window: &mut Window,
129        cx: &mut Context<Session>,
130    ) {
131        self.execution_view.update(cx, |execution_view, cx| {
132            execution_view.push_message(&message.content, window, cx);
133        });
134    }
135
136    fn create_output_area_renderer(
137        execution_view: Entity<ExecutionView>,
138        on_close: CloseBlockFn,
139    ) -> RenderBlock {
140        Arc::new(move |cx: &mut BlockContext| {
141            let execution_view = execution_view.clone();
142            let text_style = crate::outputs::plain::text_style(cx.window, cx.app);
143
144            let editor_margins = cx.margins;
145            let gutter = editor_margins.gutter;
146
147            let block_id = cx.block_id;
148            let on_close = on_close.clone();
149
150            let rem_size = cx.window.rem_size();
151
152            let text_line_height = text_style.line_height_in_pixels(rem_size);
153            let output_settings = ReplSettings::get_global(cx.app);
154            let output_max_height = if output_settings.output_max_height_lines > 0 {
155                Some(text_line_height * output_settings.output_max_height_lines as f32)
156            } else {
157                None
158            };
159
160            let close_button = h_flex()
161                .flex_none()
162                .items_center()
163                .justify_center()
164                .absolute()
165                .top(text_line_height / 2.)
166                .right(
167                    // 2px is a magic number to nudge the button just a bit closer to
168                    // the line number start
169                    gutter.full_width() / 2.0 - text_line_height / 2.0 - px(2.),
170                )
171                .w(text_line_height)
172                .h(text_line_height)
173                .child(
174                    IconButton::new("close_output_area", IconName::Close)
175                        .icon_size(IconSize::Small)
176                        .icon_color(Color::Muted)
177                        .size(ButtonSize::Compact)
178                        .shape(IconButtonShape::Square)
179                        .tooltip(Tooltip::text("Close output area"))
180                        .on_click(move |_, window, cx| {
181                            if let BlockId::Custom(block_id) = block_id {
182                                (on_close)(block_id, window, cx)
183                            }
184                        }),
185                );
186
187            div()
188                .id(cx.block_id)
189                .block_mouse_except_scroll()
190                .flex()
191                .items_start()
192                .min_h(text_line_height)
193                .w_full()
194                .border_y_1()
195                .border_color(cx.theme().colors().border)
196                .bg(cx.theme().colors().background)
197                .child(
198                    div()
199                        .relative()
200                        .w(gutter.full_width())
201                        .h(text_line_height * 2)
202                        .child(close_button),
203                )
204                .child(
205                    div()
206                        .id((ElementId::from(cx.block_id), "output-scroll"))
207                        .flex_1()
208                        .overflow_x_hidden()
209                        .py(text_line_height / 2.)
210                        .mr(editor_margins.right)
211                        .pr_2()
212                        .when_some(output_max_height, |div, max_h| {
213                            div.max_h(max_h).overflow_y_scroll()
214                        })
215                        .child(execution_view),
216                )
217                .into_any_element()
218        })
219    }
220}
221
222impl Session {
223    pub fn new(
224        editor: WeakEntity<Editor>,
225        fs: Arc<dyn Fs>,
226        kernel_specification: KernelSpecification,
227        window: &mut Window,
228        cx: &mut Context<Self>,
229    ) -> Self {
230        let subscription = match editor.upgrade() {
231            Some(editor) => {
232                let buffer = editor.read(cx).buffer().clone();
233                cx.subscribe(&buffer, Self::on_buffer_event)
234            }
235            None => Subscription::new(|| {}),
236        };
237
238        let editor_handle = editor.clone();
239
240        editor
241            .update(cx, |editor, _cx| {
242                setup_editor_session_actions(editor, editor_handle);
243            })
244            .ok();
245
246        let mut session = Self {
247            fs,
248            editor,
249            kernel: Kernel::StartingKernel(Task::ready(()).shared()),
250            blocks: HashMap::default(),
251            result_inlays: HashMap::default(),
252            next_inlay_id: 0,
253            kernel_specification,
254            _subscriptions: vec![subscription],
255        };
256
257        session.start_kernel(window, cx);
258        session
259    }
260
261    fn start_kernel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
262        let kernel_language = self.kernel_specification.language();
263        let entity_id = self.editor.entity_id();
264        let working_directory = self
265            .editor
266            .upgrade()
267            .and_then(|editor| editor.read(cx).working_directory(cx))
268            .unwrap_or_else(temp_dir);
269
270        telemetry::event!(
271            "Kernel Status Changed",
272            kernel_language,
273            kernel_status = KernelStatus::Starting.to_string(),
274            repl_session_id = cx.entity_id().to_string(),
275        );
276
277        let session_view = cx.entity();
278
279        let kernel = match self.kernel_specification.clone() {
280            KernelSpecification::Jupyter(kernel_specification)
281            | KernelSpecification::PythonEnv(kernel_specification) => NativeRunningKernel::new(
282                kernel_specification,
283                entity_id,
284                working_directory,
285                self.fs.clone(),
286                session_view,
287                window,
288                cx,
289            ),
290            KernelSpecification::Remote(remote_kernel_specification) => RemoteRunningKernel::new(
291                remote_kernel_specification,
292                working_directory,
293                session_view,
294                window,
295                cx,
296            ),
297        };
298
299        let pending_kernel = cx
300            .spawn(async move |this, cx| {
301                let kernel = kernel.await;
302
303                match kernel {
304                    Ok(kernel) => {
305                        this.update(cx, |session, cx| {
306                            session.kernel(Kernel::RunningKernel(kernel), cx);
307                        })
308                        .ok();
309                    }
310                    Err(err) => {
311                        this.update(cx, |session, cx| {
312                            session.kernel_errored(err.to_string(), cx);
313                        })
314                        .ok();
315                    }
316                }
317            })
318            .shared();
319
320        self.kernel(Kernel::StartingKernel(pending_kernel), cx);
321        cx.notify();
322    }
323
324    pub fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
325        self.kernel(Kernel::ErroredLaunch(error_message.clone()), cx);
326
327        self.blocks.values().for_each(|block| {
328            block.execution_view.update(cx, |execution_view, cx| {
329                match execution_view.status {
330                    ExecutionStatus::Finished => {
331                        // Do nothing when the output was good
332                    }
333                    _ => {
334                        // All other cases, set the status to errored
335                        execution_view.status =
336                            ExecutionStatus::KernelErrored(error_message.clone())
337                    }
338                }
339                cx.notify();
340            });
341        });
342    }
343
344    fn on_buffer_event(
345        &mut self,
346        buffer: Entity<MultiBuffer>,
347        event: &multi_buffer::Event,
348        cx: &mut Context<Self>,
349    ) {
350        if let multi_buffer::Event::Edited { .. } = event {
351            let snapshot = buffer.read(cx).snapshot(cx);
352
353            let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
354            let mut gutter_ranges_to_remove: Vec<Range<Anchor>> = Vec::new();
355            let mut keys_to_remove: Vec<String> = Vec::new();
356
357            self.blocks.retain(|id, block| {
358                if block.invalidation_anchor.is_valid(&snapshot) {
359                    true
360                } else {
361                    blocks_to_remove.insert(block.block_id);
362                    gutter_ranges_to_remove.push(block.code_range.clone());
363                    keys_to_remove.push(id.clone());
364                    false
365                }
366            });
367
368            let mut inlays_to_remove: Vec<InlayId> = Vec::new();
369
370            self.result_inlays
371                .retain(|id, (inlay_id, code_range, original_len)| {
372                    let start_offset = code_range.start.to_offset(&snapshot);
373                    let end_offset = code_range.end.to_offset(&snapshot);
374                    let current_len = end_offset.saturating_sub(start_offset);
375
376                    if current_len != *original_len {
377                        inlays_to_remove.push(*inlay_id);
378                        gutter_ranges_to_remove.push(code_range.clone());
379                        keys_to_remove.push(id.clone());
380                        false
381                    } else {
382                        true
383                    }
384                });
385
386            if !blocks_to_remove.is_empty()
387                || !inlays_to_remove.is_empty()
388                || !gutter_ranges_to_remove.is_empty()
389            {
390                self.editor
391                    .update(cx, |editor, cx| {
392                        if !blocks_to_remove.is_empty() {
393                            editor.remove_blocks(blocks_to_remove, None, cx);
394                        }
395                        if !inlays_to_remove.is_empty() {
396                            editor.splice_inlays(&inlays_to_remove, vec![], cx);
397                        }
398                        if !gutter_ranges_to_remove.is_empty() {
399                            editor.remove_gutter_highlights::<ReplExecutedRange>(
400                                gutter_ranges_to_remove,
401                                cx,
402                            );
403                        }
404                    })
405                    .ok();
406                cx.notify();
407            }
408        }
409    }
410
411    fn send(&mut self, message: JupyterMessage, _cx: &mut Context<Self>) -> anyhow::Result<()> {
412        if let Kernel::RunningKernel(kernel) = &mut self.kernel {
413            kernel.request_tx().try_send(message).ok();
414        }
415
416        anyhow::Ok(())
417    }
418
419    fn replace_block_with_inlay(&mut self, message_id: &str, text: &str, cx: &mut Context<Self>) {
420        let Some(block) = self.blocks.remove(message_id) else {
421            return;
422        };
423
424        let Some(editor) = self.editor.upgrade() else {
425            return;
426        };
427
428        let code_range = block.code_range.clone();
429
430        editor.update(cx, |editor, cx| {
431            let mut block_ids = HashSet::default();
432            block_ids.insert(block.block_id);
433            editor.remove_blocks(block_ids, None, cx);
434
435            let buffer = editor.buffer().read(cx).snapshot(cx);
436            let start_offset = code_range.start.to_offset(&buffer);
437            let end_offset = code_range.end.to_offset(&buffer);
438            let original_len = end_offset.saturating_sub(start_offset);
439
440            let end_point = code_range.end.to_point(&buffer);
441            let inlay_position = buffer.anchor_after(end_point);
442
443            let inlay_id = self.next_inlay_id;
444            self.next_inlay_id += 1;
445
446            let inlay = Inlay::repl_result(inlay_id, inlay_position, format!("    {}", text));
447
448            editor.splice_inlays(&[], vec![inlay], cx);
449            self.result_inlays.insert(
450                message_id.to_string(),
451                (
452                    InlayId::ReplResult(inlay_id),
453                    code_range.clone(),
454                    original_len,
455                ),
456            );
457
458            editor.insert_gutter_highlight::<ReplExecutedRange>(
459                code_range,
460                |cx| cx.theme().status().success,
461                cx,
462            );
463        });
464
465        cx.notify();
466    }
467
468    pub fn clear_outputs(&mut self, cx: &mut Context<Self>) {
469        let blocks_to_remove: HashSet<CustomBlockId> =
470            self.blocks.values().map(|block| block.block_id).collect();
471
472        let inlays_to_remove: Vec<InlayId> =
473            self.result_inlays.values().map(|(id, _, _)| *id).collect();
474
475        self.editor
476            .update(cx, |editor, cx| {
477                editor.remove_blocks(blocks_to_remove, None, cx);
478                editor.splice_inlays(&inlays_to_remove, vec![], cx);
479                editor.clear_gutter_highlights::<ReplExecutedRange>(cx);
480            })
481            .ok();
482
483        self.blocks.clear();
484        self.result_inlays.clear();
485    }
486
487    pub fn execute(
488        &mut self,
489        code: String,
490        anchor_range: Range<Anchor>,
491        next_cell: Option<Anchor>,
492        move_down: bool,
493        window: &mut Window,
494        cx: &mut Context<Self>,
495    ) {
496        let Some(editor) = self.editor.upgrade() else {
497            return;
498        };
499
500        if code.is_empty() {
501            return;
502        }
503
504        let execute_request = ExecuteRequest {
505            code,
506            ..ExecuteRequest::default()
507        };
508
509        let message: JupyterMessage = execute_request.into();
510
511        let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
512        let mut inlays_to_remove: Vec<InlayId> = Vec::new();
513        let mut gutter_ranges_to_remove: Vec<Range<Anchor>> = Vec::new();
514
515        let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
516
517        self.blocks.retain(|_key, block| {
518            if anchor_range.overlaps(&block.code_range, &buffer) {
519                blocks_to_remove.insert(block.block_id);
520                false
521            } else {
522                true
523            }
524        });
525
526        self.result_inlays
527            .retain(|_key, (inlay_id, inlay_range, _)| {
528                if anchor_range.overlaps(inlay_range, &buffer) {
529                    inlays_to_remove.push(*inlay_id);
530                    gutter_ranges_to_remove.push(inlay_range.clone());
531                    false
532                } else {
533                    true
534                }
535            });
536
537        self.editor
538            .update(cx, |editor, cx| {
539                editor.remove_blocks(blocks_to_remove, None, cx);
540                if !inlays_to_remove.is_empty() {
541                    editor.splice_inlays(&inlays_to_remove, vec![], cx);
542                }
543                if !gutter_ranges_to_remove.is_empty() {
544                    editor
545                        .remove_gutter_highlights::<ReplExecutedRange>(gutter_ranges_to_remove, cx);
546                }
547            })
548            .ok();
549
550        let status = match &self.kernel {
551            Kernel::Restarting => ExecutionStatus::Restarting,
552            Kernel::RunningKernel(_) => ExecutionStatus::Queued,
553            Kernel::StartingKernel(_) => ExecutionStatus::ConnectingToKernel,
554            Kernel::ErroredLaunch(error) => ExecutionStatus::KernelErrored(error.clone()),
555            Kernel::ShuttingDown => ExecutionStatus::ShuttingDown,
556            Kernel::Shutdown => ExecutionStatus::Shutdown,
557        };
558
559        let parent_message_id = message.header.msg_id.clone();
560        let session_view = cx.entity().downgrade();
561        let weak_editor = self.editor.clone();
562        let code_range_for_close = anchor_range.clone();
563
564        let on_close: CloseBlockFn = Arc::new(
565            move |block_id: CustomBlockId, _: &mut Window, cx: &mut App| {
566                if let Some(session) = session_view.upgrade() {
567                    session.update(cx, |session, cx| {
568                        session.blocks.remove(&parent_message_id);
569                        cx.notify();
570                    });
571                }
572
573                if let Some(editor) = weak_editor.upgrade() {
574                    editor.update(cx, |editor, cx| {
575                        let mut block_ids = HashSet::default();
576                        block_ids.insert(block_id);
577                        editor.remove_blocks(block_ids, None, cx);
578                        editor.remove_gutter_highlights::<ReplExecutedRange>(
579                            vec![code_range_for_close.clone()],
580                            cx,
581                        );
582                    });
583                }
584            },
585        );
586
587        let Ok(editor_block) = EditorBlock::new(
588            self.editor.clone(),
589            anchor_range.clone(),
590            status,
591            on_close,
592            cx,
593        ) else {
594            return;
595        };
596
597        self.editor
598            .update(cx, |editor, cx| {
599                editor.insert_gutter_highlight::<ReplExecutedRange>(
600                    anchor_range.clone(),
601                    |cx| cx.theme().status().success,
602                    cx,
603                );
604            })
605            .ok();
606
607        let new_cursor_pos = if let Some(next_cursor) = next_cell {
608            next_cursor
609        } else {
610            editor_block.invalidation_anchor
611        };
612
613        let msg_id = message.header.msg_id.clone();
614        let subscription = cx.subscribe(
615            &editor_block.execution_view,
616            move |session, _execution_view, _event: &ExecutionViewFinishedEmpty, cx| {
617                session.replace_block_with_inlay(&msg_id, "", cx);
618            },
619        );
620        self._subscriptions.push(subscription);
621
622        let msg_id = message.header.msg_id.clone();
623        let subscription = cx.subscribe(
624            &editor_block.execution_view,
625            move |session, _execution_view, event: &ExecutionViewFinishedSmall, cx| {
626                session.replace_block_with_inlay(&msg_id, &event.0, cx);
627            },
628        );
629        self._subscriptions.push(subscription);
630
631        self.blocks
632            .insert(message.header.msg_id.clone(), editor_block);
633
634        match &self.kernel {
635            Kernel::RunningKernel(_) => {
636                self.send(message, cx).ok();
637            }
638            Kernel::StartingKernel(task) => {
639                // Queue up the execution as a task to run after the kernel starts
640                let task = task.clone();
641
642                cx.spawn(async move |this, cx| {
643                    task.await;
644                    this.update(cx, |session, cx| {
645                        session.send(message, cx).ok();
646                    })
647                    .ok();
648                })
649                .detach();
650            }
651            _ => {}
652        }
653
654        if move_down {
655            editor.update(cx, move |editor, cx| {
656                editor.change_selections(
657                    SelectionEffects::scroll(Autoscroll::top_relative(8)),
658                    window,
659                    cx,
660                    |selections| {
661                        selections.select_ranges([new_cursor_pos..new_cursor_pos]);
662                    },
663                );
664            });
665        }
666    }
667
668    pub fn interrupt(&mut self, cx: &mut Context<Self>) {
669        match &mut self.kernel {
670            Kernel::RunningKernel(_kernel) => {
671                self.send(InterruptRequest {}.into(), cx).ok();
672            }
673            Kernel::StartingKernel(_task) => {
674                // NOTE: If we switch to a literal queue instead of chaining on to the task, clear all queued executions
675            }
676            _ => {}
677        }
678    }
679
680    pub fn kernel(&mut self, kernel: Kernel, cx: &mut Context<Self>) {
681        if let Kernel::Shutdown = kernel {
682            cx.emit(SessionEvent::Shutdown(self.editor.clone()));
683        }
684
685        let kernel_status = KernelStatus::from(&kernel).to_string();
686        let kernel_language = self.kernel_specification.language();
687
688        telemetry::event!(
689            "Kernel Status Changed",
690            kernel_language,
691            kernel_status,
692            repl_session_id = cx.entity_id().to_string(),
693        );
694
695        self.kernel = kernel;
696    }
697
698    pub fn shutdown(&mut self, window: &mut Window, cx: &mut Context<Self>) {
699        let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown);
700
701        match kernel {
702            Kernel::RunningKernel(mut kernel) => {
703                let mut request_tx = kernel.request_tx();
704
705                let forced = kernel.force_shutdown(window, cx);
706
707                cx.spawn(async move |this, cx| {
708                    let message: JupyterMessage = ShutdownRequest { restart: false }.into();
709                    request_tx.try_send(message).ok();
710
711                    forced.await.log_err();
712
713                    // Give the kernel a bit of time to clean up
714                    cx.background_executor().timer(Duration::from_secs(3)).await;
715
716                    this.update(cx, |session, cx| {
717                        session.clear_outputs(cx);
718                        session.kernel(Kernel::Shutdown, cx);
719                        cx.notify();
720                    })
721                    .ok();
722                })
723                .detach();
724            }
725            _ => {
726                self.kernel(Kernel::Shutdown, cx);
727            }
728        }
729        cx.notify();
730    }
731
732    pub fn restart(&mut self, window: &mut Window, cx: &mut Context<Self>) {
733        let kernel = std::mem::replace(&mut self.kernel, Kernel::Restarting);
734
735        match kernel {
736            Kernel::Restarting => {
737                // Do nothing if already restarting
738            }
739            Kernel::RunningKernel(mut kernel) => {
740                let mut request_tx = kernel.request_tx();
741
742                let forced = kernel.force_shutdown(window, cx);
743
744                cx.spawn_in(window, async move |this, cx| {
745                    // Send shutdown request with restart flag
746                    log::debug!("restarting kernel");
747                    let message: JupyterMessage = ShutdownRequest { restart: true }.into();
748                    request_tx.try_send(message).ok();
749
750                    // Wait for kernel to shutdown
751                    cx.background_executor().timer(Duration::from_secs(1)).await;
752
753                    // Force kill the kernel if it hasn't shut down
754                    forced.await.log_err();
755
756                    // Start a new kernel
757                    this.update_in(cx, |session, window, cx| {
758                        // TODO: Differentiate between restart and restart+clear-outputs
759                        session.clear_outputs(cx);
760                        session.start_kernel(window, cx);
761                    })
762                    .ok();
763                })
764                .detach();
765            }
766            _ => {
767                self.clear_outputs(cx);
768                self.start_kernel(window, cx);
769            }
770        }
771        cx.notify();
772    }
773}
774
775pub enum SessionEvent {
776    Shutdown(WeakEntity<Editor>),
777}
778
779impl EventEmitter<SessionEvent> for Session {}
780
781impl Render for Session {
782    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
783        let (status_text, interrupt_button) = match &self.kernel {
784            Kernel::RunningKernel(kernel) => (
785                kernel
786                    .kernel_info()
787                    .as_ref()
788                    .map(|info| info.language_info.name.clone()),
789                Some(
790                    Button::new("interrupt", "Interrupt")
791                        .style(ButtonStyle::Subtle)
792                        .on_click(cx.listener(move |session, _, _, cx| {
793                            session.interrupt(cx);
794                        })),
795                ),
796            ),
797            Kernel::StartingKernel(_) => (Some("Starting".into()), None),
798            Kernel::ErroredLaunch(err) => (Some(format!("Error: {err}")), None),
799            Kernel::ShuttingDown => (Some("Shutting Down".into()), None),
800            Kernel::Shutdown => (Some("Shutdown".into()), None),
801            Kernel::Restarting => (Some("Restarting".into()), None),
802        };
803
804        KernelListItem::new(self.kernel_specification.clone())
805            .status_color(match &self.kernel {
806                Kernel::RunningKernel(kernel) => match kernel.execution_state() {
807                    ExecutionState::Idle => Color::Success,
808                    ExecutionState::Busy => Color::Modified,
809                    ExecutionState::Unknown => Color::Modified,
810                    ExecutionState::Starting => Color::Modified,
811                    ExecutionState::Restarting => Color::Modified,
812                    ExecutionState::Terminating => Color::Disabled,
813                    ExecutionState::AutoRestarting => Color::Modified,
814                    ExecutionState::Dead => Color::Disabled,
815                    ExecutionState::Other(_) => Color::Modified,
816                },
817                Kernel::StartingKernel(_) => Color::Modified,
818                Kernel::ErroredLaunch(_) => Color::Error,
819                Kernel::ShuttingDown => Color::Modified,
820                Kernel::Shutdown => Color::Disabled,
821                Kernel::Restarting => Color::Modified,
822            })
823            .child(Label::new(self.kernel_specification.name()))
824            .children(status_text.map(|status_text| Label::new(format!("({status_text})"))))
825            .button(
826                Button::new("shutdown", "Shutdown")
827                    .style(ButtonStyle::Subtle)
828                    .disabled(self.kernel.is_shutting_down())
829                    .on_click(cx.listener(move |session, _, window, cx| {
830                        session.shutdown(window, cx);
831                    })),
832            )
833            .buttons(interrupt_button)
834    }
835}
836
837impl KernelSession for Session {
838    fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context<Self>) {
839        let parent_message_id = match message.parent_header.as_ref() {
840            Some(header) => &header.msg_id,
841            None => return,
842        };
843
844        match &message.content {
845            JupyterMessageContent::Status(status) => {
846                self.kernel.set_execution_state(&status.execution_state);
847
848                telemetry::event!(
849                    "Kernel Status Changed",
850                    kernel_language = self.kernel_specification.language(),
851                    kernel_status = KernelStatus::from(&self.kernel).to_string(),
852                    repl_session_id = cx.entity_id().to_string(),
853                );
854
855                cx.notify();
856            }
857            JupyterMessageContent::KernelInfoReply(reply) => {
858                self.kernel.set_kernel_info(reply);
859                cx.notify();
860            }
861            JupyterMessageContent::UpdateDisplayData(update) => {
862                let display_id = if let Some(display_id) = update.transient.display_id.clone() {
863                    display_id
864                } else {
865                    return;
866                };
867
868                self.blocks.iter_mut().for_each(|(_, block)| {
869                    block.execution_view.update(cx, |execution_view, cx| {
870                        execution_view.update_display_data(&update.data, &display_id, window, cx);
871                    });
872                });
873                return;
874            }
875            _ => {}
876        }
877
878        if let Some(block) = self.blocks.get_mut(parent_message_id) {
879            block.handle_message(message, window, cx);
880        }
881    }
882
883    fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
884        self.kernel_errored(error_message, cx);
885    }
886}