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 (runnable_ranges, next_cell_point) =
31 runnable_ranges(&buffer.read(cx).snapshot(), selected_range);
32
33 for runnable_range in runnable_ranges {
34 let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else {
35 continue;
36 };
37
38 let kernel_specification = store.update(cx, |store, cx| {
39 store
40 .kernelspec(&language, cx)
41 .with_context(|| format!("No kernel found for language: {}", language.name()))
42 })?;
43
44 let fs = store.read(cx).fs().clone();
45 let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
46 {
47 session
48 } else {
49 let weak_editor = editor.downgrade();
50 let session = cx.new_view(|cx| Session::new(weak_editor, fs, kernel_specification, cx));
51
52 editor.update(cx, |_editor, cx| {
53 cx.notify();
54
55 cx.subscribe(&session, {
56 let store = store.clone();
57 move |_this, _session, event, cx| match event {
58 SessionEvent::Shutdown(shutdown_event) => {
59 store.update(cx, |store, _cx| {
60 store.remove_session(shutdown_event.entity_id());
61 });
62 }
63 }
64 })
65 .detach();
66 });
67
68 store.update(cx, |store, _cx| {
69 store.insert_session(editor.entity_id(), session.clone());
70 });
71
72 session
73 };
74
75 let selected_text;
76 let anchor_range;
77 let next_cursor;
78 {
79 let snapshot = multibuffer.read(cx).read(cx);
80 selected_text = snapshot
81 .text_for_range(runnable_range.clone())
82 .collect::<String>();
83 anchor_range = snapshot.anchor_before(runnable_range.start)
84 ..snapshot.anchor_after(runnable_range.end);
85 next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point));
86 }
87
88 session.update(cx, |session, cx| {
89 session.execute(selected_text, anchor_range, next_cursor, cx);
90 });
91 }
92
93 anyhow::Ok(())
94}
95
96pub enum SessionSupport {
97 ActiveSession(View<Session>),
98 Inactive(Box<KernelSpecification>),
99 RequiresSetup(Arc<str>),
100 Unsupported,
101}
102
103pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> SessionSupport {
104 let store = ReplStore::global(cx);
105 let entity_id = editor.entity_id();
106
107 if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
108 return SessionSupport::ActiveSession(session);
109 };
110
111 let Some(language) = get_language(editor, cx) else {
112 return SessionSupport::Unsupported;
113 };
114 let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
115
116 match kernelspec {
117 Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
118 None => {
119 if language_supported(&language) {
120 SessionSupport::RequiresSetup(language.name())
121 } else {
122 SessionSupport::Unsupported
123 }
124 }
125 }
126}
127
128pub fn clear_outputs(editor: WeakView<Editor>, cx: &mut WindowContext) {
129 let store = ReplStore::global(cx);
130 let entity_id = editor.entity_id();
131 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
132 return;
133 };
134 session.update(cx, |session, cx| {
135 session.clear_outputs(cx);
136 cx.notify();
137 });
138}
139
140pub fn interrupt(editor: WeakView<Editor>, cx: &mut WindowContext) {
141 let store = ReplStore::global(cx);
142 let entity_id = editor.entity_id();
143 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
144 return;
145 };
146
147 session.update(cx, |session, cx| {
148 session.interrupt(cx);
149 cx.notify();
150 });
151}
152
153pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
154 let store = ReplStore::global(cx);
155 let entity_id = editor.entity_id();
156 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
157 return;
158 };
159
160 session.update(cx, |session, cx| {
161 session.shutdown(cx);
162 cx.notify();
163 });
164}
165
166fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
167 let mut snippet_end_row = end_row;
168 while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
169 snippet_end_row -= 1;
170 }
171 Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
172}
173
174// Returns the ranges of the snippets in the buffer and the next point for moving the cursor to
175fn jupytext_cells(
176 buffer: &BufferSnapshot,
177 range: Range<Point>,
178) -> (Vec<Range<Point>>, Option<Point>) {
179 let mut current_row = range.start.row;
180
181 let Some(language) = buffer.language() else {
182 return (Vec::new(), None);
183 };
184
185 let default_scope = language.default_scope();
186 let comment_prefixes = default_scope.line_comment_prefixes();
187 if comment_prefixes.is_empty() {
188 return (Vec::new(), None);
189 }
190
191 let jupytext_prefixes = comment_prefixes
192 .iter()
193 .map(|comment_prefix| format!("{comment_prefix}%%"))
194 .collect::<Vec<_>>();
195
196 let mut snippet_start_row = None;
197 loop {
198 if jupytext_prefixes
199 .iter()
200 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
201 {
202 snippet_start_row = Some(current_row);
203 break;
204 } else if current_row > 0 {
205 current_row -= 1;
206 } else {
207 break;
208 }
209 }
210
211 let mut snippets = Vec::new();
212 if let Some(mut snippet_start_row) = snippet_start_row {
213 for current_row in range.start.row + 1..=buffer.max_point().row {
214 if jupytext_prefixes
215 .iter()
216 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
217 {
218 snippets.push(cell_range(buffer, snippet_start_row, current_row - 1));
219
220 if current_row <= range.end.row {
221 snippet_start_row = current_row;
222 } else {
223 // Return our snippets as well as the next point for moving the cursor to
224 return (snippets, Some(Point::new(current_row, 0)));
225 }
226 }
227 }
228
229 // Go to the end of the buffer (no more jupytext cells found)
230 snippets.push(cell_range(
231 buffer,
232 snippet_start_row,
233 buffer.max_point().row,
234 ));
235 }
236
237 (snippets, None)
238}
239
240fn runnable_ranges(
241 buffer: &BufferSnapshot,
242 range: Range<Point>,
243) -> (Vec<Range<Point>>, Option<Point>) {
244 if let Some(language) = buffer.language() {
245 if language.name().as_ref() == "Markdown" {
246 return (markdown_code_blocks(buffer, range.clone()), None);
247 }
248 }
249
250 let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone());
251 if !jupytext_snippets.is_empty() {
252 return (jupytext_snippets, next_cursor);
253 }
254
255 let snippet_range = cell_range(buffer, range.start.row, range.end.row);
256 let start_language = buffer.language_at(snippet_range.start);
257 let end_language = buffer.language_at(snippet_range.end);
258
259 if start_language
260 .zip(end_language)
261 .map_or(false, |(start, end)| start == end)
262 {
263 (vec![snippet_range], None)
264 } else {
265 (Vec::new(), None)
266 }
267}
268
269// We allow markdown code blocks to end in a trailing newline in order to render the output
270// below the final code fence. This is different than our behavior for selections and Jupytext cells.
271fn markdown_code_blocks(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Range<Point>> {
272 buffer
273 .injections_intersecting_range(range)
274 .filter(|(_, language)| language_supported(language))
275 .map(|(content_range, _)| {
276 buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
277 })
278 .collect()
279}
280
281fn language_supported(language: &Arc<Language>) -> bool {
282 match language.name().as_ref() {
283 "TypeScript" | "Python" => true,
284 _ => false,
285 }
286}
287
288fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Language>> {
289 let editor = editor.upgrade()?;
290 let selection = editor.read(cx).selections.newest::<usize>(cx);
291 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
292 buffer.language_at(selection.head()).cloned()
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use gpui::{Context, Task};
299 use indoc::indoc;
300 use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
301
302 #[gpui::test]
303 fn test_snippet_ranges(cx: &mut AppContext) {
304 // Create a test language
305 let test_language = Arc::new(Language::new(
306 LanguageConfig {
307 name: "TestLang".into(),
308 line_comments: vec!["# ".into()],
309 ..Default::default()
310 },
311 None,
312 ));
313
314 let buffer = cx.new_model(|cx| {
315 Buffer::local(
316 indoc! { r#"
317 print(1 + 1)
318 print(2 + 2)
319
320 print(4 + 4)
321
322
323 "# },
324 cx,
325 )
326 .with_language(test_language, cx)
327 });
328 let snapshot = buffer.read(cx).snapshot();
329
330 // Single-point selection
331 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
332 let snippets = snippets
333 .into_iter()
334 .map(|range| snapshot.text_for_range(range).collect::<String>())
335 .collect::<Vec<_>>();
336 assert_eq!(snippets, vec!["print(1 + 1)"]);
337
338 // Multi-line selection
339 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
340 let snippets = snippets
341 .into_iter()
342 .map(|range| snapshot.text_for_range(range).collect::<String>())
343 .collect::<Vec<_>>();
344 assert_eq!(
345 snippets,
346 vec![indoc! { r#"
347 print(1 + 1)
348 print(2 + 2)"# }]
349 );
350
351 // Trimming multiple trailing blank lines
352 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
353
354 let snippets = snippets
355 .into_iter()
356 .map(|range| snapshot.text_for_range(range).collect::<String>())
357 .collect::<Vec<_>>();
358 assert_eq!(
359 snippets,
360 vec![indoc! { r#"
361 print(1 + 1)
362 print(2 + 2)
363
364 print(4 + 4)"# }]
365 );
366 }
367
368 #[gpui::test]
369 fn test_jupytext_snippet_ranges(cx: &mut AppContext) {
370 // Create a test language
371 let test_language = Arc::new(Language::new(
372 LanguageConfig {
373 name: "TestLang".into(),
374 line_comments: vec!["# ".into()],
375 ..Default::default()
376 },
377 None,
378 ));
379
380 let buffer = cx.new_model(|cx| {
381 Buffer::local(
382 indoc! { r#"
383 # Hello!
384 # %% [markdown]
385 # This is some arithmetic
386 print(1 + 1)
387 print(2 + 2)
388
389 # %%
390 print(3 + 3)
391 print(4 + 4)
392
393 print(5 + 5)
394
395
396
397 "# },
398 cx,
399 )
400 .with_language(test_language, cx)
401 });
402 let snapshot = buffer.read(cx).snapshot();
403
404 // Jupytext snippet surrounding an empty selection
405 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
406
407 let snippets = snippets
408 .into_iter()
409 .map(|range| snapshot.text_for_range(range).collect::<String>())
410 .collect::<Vec<_>>();
411 assert_eq!(
412 snippets,
413 vec![indoc! { r#"
414 # %% [markdown]
415 # This is some arithmetic
416 print(1 + 1)
417 print(2 + 2)"# }]
418 );
419
420 // Jupytext snippets intersecting a non-empty selection
421 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
422 let snippets = snippets
423 .into_iter()
424 .map(|range| snapshot.text_for_range(range).collect::<String>())
425 .collect::<Vec<_>>();
426 assert_eq!(
427 snippets,
428 vec![
429 indoc! { r#"
430 # %% [markdown]
431 # This is some arithmetic
432 print(1 + 1)
433 print(2 + 2)"#
434 },
435 indoc! { r#"
436 # %%
437 print(3 + 3)
438 print(4 + 4)
439
440 print(5 + 5)"#
441 }
442 ]
443 );
444 }
445
446 #[gpui::test]
447 fn test_markdown_code_blocks(cx: &mut AppContext) {
448 let markdown = languages::language("markdown", tree_sitter_md::language());
449 let typescript =
450 languages::language("typescript", tree_sitter_typescript::language_typescript());
451 let python = languages::language("python", tree_sitter_python::language());
452 let language_registry = Arc::new(LanguageRegistry::new(
453 Task::ready(()),
454 cx.background_executor().clone(),
455 ));
456 language_registry.add(markdown.clone());
457 language_registry.add(typescript.clone());
458 language_registry.add(python.clone());
459
460 // Two code blocks intersecting with selection
461 let buffer = cx.new_model(|cx| {
462 let mut buffer = Buffer::local(
463 indoc! { r#"
464 Hey this is Markdown!
465
466 ```typescript
467 let foo = 999;
468 console.log(foo + 1999);
469 ```
470
471 ```typescript
472 console.log("foo")
473 ```
474 "#
475 },
476 cx,
477 );
478 buffer.set_language_registry(language_registry.clone());
479 buffer.set_language(Some(markdown.clone()), cx);
480 buffer
481 });
482 let snapshot = buffer.read(cx).snapshot();
483
484 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
485 let snippets = snippets
486 .into_iter()
487 .map(|range| snapshot.text_for_range(range).collect::<String>())
488 .collect::<Vec<_>>();
489
490 assert_eq!(
491 snippets,
492 vec![
493 indoc! { r#"
494 let foo = 999;
495 console.log(foo + 1999);
496 "#
497 },
498 "console.log(\"foo\")\n"
499 ]
500 );
501
502 // Three code blocks intersecting with selection
503 let buffer = cx.new_model(|cx| {
504 let mut buffer = Buffer::local(
505 indoc! { r#"
506 Hey this is Markdown!
507
508 ```typescript
509 let foo = 999;
510 console.log(foo + 1999);
511 ```
512
513 ```ts
514 console.log("foo")
515 ```
516
517 ```typescript
518 console.log("another code block")
519 ```
520 "# },
521 cx,
522 );
523 buffer.set_language_registry(language_registry.clone());
524 buffer.set_language(Some(markdown.clone()), cx);
525 buffer
526 });
527 let snapshot = buffer.read(cx).snapshot();
528
529 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
530 let snippets = snippets
531 .into_iter()
532 .map(|range| snapshot.text_for_range(range).collect::<String>())
533 .collect::<Vec<_>>();
534
535 assert_eq!(
536 snippets,
537 vec![
538 indoc! { r#"
539 let foo = 999;
540 console.log(foo + 1999);
541 "#
542 },
543 "console.log(\"foo\")\n",
544 "console.log(\"another code block\")\n",
545 ]
546 );
547
548 // Python code block
549 let buffer = cx.new_model(|cx| {
550 let mut buffer = Buffer::local(
551 indoc! { r#"
552 Hey this is Markdown!
553
554 ```python
555 print("hello there")
556 print("hello there")
557 print("hello there")
558 ```
559 "# },
560 cx,
561 );
562 buffer.set_language_registry(language_registry.clone());
563 buffer.set_language(Some(markdown.clone()), cx);
564 buffer
565 });
566 let snapshot = buffer.read(cx).snapshot();
567
568 let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
569 let snippets = snippets
570 .into_iter()
571 .map(|range| snapshot.text_for_range(range).collect::<String>())
572 .collect::<Vec<_>>();
573
574 assert_eq!(
575 snippets,
576 vec![indoc! { r#"
577 print("hello there")
578 print("hello there")
579 print("hello there")
580 "#
581 },]
582 );
583 }
584}