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::*, 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 WindowContext) -> 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 WindowContext) -> Option<Arc<Language>> {
315 editor
316 .update(cx, |editor, cx| {
317 let selection = editor.selections.newest::<usize>(cx);
318 let buffer = editor.buffer().read(cx).snapshot(cx);
319 buffer.language_at(selection.head()).cloned()
320 })
321 .ok()
322 .flatten()
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use gpui::{AppContext, Context};
329 use indoc::indoc;
330 use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
331
332 #[gpui::test]
333 fn test_snippet_ranges(cx: &mut AppContext) {
334 // Create a test language
335 let test_language = Arc::new(Language::new(
336 LanguageConfig {
337 name: "TestLang".into(),
338 line_comments: vec!["# ".into()],
339 ..Default::default()
340 },
341 None,
342 ));
343
344 let buffer = cx.new_model(|cx| {
345 Buffer::local(
346 indoc! { r#"
347 print(1 + 1)
348 print(2 + 2)
349
350 print(4 + 4)
351
352
353 "# },
354 cx,
355 )
356 .with_language(test_language, cx)
357 });
358 let snapshot = buffer.read(cx).snapshot();
359
360 // Single-point selection
361 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
362 let snippets = snippets
363 .into_iter()
364 .map(|range| snapshot.text_for_range(range).collect::<String>())
365 .collect::<Vec<_>>();
366 assert_eq!(snippets, vec!["print(1 + 1)"]);
367
368 // Multi-line selection
369 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
370 let snippets = snippets
371 .into_iter()
372 .map(|range| snapshot.text_for_range(range).collect::<String>())
373 .collect::<Vec<_>>();
374 assert_eq!(
375 snippets,
376 vec![indoc! { r#"
377 print(1 + 1)
378 print(2 + 2)"# }]
379 );
380
381 // Trimming multiple trailing blank lines
382 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
383
384 let snippets = snippets
385 .into_iter()
386 .map(|range| snapshot.text_for_range(range).collect::<String>())
387 .collect::<Vec<_>>();
388 assert_eq!(
389 snippets,
390 vec![indoc! { r#"
391 print(1 + 1)
392 print(2 + 2)
393
394 print(4 + 4)"# }]
395 );
396 }
397
398 #[gpui::test]
399 fn test_jupytext_snippet_ranges(cx: &mut AppContext) {
400 // Create a test language
401 let test_language = Arc::new(Language::new(
402 LanguageConfig {
403 name: "TestLang".into(),
404 line_comments: vec!["# ".into()],
405 ..Default::default()
406 },
407 None,
408 ));
409
410 let buffer = cx.new_model(|cx| {
411 Buffer::local(
412 indoc! { r#"
413 # Hello!
414 # %% [markdown]
415 # This is some arithmetic
416 print(1 + 1)
417 print(2 + 2)
418
419 # %%
420 print(3 + 3)
421 print(4 + 4)
422
423 print(5 + 5)
424
425
426
427 "# },
428 cx,
429 )
430 .with_language(test_language, cx)
431 });
432 let snapshot = buffer.read(cx).snapshot();
433
434 // Jupytext snippet surrounding an empty selection
435 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
436
437 let snippets = snippets
438 .into_iter()
439 .map(|range| snapshot.text_for_range(range).collect::<String>())
440 .collect::<Vec<_>>();
441 assert_eq!(
442 snippets,
443 vec![indoc! { r#"
444 # %% [markdown]
445 # This is some arithmetic
446 print(1 + 1)
447 print(2 + 2)"# }]
448 );
449
450 // Jupytext snippets intersecting a non-empty selection
451 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
452 let snippets = snippets
453 .into_iter()
454 .map(|range| snapshot.text_for_range(range).collect::<String>())
455 .collect::<Vec<_>>();
456 assert_eq!(
457 snippets,
458 vec![
459 indoc! { r#"
460 # %% [markdown]
461 # This is some arithmetic
462 print(1 + 1)
463 print(2 + 2)"#
464 },
465 indoc! { r#"
466 # %%
467 print(3 + 3)
468 print(4 + 4)
469
470 print(5 + 5)"#
471 }
472 ]
473 );
474 }
475
476 #[gpui::test]
477 fn test_markdown_code_blocks(cx: &mut AppContext) {
478 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
479 let typescript = languages::language(
480 "typescript",
481 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
482 );
483 let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
484 let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
485 language_registry.add(markdown.clone());
486 language_registry.add(typescript.clone());
487 language_registry.add(python.clone());
488
489 // Two code blocks intersecting with selection
490 let buffer = cx.new_model(|cx| {
491 let mut buffer = Buffer::local(
492 indoc! { r#"
493 Hey this is Markdown!
494
495 ```typescript
496 let foo = 999;
497 console.log(foo + 1999);
498 ```
499
500 ```typescript
501 console.log("foo")
502 ```
503 "#
504 },
505 cx,
506 );
507 buffer.set_language_registry(language_registry.clone());
508 buffer.set_language(Some(markdown.clone()), cx);
509 buffer
510 });
511 let snapshot = buffer.read(cx).snapshot();
512
513 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
514 let snippets = snippets
515 .into_iter()
516 .map(|range| snapshot.text_for_range(range).collect::<String>())
517 .collect::<Vec<_>>();
518
519 assert_eq!(
520 snippets,
521 vec![
522 indoc! { r#"
523 let foo = 999;
524 console.log(foo + 1999);
525 "#
526 },
527 "console.log(\"foo\")\n"
528 ]
529 );
530
531 // Three code blocks intersecting with selection
532 let buffer = cx.new_model(|cx| {
533 let mut buffer = Buffer::local(
534 indoc! { r#"
535 Hey this is Markdown!
536
537 ```typescript
538 let foo = 999;
539 console.log(foo + 1999);
540 ```
541
542 ```ts
543 console.log("foo")
544 ```
545
546 ```typescript
547 console.log("another code block")
548 ```
549 "# },
550 cx,
551 );
552 buffer.set_language_registry(language_registry.clone());
553 buffer.set_language(Some(markdown.clone()), cx);
554 buffer
555 });
556 let snapshot = buffer.read(cx).snapshot();
557
558 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
559 let snippets = snippets
560 .into_iter()
561 .map(|range| snapshot.text_for_range(range).collect::<String>())
562 .collect::<Vec<_>>();
563
564 assert_eq!(
565 snippets,
566 vec![
567 indoc! { r#"
568 let foo = 999;
569 console.log(foo + 1999);
570 "#
571 },
572 "console.log(\"foo\")\n",
573 "console.log(\"another code block\")\n",
574 ]
575 );
576
577 // Python code block
578 let buffer = cx.new_model(|cx| {
579 let mut buffer = Buffer::local(
580 indoc! { r#"
581 Hey this is Markdown!
582
583 ```python
584 print("hello there")
585 print("hello there")
586 print("hello there")
587 ```
588 "# },
589 cx,
590 );
591 buffer.set_language_registry(language_registry.clone());
592 buffer.set_language(Some(markdown.clone()), cx);
593 buffer
594 });
595 let snapshot = buffer.read(cx).snapshot();
596
597 let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
598 let snippets = snippets
599 .into_iter()
600 .map(|range| snapshot.text_for_range(range).collect::<String>())
601 .collect::<Vec<_>>();
602
603 assert_eq!(
604 snippets,
605 vec![indoc! { r#"
606 print("hello there")
607 print("hello there")
608 print("hello there")
609 "#
610 },]
611 );
612 }
613}