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