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