1use editor::Editor;
2use gpui::{
3 AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, actions,
4 prelude::*,
5};
6use project::ProjectItem as _;
7use ui::{ButtonLike, ElevationIndex, KeyBinding, prelude::*};
8use util::ResultExt as _;
9use workspace::WorkspaceId;
10use workspace::item::ItemEvent;
11use workspace::{Workspace, item::Item};
12
13use crate::jupyter_settings::JupyterSettings;
14use crate::repl_store::ReplStore;
15
16actions!(
17 repl,
18 [
19 /// Runs the current cell and advances to the next one.
20 Run,
21 /// Runs the current cell without advancing.
22 RunInPlace,
23 /// Clears all outputs in the REPL.
24 ClearOutputs,
25 /// Opens the REPL sessions panel.
26 Sessions,
27 /// Interrupts the currently running kernel.
28 Interrupt,
29 /// Shuts down the current kernel.
30 Shutdown,
31 /// Restarts the current kernel.
32 Restart,
33 /// Refreshes the list of available kernelspecs.
34 RefreshKernelspecs
35 ]
36);
37
38pub fn init(cx: &mut App) {
39 cx.observe_new(
40 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
41 workspace.register_action(|workspace, _: &Sessions, window, cx| {
42 let existing = workspace
43 .active_pane()
44 .read(cx)
45 .items()
46 .find_map(|item| item.downcast::<ReplSessionsPage>());
47
48 if let Some(existing) = existing {
49 workspace.activate_item(&existing, true, true, window, cx);
50 } else {
51 let repl_sessions_page = ReplSessionsPage::new(window, cx);
52 workspace.add_item_to_active_pane(
53 Box::new(repl_sessions_page),
54 None,
55 true,
56 window,
57 cx,
58 )
59 }
60 });
61
62 workspace.register_action(|_workspace, _: &RefreshKernelspecs, _, cx| {
63 let store = ReplStore::global(cx);
64 store.update(cx, |store, cx| {
65 store.refresh_kernelspecs(cx).detach();
66 });
67 });
68 },
69 )
70 .detach();
71
72 cx.observe_new(
73 move |editor: &mut Editor, window, cx: &mut Context<Editor>| {
74 let Some(window) = window else {
75 return;
76 };
77
78 if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
79 return;
80 }
81
82 cx.defer_in(window, |editor, window, cx| {
83 let workspace = Workspace::for_window(window, cx);
84 let project = workspace.map(|workspace| workspace.read(cx).project().clone());
85
86 let is_local_project = project
87 .as_ref()
88 .map(|project| project.read(cx).is_local())
89 .unwrap_or(false);
90
91 if !is_local_project {
92 return;
93 }
94
95 let buffer = editor.buffer().read(cx).as_singleton();
96
97 let language = buffer
98 .as_ref()
99 .and_then(|buffer| buffer.read(cx).language());
100
101 let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx));
102
103 let editor_handle = cx.entity().downgrade();
104
105 if let Some(language) = language {
106 if language.name() == "Python".into() {
107 if let (Some(project_path), Some(project)) = (project_path, project) {
108 let store = ReplStore::global(cx);
109 store.update(cx, |store, cx| {
110 store
111 .refresh_python_kernelspecs(
112 project_path.worktree_id,
113 &project,
114 cx,
115 )
116 .detach_and_log_err(cx);
117 });
118 }
119 }
120 }
121
122 editor
123 .register_action({
124 let editor_handle = editor_handle.clone();
125 move |_: &Run, window, cx| {
126 if !JupyterSettings::enabled(cx) {
127 return;
128 }
129
130 crate::run(editor_handle.clone(), true, window, cx).log_err();
131 }
132 })
133 .detach();
134
135 editor
136 .register_action({
137 let editor_handle = editor_handle.clone();
138 move |_: &RunInPlace, window, cx| {
139 if !JupyterSettings::enabled(cx) {
140 return;
141 }
142
143 crate::run(editor_handle.clone(), false, window, cx).log_err();
144 }
145 })
146 .detach();
147 });
148 },
149 )
150 .detach();
151}
152
153pub struct ReplSessionsPage {
154 focus_handle: FocusHandle,
155 _subscriptions: Vec<Subscription>,
156}
157
158impl ReplSessionsPage {
159 pub fn new(window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> {
160 cx.new(|cx| {
161 let focus_handle = cx.focus_handle();
162
163 let subscriptions = vec![
164 cx.on_focus_in(&focus_handle, window, |_this, _window, cx| cx.notify()),
165 cx.on_focus_out(&focus_handle, window, |_this, _event, _window, cx| {
166 cx.notify()
167 }),
168 ];
169
170 Self {
171 focus_handle,
172 _subscriptions: subscriptions,
173 }
174 })
175 }
176}
177
178impl EventEmitter<ItemEvent> for ReplSessionsPage {}
179
180impl Focusable for ReplSessionsPage {
181 fn focus_handle(&self, _cx: &App) -> FocusHandle {
182 self.focus_handle.clone()
183 }
184}
185
186impl Item for ReplSessionsPage {
187 type Event = ItemEvent;
188
189 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
190 "REPL Sessions".into()
191 }
192
193 fn telemetry_event_text(&self) -> Option<&'static str> {
194 Some("REPL Session Started")
195 }
196
197 fn show_toolbar(&self) -> bool {
198 false
199 }
200
201 fn clone_on_split(
202 &self,
203 _workspace_id: Option<WorkspaceId>,
204 _window: &mut Window,
205 _: &mut Context<Self>,
206 ) -> Option<Entity<Self>> {
207 None
208 }
209
210 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
211 f(*event)
212 }
213}
214
215impl Render for ReplSessionsPage {
216 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
217 let store = ReplStore::global(cx);
218
219 let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
220 (
221 store
222 .pure_jupyter_kernel_specifications()
223 .cloned()
224 .collect::<Vec<_>>(),
225 store.sessions().cloned().collect::<Vec<_>>(),
226 )
227 });
228
229 // When there are no kernel specifications, show a link to the Zed docs explaining how to
230 // install kernels. It can be assumed they don't have a running kernel if we have no
231 // specifications.
232 if kernel_specifications.is_empty() {
233 let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
234
235 return ReplSessionsContainer::new("No Jupyter Kernels Available")
236 .child(Label::new(instructions))
237 .child(
238 h_flex().w_full().p_4().justify_center().gap_2().child(
239 ButtonLike::new("install-kernels")
240 .style(ButtonStyle::Filled)
241 .size(ButtonSize::Large)
242 .layer(ElevationIndex::ModalSurface)
243 .child(Label::new("Install Kernels"))
244 .on_click(move |_, _, cx| {
245 cx.open_url(
246 "https://zed.dev/docs/repl#language-specific-instructions",
247 )
248 }),
249 ),
250 );
251 }
252
253 // When there are no sessions, show the command to run code in an editor
254 if sessions.is_empty() {
255 let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
256
257 return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child(
258 v_flex()
259 .child(Label::new(instructions))
260 .children(KeyBinding::for_action(&Run, window, cx)),
261 );
262 }
263
264 ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions)
265 }
266}
267
268#[derive(IntoElement)]
269struct ReplSessionsContainer {
270 title: SharedString,
271 children: Vec<AnyElement>,
272}
273
274impl ReplSessionsContainer {
275 pub fn new(title: impl Into<SharedString>) -> Self {
276 Self {
277 title: title.into(),
278 children: Vec::new(),
279 }
280 }
281}
282
283impl ParentElement for ReplSessionsContainer {
284 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
285 self.children.extend(elements)
286 }
287}
288
289impl RenderOnce for ReplSessionsContainer {
290 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
291 v_flex()
292 .p_4()
293 .gap_2()
294 .size_full()
295 .child(Label::new(self.title).size(LabelSize::Large))
296 .children(self.children)
297 }
298}