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