1use crate::{
2 jupyter_settings::{JupyterDockPosition, JupyterSettings},
3 kernels::{kernel_specifications, KernelSpecification},
4 session::{Session, SessionEvent},
5};
6use anyhow::{Context as _, Result};
7use collections::HashMap;
8use editor::{Anchor, Editor, RangeToAnchorExt};
9use futures::StreamExt as _;
10use gpui::{
11 actions, prelude::*, AppContext, AsyncWindowContext, EntityId, EventEmitter, FocusHandle,
12 FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
13};
14use language::Point;
15use project::Fs;
16use settings::{Settings as _, SettingsStore};
17use std::{ops::Range, sync::Arc};
18use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
19use util::ResultExt as _;
20use workspace::{
21 dock::{Panel, PanelEvent},
22 Workspace,
23};
24
25actions!(repl, [Run, ClearOutputs]);
26actions!(repl_panel, [ToggleFocus]);
27
28pub fn init(cx: &mut AppContext) {
29 cx.observe_new_views(
30 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
31 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
32 workspace.toggle_panel_focus::<RuntimePanel>(cx);
33 });
34 },
35 )
36 .detach();
37}
38
39pub struct RuntimePanel {
40 fs: Arc<dyn Fs>,
41 enabled: bool,
42 focus_handle: FocusHandle,
43 width: Option<Pixels>,
44 sessions: HashMap<EntityId, View<Session>>,
45 kernel_specifications: Vec<KernelSpecification>,
46 _subscriptions: Vec<Subscription>,
47 _editor_events_task: Task<()>,
48}
49
50pub enum ReplEvent {
51 Run(WeakView<Editor>),
52 ClearOutputs(WeakView<Editor>),
53}
54
55impl RuntimePanel {
56 pub fn load(
57 workspace: WeakView<Workspace>,
58 cx: AsyncWindowContext,
59 ) -> Task<Result<View<Self>>> {
60 cx.spawn(|mut cx| async move {
61 let view = workspace.update(&mut cx, |workspace, cx| {
62 cx.new_view::<Self>(|cx| {
63 let focus_handle = cx.focus_handle();
64
65 let fs = workspace.app_state().fs.clone();
66
67 // Make a channel that we receive editor events on (for repl::Run, repl::ClearOutputs)
68 // This allows us to inject actions on the editor from the repl panel without requiring the editor to
69 // depend on the `repl` crate.
70 let (repl_editor_event_tx, mut repl_editor_event_rx) =
71 futures::channel::mpsc::unbounded::<ReplEvent>();
72
73 let subscriptions = vec![
74 cx.on_focus_in(&focus_handle, Self::focus_in),
75 cx.on_focus_out(&focus_handle, Self::focus_out),
76 cx.observe_global::<SettingsStore>(move |this, cx| {
77 this.set_enabled(JupyterSettings::enabled(cx), cx);
78 }),
79 cx.observe_new_views(
80 move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
81 let editor_view = cx.view().downgrade();
82 let run_event_tx = repl_editor_event_tx.clone();
83 let clear_event_tx = repl_editor_event_tx.clone();
84 editor
85 .register_action(move |_: &Run, cx: &mut WindowContext| {
86 if !JupyterSettings::enabled(cx) {
87 return;
88 }
89 run_event_tx
90 .unbounded_send(ReplEvent::Run(editor_view.clone()))
91 .ok();
92 })
93 .detach();
94
95 let editor_view = cx.view().downgrade();
96 editor
97 .register_action(
98 move |_: &ClearOutputs, cx: &mut WindowContext| {
99 if !JupyterSettings::enabled(cx) {
100 return;
101 }
102 clear_event_tx
103 .unbounded_send(ReplEvent::ClearOutputs(
104 editor_view.clone(),
105 ))
106 .ok();
107 },
108 )
109 .detach();
110 },
111 ),
112 ];
113
114 // Listen for events from the editor on the `repl_editor_event_rx` channel
115 let _editor_events_task = cx.spawn(
116 move |this: WeakView<RuntimePanel>, mut cx: AsyncWindowContext| async move {
117 while let Some(event) = repl_editor_event_rx.next().await {
118 this.update(&mut cx, |runtime_panel, cx| match event {
119 ReplEvent::Run(editor) => {
120 runtime_panel.run(editor, cx).log_err();
121 }
122 ReplEvent::ClearOutputs(editor) => {
123 runtime_panel.clear_outputs(editor, cx);
124 }
125 })
126 .ok();
127 }
128 },
129 );
130
131 let runtime_panel = Self {
132 fs: fs.clone(),
133 width: None,
134 focus_handle,
135 kernel_specifications: Vec::new(),
136 sessions: Default::default(),
137 _subscriptions: subscriptions,
138 enabled: JupyterSettings::enabled(cx),
139 _editor_events_task,
140 };
141
142 runtime_panel
143 })
144 })?;
145
146 view.update(&mut cx, |this, cx| this.refresh_kernelspecs(cx))?
147 .await?;
148
149 Ok(view)
150 })
151 }
152
153 fn set_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
154 if self.enabled != enabled {
155 self.enabled = enabled;
156 cx.notify();
157 }
158 }
159
160 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
161 cx.notify();
162 }
163
164 fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
165 cx.notify();
166 }
167
168 // Gets the active selection in the editor or the current line
169 fn selection(&self, editor: View<Editor>, cx: &mut ViewContext<Self>) -> Range<Anchor> {
170 let editor = editor.read(cx);
171 let selection = editor.selections.newest::<usize>(cx);
172 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
173
174 let range = if selection.is_empty() {
175 let cursor = selection.head();
176
177 let line_start = multi_buffer_snapshot.offset_to_point(cursor).row;
178 let mut start_offset = multi_buffer_snapshot.point_to_offset(Point::new(line_start, 0));
179
180 // Iterate backwards to find the start of the line
181 while start_offset > 0 {
182 let ch = multi_buffer_snapshot
183 .chars_at(start_offset - 1)
184 .next()
185 .unwrap_or('\0');
186 if ch == '\n' {
187 break;
188 }
189 start_offset -= 1;
190 }
191
192 let mut end_offset = cursor;
193
194 // Iterate forwards to find the end of the line
195 while end_offset < multi_buffer_snapshot.len() {
196 let ch = multi_buffer_snapshot
197 .chars_at(end_offset)
198 .next()
199 .unwrap_or('\0');
200 if ch == '\n' {
201 break;
202 }
203 end_offset += 1;
204 }
205
206 // Create a range from the start to the end of the line
207 start_offset..end_offset
208 } else {
209 selection.range()
210 };
211
212 range.to_anchors(&multi_buffer_snapshot)
213 }
214
215 pub fn snippet(
216 &self,
217 editor: WeakView<Editor>,
218 cx: &mut ViewContext<Self>,
219 ) -> Option<(String, Arc<str>, Range<Anchor>)> {
220 let editor = editor.upgrade()?;
221
222 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
223 let anchor_range = self.selection(editor, cx);
224
225 let selected_text = buffer
226 .text_for_range(anchor_range.clone())
227 .collect::<String>();
228
229 let start_language = buffer.language_at(anchor_range.start);
230 let end_language = buffer.language_at(anchor_range.end);
231
232 let language_name = if start_language == end_language {
233 start_language
234 .map(|language| language.code_fence_block_name())
235 .filter(|lang| **lang != *"markdown")?
236 } else {
237 // If the selection spans multiple languages, don't run it
238 return None;
239 };
240
241 Some((selected_text, language_name, anchor_range))
242 }
243
244 pub fn language(
245 &self,
246 editor: WeakView<Editor>,
247 cx: &mut ViewContext<Self>,
248 ) -> Option<Arc<str>> {
249 match self.snippet(editor, cx) {
250 Some((_, language, _)) => Some(language),
251 None => None,
252 }
253 }
254
255 pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
256 let kernel_specifications = kernel_specifications(self.fs.clone());
257 cx.spawn(|this, mut cx| async move {
258 let kernel_specifications = kernel_specifications.await?;
259
260 this.update(&mut cx, |this, cx| {
261 this.kernel_specifications = kernel_specifications;
262 cx.notify();
263 })
264 })
265 }
266
267 pub fn kernelspec(
268 &self,
269 language_name: &str,
270 cx: &mut ViewContext<Self>,
271 ) -> Option<KernelSpecification> {
272 let settings = JupyterSettings::get_global(cx);
273 let selected_kernel = settings.kernel_selections.get(language_name);
274
275 self.kernel_specifications
276 .iter()
277 .find(|runtime_specification| {
278 if let Some(selected) = selected_kernel {
279 // Top priority is the selected kernel
280 runtime_specification.name.to_lowercase() == selected.to_lowercase()
281 } else {
282 // Otherwise, we'll try to find a kernel that matches the language
283 runtime_specification.kernelspec.language.to_lowercase()
284 == language_name.to_lowercase()
285 }
286 })
287 .cloned()
288 }
289
290 pub fn run(
291 &mut self,
292 editor: WeakView<Editor>,
293 cx: &mut ViewContext<Self>,
294 ) -> anyhow::Result<()> {
295 if !self.enabled {
296 return Ok(());
297 }
298
299 let (selected_text, language_name, anchor_range) = match self.snippet(editor.clone(), cx) {
300 Some(snippet) => snippet,
301 None => return Ok(()),
302 };
303
304 let entity_id = editor.entity_id();
305
306 let kernel_specification = self
307 .kernelspec(&language_name, cx)
308 .with_context(|| format!("No kernel found for language: {language_name}"))?;
309
310 let session = self.sessions.entry(entity_id).or_insert_with(|| {
311 let view =
312 cx.new_view(|cx| Session::new(editor, self.fs.clone(), kernel_specification, cx));
313 cx.notify();
314
315 let subscription = cx.subscribe(
316 &view,
317 |panel: &mut RuntimePanel, _session: View<Session>, event: &SessionEvent, _cx| {
318 match event {
319 SessionEvent::Shutdown(shutdown_event) => {
320 panel.sessions.remove(&shutdown_event.entity_id());
321 }
322 }
323 //
324 },
325 );
326
327 subscription.detach();
328
329 view
330 });
331
332 session.update(cx, |session, cx| {
333 session.execute(&selected_text, anchor_range, cx);
334 });
335
336 anyhow::Ok(())
337 }
338
339 pub fn clear_outputs(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
340 let entity_id = editor.entity_id();
341 if let Some(session) = self.sessions.get_mut(&entity_id) {
342 session.update(cx, |session, cx| {
343 session.clear_outputs(cx);
344 });
345 cx.notify();
346 }
347 }
348}
349
350pub enum SessionSupport {
351 ActiveSession(View<Session>),
352 Inactive(KernelSpecification),
353 RequiresSetup(String),
354 Unsupported,
355}
356
357impl RuntimePanel {
358 pub fn session(
359 &mut self,
360 editor: WeakView<Editor>,
361 cx: &mut ViewContext<Self>,
362 ) -> SessionSupport {
363 let entity_id = editor.entity_id();
364 let session = self.sessions.get(&entity_id).cloned();
365
366 match session {
367 Some(session) => SessionSupport::ActiveSession(session),
368 None => {
369 let language = self.language(editor, cx);
370 let language = match language {
371 Some(language) => language,
372 None => return SessionSupport::Unsupported,
373 };
374 // Check for kernelspec
375 let kernelspec = self.kernelspec(&language, cx);
376
377 match kernelspec {
378 Some(kernelspec) => SessionSupport::Inactive(kernelspec),
379 None => {
380 let language: String = language.to_lowercase();
381 // If no kernelspec but language is one of typescript, python, r, or julia
382 // then we return RequiresSetup
383 match language.as_str() {
384 "typescript" | "python" => SessionSupport::RequiresSetup(language),
385 _ => SessionSupport::Unsupported,
386 }
387 }
388 }
389 }
390 }
391 }
392}
393
394impl Panel for RuntimePanel {
395 fn persistent_name() -> &'static str {
396 "RuntimePanel"
397 }
398
399 fn position(&self, cx: &ui::WindowContext) -> workspace::dock::DockPosition {
400 match JupyterSettings::get_global(cx).dock {
401 JupyterDockPosition::Left => workspace::dock::DockPosition::Left,
402 JupyterDockPosition::Right => workspace::dock::DockPosition::Right,
403 JupyterDockPosition::Bottom => workspace::dock::DockPosition::Bottom,
404 }
405 }
406
407 fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool {
408 true
409 }
410
411 fn set_position(
412 &mut self,
413 position: workspace::dock::DockPosition,
414 cx: &mut ViewContext<Self>,
415 ) {
416 settings::update_settings_file::<JupyterSettings>(self.fs.clone(), cx, move |settings| {
417 let dock = match position {
418 workspace::dock::DockPosition::Left => JupyterDockPosition::Left,
419 workspace::dock::DockPosition::Right => JupyterDockPosition::Right,
420 workspace::dock::DockPosition::Bottom => JupyterDockPosition::Bottom,
421 };
422 settings.set_dock(dock);
423 })
424 }
425
426 fn size(&self, cx: &ui::WindowContext) -> Pixels {
427 let settings = JupyterSettings::get_global(cx);
428
429 self.width.unwrap_or(settings.default_width)
430 }
431
432 fn set_size(&mut self, size: Option<ui::Pixels>, _cx: &mut ViewContext<Self>) {
433 self.width = size;
434 }
435
436 fn icon(&self, _cx: &ui::WindowContext) -> Option<ui::IconName> {
437 if !self.enabled {
438 return None;
439 }
440
441 Some(IconName::Code)
442 }
443
444 fn icon_tooltip(&self, _cx: &ui::WindowContext) -> Option<&'static str> {
445 Some("Runtime Panel")
446 }
447
448 fn toggle_action(&self) -> Box<dyn gpui::Action> {
449 Box::new(ToggleFocus)
450 }
451}
452
453impl EventEmitter<PanelEvent> for RuntimePanel {}
454
455impl FocusableView for RuntimePanel {
456 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
457 self.focus_handle.clone()
458 }
459}
460
461impl Render for RuntimePanel {
462 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
463 // When there are no kernel specifications, show a link to the Zed docs explaining how to
464 // install kernels. It can be assumed they don't have a running kernel if we have no
465 // specifications.
466 if self.kernel_specifications.is_empty() {
467 return v_flex()
468 .p_4()
469 .size_full()
470 .gap_2()
471 .child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
472 .child(
473 Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
474 .size(LabelSize::Default),
475 )
476 .child(
477 h_flex().w_full().p_4().justify_center().gap_2().child(
478 ButtonLike::new("install-kernels")
479 .style(ButtonStyle::Filled)
480 .size(ButtonSize::Large)
481 .layer(ElevationIndex::ModalSurface)
482 .child(Label::new("Install Kernels"))
483 .on_click(move |_, cx| {
484 cx.open_url(
485 "https://docs.jupyter.org/en/latest/install/kernels.html",
486 )
487 }),
488 ),
489 )
490 .into_any_element();
491 }
492
493 // When there are no sessions, show the command to run code in an editor
494 if self.sessions.is_empty() {
495 return v_flex()
496 .p_4()
497 .size_full()
498 .gap_2()
499 .child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
500 .child(
501 v_flex().child(
502 Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
503 .size(LabelSize::Default)
504 )
505 .children(
506 KeyBinding::for_action(&Run, cx)
507 .map(|binding|
508 binding.into_any_element()
509 )
510 )
511 )
512
513 .into_any_element();
514 }
515
516 v_flex()
517 .p_4()
518 .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
519 .children(
520 self.sessions
521 .values()
522 .map(|session| session.clone().into_any_element()),
523 )
524 .into_any_element()
525 }
526}