1//! REPL operations on an [`Editor`].
2
3use std::ops::Range;
4use std::sync::Arc;
5
6use anyhow::{Context, Result};
7use editor::Editor;
8use gpui::{prelude::*, AppContext, Entity, View, WeakView, WindowContext};
9use language::{BufferSnapshot, Language, Point};
10
11use crate::repl_store::ReplStore;
12use crate::session::SessionEvent;
13use crate::{KernelSpecification, Session};
14
15pub fn run(editor: WeakView<Editor>, cx: &mut WindowContext) -> Result<()> {
16 let store = ReplStore::global(cx);
17 if !store.read(cx).is_enabled() {
18 return Ok(());
19 }
20
21 let editor = editor.upgrade().context("editor was dropped")?;
22 let selected_range = editor
23 .update(cx, |editor, cx| editor.selections.newest_adjusted(cx))
24 .range();
25 let multibuffer = editor.read(cx).buffer().clone();
26 let Some(buffer) = multibuffer.read(cx).as_singleton() else {
27 return Ok(());
28 };
29
30 for range in snippet_ranges(&buffer.read(cx).snapshot(), selected_range) {
31 let Some(language) = multibuffer.read(cx).language_at(range.start, cx) else {
32 continue;
33 };
34
35 let kernel_specification = store.update(cx, |store, cx| {
36 store
37 .kernelspec(&language, cx)
38 .with_context(|| format!("No kernel found for language: {}", language.name()))
39 })?;
40
41 let fs = store.read(cx).fs().clone();
42 let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
43 {
44 session
45 } else {
46 let weak_editor = editor.downgrade();
47 let session = cx.new_view(|cx| Session::new(weak_editor, fs, kernel_specification, cx));
48
49 editor.update(cx, |_editor, cx| {
50 cx.notify();
51
52 cx.subscribe(&session, {
53 let store = store.clone();
54 move |_this, _session, event, cx| match event {
55 SessionEvent::Shutdown(shutdown_event) => {
56 store.update(cx, |store, _cx| {
57 store.remove_session(shutdown_event.entity_id());
58 });
59 }
60 }
61 })
62 .detach();
63 });
64
65 store.update(cx, |store, _cx| {
66 store.insert_session(editor.entity_id(), session.clone());
67 });
68
69 session
70 };
71
72 let selected_text;
73 let anchor_range;
74 {
75 let snapshot = multibuffer.read(cx).read(cx);
76 selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
77 anchor_range = snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end);
78 }
79
80 session.update(cx, |session, cx| {
81 session.execute(selected_text, anchor_range, cx);
82 });
83 }
84
85 anyhow::Ok(())
86}
87
88pub enum SessionSupport {
89 ActiveSession(View<Session>),
90 Inactive(Box<KernelSpecification>),
91 RequiresSetup(Arc<str>),
92 Unsupported,
93}
94
95pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> SessionSupport {
96 let store = ReplStore::global(cx);
97 let entity_id = editor.entity_id();
98
99 if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
100 return SessionSupport::ActiveSession(session);
101 };
102
103 let Some(language) = get_language(editor, cx) else {
104 return SessionSupport::Unsupported;
105 };
106 let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
107
108 match kernelspec {
109 Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
110 None => match language.name().as_ref() {
111 "TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
112 _ => SessionSupport::Unsupported,
113 },
114 }
115}
116
117pub fn clear_outputs(editor: WeakView<Editor>, cx: &mut WindowContext) {
118 let store = ReplStore::global(cx);
119 let entity_id = editor.entity_id();
120 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
121 return;
122 };
123 session.update(cx, |session, cx| {
124 session.clear_outputs(cx);
125 cx.notify();
126 });
127}
128
129pub fn interrupt(editor: WeakView<Editor>, cx: &mut WindowContext) {
130 let store = ReplStore::global(cx);
131 let entity_id = editor.entity_id();
132 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
133 return;
134 };
135
136 session.update(cx, |session, cx| {
137 session.interrupt(cx);
138 cx.notify();
139 });
140}
141
142pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
143 let store = ReplStore::global(cx);
144 let entity_id = editor.entity_id();
145 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
146 return;
147 };
148
149 session.update(cx, |session, cx| {
150 session.shutdown(cx);
151 cx.notify();
152 });
153}
154
155fn snippet_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
156 let mut snippet_end_row = end_row;
157 while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
158 snippet_end_row -= 1;
159 }
160 Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
161}
162
163fn jupytext_snippets(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Range<Point>> {
164 let mut current_row = range.start.row;
165
166 let Some(language) = buffer.language() else {
167 return Vec::new();
168 };
169
170 let default_scope = language.default_scope();
171 let comment_prefixes = default_scope.line_comment_prefixes();
172 if comment_prefixes.is_empty() {
173 return Vec::new();
174 }
175
176 let jupytext_prefixes = comment_prefixes
177 .iter()
178 .map(|comment_prefix| format!("{comment_prefix}%%"))
179 .collect::<Vec<_>>();
180
181 let mut snippet_start_row = None;
182 loop {
183 if jupytext_prefixes
184 .iter()
185 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
186 {
187 snippet_start_row = Some(current_row);
188 break;
189 } else if current_row > 0 {
190 current_row -= 1;
191 } else {
192 break;
193 }
194 }
195
196 let mut snippets = Vec::new();
197 if let Some(mut snippet_start_row) = snippet_start_row {
198 for current_row in range.start.row + 1..=buffer.max_point().row {
199 if jupytext_prefixes
200 .iter()
201 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
202 {
203 snippets.push(snippet_range(buffer, snippet_start_row, current_row - 1));
204
205 if current_row <= range.end.row {
206 snippet_start_row = current_row;
207 } else {
208 return snippets;
209 }
210 }
211 }
212
213 snippets.push(snippet_range(
214 buffer,
215 snippet_start_row,
216 buffer.max_point().row,
217 ));
218 }
219
220 snippets
221}
222
223fn snippet_ranges(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Range<Point>> {
224 let jupytext_snippets = jupytext_snippets(buffer, range.clone());
225 if !jupytext_snippets.is_empty() {
226 return jupytext_snippets;
227 }
228
229 let snippet_range = snippet_range(buffer, range.start.row, range.end.row);
230 let start_language = buffer.language_at(snippet_range.start);
231 let end_language = buffer.language_at(snippet_range.end);
232
233 if let Some((start, end)) = start_language.zip(end_language) {
234 if start == end {
235 return vec![snippet_range];
236 }
237 }
238
239 Vec::new()
240}
241
242fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Language>> {
243 let editor = editor.upgrade()?;
244 let selection = editor.read(cx).selections.newest::<usize>(cx);
245 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
246 buffer.language_at(selection.head()).cloned()
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use gpui::Context;
253 use indoc::indoc;
254 use language::{Buffer, Language, LanguageConfig};
255
256 #[gpui::test]
257 fn test_snippet_ranges(cx: &mut AppContext) {
258 // Create a test language
259 let test_language = Arc::new(Language::new(
260 LanguageConfig {
261 name: "TestLang".into(),
262 line_comments: vec!["# ".into()],
263 ..Default::default()
264 },
265 None,
266 ));
267
268 let buffer = cx.new_model(|cx| {
269 Buffer::local(
270 indoc! { r#"
271 print(1 + 1)
272 print(2 + 2)
273
274 print(4 + 4)
275
276
277 "# },
278 cx,
279 )
280 .with_language(test_language, cx)
281 });
282 let snapshot = buffer.read(cx).snapshot();
283
284 // Single-point selection
285 let snippets = snippet_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4))
286 .into_iter()
287 .map(|range| snapshot.text_for_range(range).collect::<String>())
288 .collect::<Vec<_>>();
289 assert_eq!(snippets, vec!["print(1 + 1)"]);
290
291 // Multi-line selection
292 let snippets = snippet_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0))
293 .into_iter()
294 .map(|range| snapshot.text_for_range(range).collect::<String>())
295 .collect::<Vec<_>>();
296 assert_eq!(
297 snippets,
298 vec![indoc! { r#"
299 print(1 + 1)
300 print(2 + 2)"# }]
301 );
302
303 // Trimming multiple trailing blank lines
304 let snippets = snippet_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0))
305 .into_iter()
306 .map(|range| snapshot.text_for_range(range).collect::<String>())
307 .collect::<Vec<_>>();
308 assert_eq!(
309 snippets,
310 vec![indoc! { r#"
311 print(1 + 1)
312 print(2 + 2)
313
314 print(4 + 4)"# }]
315 );
316 }
317
318 #[gpui::test]
319 fn test_jupytext_snippet_ranges(cx: &mut AppContext) {
320 // Create a test language
321 let test_language = Arc::new(Language::new(
322 LanguageConfig {
323 name: "TestLang".into(),
324 line_comments: vec!["# ".into()],
325 ..Default::default()
326 },
327 None,
328 ));
329
330 let buffer = cx.new_model(|cx| {
331 Buffer::local(
332 indoc! { r#"
333 # Hello!
334 # %% [markdown]
335 # This is some arithmetic
336 print(1 + 1)
337 print(2 + 2)
338
339 # %%
340 print(3 + 3)
341 print(4 + 4)
342
343 print(5 + 5)
344
345
346
347 "# },
348 cx,
349 )
350 .with_language(test_language, cx)
351 });
352 let snapshot = buffer.read(cx).snapshot();
353
354 // Jupytext snippet surrounding an empty selection
355 let snippets = snippet_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5))
356 .into_iter()
357 .map(|range| snapshot.text_for_range(range).collect::<String>())
358 .collect::<Vec<_>>();
359 assert_eq!(
360 snippets,
361 vec![indoc! { r#"
362 # %% [markdown]
363 # This is some arithmetic
364 print(1 + 1)
365 print(2 + 2)"# }]
366 );
367
368 // Jupytext snippets intersecting a non-empty selection
369 let snippets = snippet_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2))
370 .into_iter()
371 .map(|range| snapshot.text_for_range(range).collect::<String>())
372 .collect::<Vec<_>>();
373 assert_eq!(
374 snippets,
375 vec![
376 indoc! { r#"
377 # %% [markdown]
378 # This is some arithmetic
379 print(1 + 1)
380 print(2 + 2)"#
381 },
382 indoc! { r#"
383 # %%
384 print(3 + 3)
385 print(4 + 4)
386
387 print(5 + 5)"#
388 }
389 ]
390 );
391 }
392}