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