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