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