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