repl_editor.rs

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