1use editor::Editor;
2use gpui::{
3 actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView,
4 FontWeight, Subscription, View,
5};
6use std::collections::HashMap;
7use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding, ListItem, Tooltip};
8use util::ResultExt as _;
9use workspace::item::ItemEvent;
10use workspace::WorkspaceId;
11use workspace::{item::Item, Workspace};
12
13use crate::jupyter_settings::JupyterSettings;
14use crate::repl_store::ReplStore;
15use crate::{KernelSpecification, KERNEL_DOCS_URL};
16
17actions!(
18 repl,
19 [
20 Run,
21 RunInPlace,
22 ClearOutputs,
23 Sessions,
24 Interrupt,
25 Shutdown,
26 Restart,
27 RefreshKernelspecs
28 ]
29);
30
31pub fn init(cx: &mut AppContext) {
32 cx.observe_new_views(
33 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
34 workspace.register_action(|workspace, _: &Sessions, cx| {
35 let existing = workspace
36 .active_pane()
37 .read(cx)
38 .items()
39 .find_map(|item| item.downcast::<ReplSessionsPage>());
40
41 if let Some(existing) = existing {
42 workspace.activate_item(&existing, true, true, cx);
43 } else {
44 let repl_sessions_page = ReplSessionsPage::new(cx);
45 workspace.add_item_to_active_pane(Box::new(repl_sessions_page), None, true, cx)
46 }
47 });
48
49 workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
50 let store = ReplStore::global(cx);
51 store.update(cx, |store, cx| {
52 store.refresh_kernelspecs(cx).detach();
53 });
54 });
55 },
56 )
57 .detach();
58
59 cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
60 if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
61 return;
62 }
63
64 let is_local_project = editor
65 .workspace()
66 .map(|workspace| workspace.read(cx).project().read(cx).is_local())
67 .unwrap_or(false);
68
69 if !is_local_project {
70 return;
71 }
72
73 let editor_handle = cx.view().downgrade();
74
75 editor
76 .register_action({
77 let editor_handle = editor_handle.clone();
78 move |_: &Run, cx| {
79 if !JupyterSettings::enabled(cx) {
80 return;
81 }
82
83 crate::run(editor_handle.clone(), true, cx).log_err();
84 }
85 })
86 .detach();
87
88 editor
89 .register_action({
90 let editor_handle = editor_handle.clone();
91 move |_: &RunInPlace, cx| {
92 if !JupyterSettings::enabled(cx) {
93 return;
94 }
95
96 crate::run(editor_handle.clone(), false, cx).log_err();
97 }
98 })
99 .detach();
100 })
101 .detach();
102}
103
104pub struct ReplSessionsPage {
105 focus_handle: FocusHandle,
106 _subscriptions: Vec<Subscription>,
107}
108
109impl ReplSessionsPage {
110 pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
111 cx.new_view(|cx: &mut ViewContext<Self>| {
112 let focus_handle = cx.focus_handle();
113
114 let subscriptions = vec![
115 cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
116 cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
117 ];
118
119 Self {
120 focus_handle,
121 _subscriptions: subscriptions,
122 }
123 })
124 }
125}
126
127impl EventEmitter<ItemEvent> for ReplSessionsPage {}
128
129impl FocusableView for ReplSessionsPage {
130 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
131 self.focus_handle.clone()
132 }
133}
134
135impl Item for ReplSessionsPage {
136 type Event = ItemEvent;
137
138 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
139 Some("REPL Sessions".into())
140 }
141
142 fn telemetry_event_text(&self) -> Option<&'static str> {
143 Some("repl sessions")
144 }
145
146 fn show_toolbar(&self) -> bool {
147 false
148 }
149
150 fn clone_on_split(
151 &self,
152 _workspace_id: Option<WorkspaceId>,
153 _: &mut ViewContext<Self>,
154 ) -> Option<View<Self>> {
155 None
156 }
157
158 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
159 f(*event)
160 }
161}
162
163impl Render for ReplSessionsPage {
164 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
165 let store = ReplStore::global(cx);
166
167 let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
168 (
169 store.kernel_specifications().cloned().collect::<Vec<_>>(),
170 store.sessions().cloned().collect::<Vec<_>>(),
171 )
172 });
173
174 // When there are no kernel specifications, show a link to the Zed docs explaining how to
175 // install kernels. It can be assumed they don't have a running kernel if we have no
176 // specifications.
177 if kernel_specifications.is_empty() {
178 let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
179
180 return ReplSessionsContainer::new("No Jupyter Kernels Available")
181 .child(Label::new(instructions))
182 .child(
183 h_flex().w_full().p_4().justify_center().gap_2().child(
184 ButtonLike::new("install-kernels")
185 .style(ButtonStyle::Filled)
186 .size(ButtonSize::Large)
187 .layer(ElevationIndex::ModalSurface)
188 .child(Label::new("Install Kernels"))
189 .on_click(move |_, cx| {
190 cx.open_url(
191 "https://zed.dev/docs/repl#language-specific-instructions",
192 )
193 }),
194 ),
195 );
196 }
197
198 let mut kernels_by_language: HashMap<SharedString, Vec<&KernelSpecification>> =
199 kernel_specifications
200 .iter()
201 .map(|spec| (spec.language(), spec))
202 .fold(HashMap::new(), |mut acc, (language, spec)| {
203 acc.entry(language).or_default().push(spec);
204 acc
205 });
206
207 for kernels in kernels_by_language.values_mut() {
208 kernels.sort_by_key(|a| a.name())
209 }
210
211 // Convert to a sorted Vec of tuples
212 let mut sorted_kernels: Vec<(SharedString, Vec<&KernelSpecification>)> =
213 kernels_by_language.into_iter().collect();
214 sorted_kernels.sort_by(|a, b| a.0.cmp(&b.0));
215
216 let kernels_available = v_flex()
217 .child(Label::new("Kernels available").size(LabelSize::Large))
218 .gap_2()
219 .child(
220 h_flex()
221 .child(Label::new(
222 "Defaults indicated with a checkmark. Learn how to change your default kernel in the ",
223 ))
224 .child(
225 ButtonLike::new("configure-kernels")
226 .style(ButtonStyle::Filled)
227 // .size(ButtonSize::Compact)
228 .layer(ElevationIndex::Surface)
229 .child(Label::new("REPL documentation"))
230 .child(Icon::new(IconName::Link))
231 .on_click(move |_, cx| {
232 cx.open_url(KERNEL_DOCS_URL)
233 }),
234 ),
235 )
236 .children(sorted_kernels.into_iter().map(|(language, specs)| {
237 let chosen_kernel = store.read(cx).kernelspec(&language, cx);
238
239 v_flex()
240 .gap_1()
241 .child(Label::new(language.clone()).weight(FontWeight::BOLD))
242 .children(specs.into_iter().map(|spec| {
243 let is_choice = if let Some(chosen_kernel) = &chosen_kernel {
244 chosen_kernel == spec
245 } else {
246 false
247 };
248
249 let path = spec.path();
250
251 ListItem::new(path.clone())
252 .selectable(false)
253 .tooltip({
254 let path = path.clone();
255 move |cx| Tooltip::text(path.clone(), cx)})
256 .child(
257 h_flex()
258 .gap_1()
259 .child(div().id(path.clone()).child(Label::new(spec.name())))
260 .when(is_choice, |el| {
261
262 let language = language.clone();
263
264 el.child(
265
266 div().id("check").tooltip(move |cx| Tooltip::text(format!("Default Kernel for {language}"), cx))
267 .child(Icon::new(IconName::Check)))}),
268 )
269
270 }))
271 }));
272
273 // When there are no sessions, show the command to run code in an editor
274 if sessions.is_empty() {
275 let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
276
277 return ReplSessionsContainer::new("No Jupyter Kernel Sessions")
278 .child(
279 v_flex()
280 .child(Label::new(instructions))
281 .children(KeyBinding::for_action(&Run, cx)),
282 )
283 .child(div().pt_3().child(kernels_available));
284 }
285
286 ReplSessionsContainer::new("Jupyter Kernel Sessions")
287 .children(sessions)
288 .child(kernels_available)
289 }
290}
291
292#[derive(IntoElement)]
293struct ReplSessionsContainer {
294 title: SharedString,
295 children: Vec<AnyElement>,
296}
297
298impl ReplSessionsContainer {
299 pub fn new(title: impl Into<SharedString>) -> Self {
300 Self {
301 title: title.into(),
302 children: Vec::new(),
303 }
304 }
305}
306
307impl ParentElement for ReplSessionsContainer {
308 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
309 self.children.extend(elements)
310 }
311}
312
313impl RenderOnce for ReplSessionsContainer {
314 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
315 v_flex()
316 .p_4()
317 .gap_2()
318 .size_full()
319 .child(Label::new(self.title).size(LabelSize::Large))
320 .children(self.children)
321 }
322}