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