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