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