repl_editor.rs

  1//! REPL operations on an [`Editor`].
  2
  3use std::ops::Range;
  4use std::sync::Arc;
  5
  6use anyhow::{Context as _, Result};
  7use editor::Editor;
  8use gpui::{App, Entity, WeakEntity, Window, prelude::*};
  9use language::{BufferSnapshot, Language, LanguageName, Point};
 10use project::{ProjectItem as _, WorktreeId};
 11
 12use crate::repl_store::ReplStore;
 13use crate::session::SessionEvent;
 14use crate::{
 15    ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart, Session, Shutdown,
 16};
 17
 18pub fn assign_kernelspec(
 19    kernel_specification: KernelSpecification,
 20    weak_editor: WeakEntity<Editor>,
 21    window: &mut Window,
 22    cx: &mut App,
 23) -> Result<()> {
 24    let store = ReplStore::global(cx);
 25    if !store.read(cx).is_enabled() {
 26        return Ok(());
 27    }
 28
 29    let worktree_id = crate::repl_editor::worktree_id_for_editor(weak_editor.clone(), cx)
 30        .context("editor is not in a worktree")?;
 31
 32    store.update(cx, |store, cx| {
 33        store.set_active_kernelspec(worktree_id, kernel_specification.clone(), cx);
 34    });
 35
 36    let fs = store.read(cx).fs().clone();
 37
 38    if let Some(session) = store.read(cx).get_session(weak_editor.entity_id()).cloned() {
 39        // Drop previous session, start new one
 40        session.update(cx, |session, cx| {
 41            session.clear_outputs(cx);
 42            session.shutdown(window, cx);
 43            cx.notify();
 44        });
 45    }
 46
 47    let session =
 48        cx.new(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, window, cx));
 49
 50    weak_editor
 51        .update(cx, |_editor, cx| {
 52            cx.notify();
 53
 54            cx.subscribe(&session, {
 55                let store = store.clone();
 56                move |_this, _session, event, cx| match event {
 57                    SessionEvent::Shutdown(shutdown_event) => {
 58                        store.update(cx, |store, _cx| {
 59                            store.remove_session(shutdown_event.entity_id());
 60                        });
 61                    }
 62                }
 63            })
 64            .detach();
 65        })
 66        .ok();
 67
 68    store.update(cx, |store, _cx| {
 69        store.insert_session(weak_editor.entity_id(), session.clone());
 70    });
 71
 72    Ok(())
 73}
 74
 75pub fn run(
 76    editor: WeakEntity<Editor>,
 77    move_down: bool,
 78    window: &mut Window,
 79    cx: &mut App,
 80) -> Result<()> {
 81    let store = ReplStore::global(cx);
 82    if !store.read(cx).is_enabled() {
 83        return Ok(());
 84    }
 85
 86    let editor = editor.upgrade().context("editor was dropped")?;
 87    let selected_range = editor
 88        .update(cx, |editor, cx| editor.selections.newest_adjusted(cx))
 89        .range();
 90    let multibuffer = editor.read(cx).buffer().clone();
 91    let Some(buffer) = multibuffer.read(cx).as_singleton() else {
 92        return Ok(());
 93    };
 94
 95    let Some(project_path) = buffer.read(cx).project_path(cx) else {
 96        return Ok(());
 97    };
 98
 99    let (runnable_ranges, next_cell_point) =
100        runnable_ranges(&buffer.read(cx).snapshot(), selected_range);
101
102    for runnable_range in runnable_ranges {
103        let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else {
104            continue;
105        };
106
107        let kernel_specification = store
108            .read(cx)
109            .active_kernelspec(project_path.worktree_id, Some(language.clone()), cx)
110            .ok_or_else(|| anyhow::anyhow!("No kernel found for language: {}", language.name()))?;
111
112        let fs = store.read(cx).fs().clone();
113
114        let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
115        {
116            session
117        } else {
118            let weak_editor = editor.downgrade();
119            let session =
120                cx.new(|cx| Session::new(weak_editor, fs, kernel_specification, window, cx));
121
122            editor.update(cx, |_editor, cx| {
123                cx.notify();
124
125                cx.subscribe(&session, {
126                    let store = store.clone();
127                    move |_this, _session, event, cx| match event {
128                        SessionEvent::Shutdown(shutdown_event) => {
129                            store.update(cx, |store, _cx| {
130                                store.remove_session(shutdown_event.entity_id());
131                            });
132                        }
133                    }
134                })
135                .detach();
136            });
137
138            store.update(cx, |store, _cx| {
139                store.insert_session(editor.entity_id(), session.clone());
140            });
141
142            session
143        };
144
145        let selected_text;
146        let anchor_range;
147        let next_cursor;
148        {
149            let snapshot = multibuffer.read(cx).read(cx);
150            selected_text = snapshot
151                .text_for_range(runnable_range.clone())
152                .collect::<String>();
153            anchor_range = snapshot.anchor_before(runnable_range.start)
154                ..snapshot.anchor_after(runnable_range.end);
155            next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point));
156        }
157
158        session.update(cx, |session, cx| {
159            session.execute(
160                selected_text,
161                anchor_range,
162                next_cursor,
163                move_down,
164                window,
165                cx,
166            );
167        });
168    }
169
170    anyhow::Ok(())
171}
172
173#[allow(clippy::large_enum_variant)]
174pub enum SessionSupport {
175    ActiveSession(Entity<Session>),
176    Inactive(KernelSpecification),
177    RequiresSetup(LanguageName),
178    Unsupported,
179}
180
181pub fn worktree_id_for_editor(editor: WeakEntity<Editor>, cx: &mut App) -> Option<WorktreeId> {
182    editor.upgrade().and_then(|editor| {
183        editor
184            .read(cx)
185            .buffer()
186            .read(cx)
187            .as_singleton()?
188            .read(cx)
189            .project_path(cx)
190            .map(|path| path.worktree_id)
191    })
192}
193
194pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport {
195    let store = ReplStore::global(cx);
196    let entity_id = editor.entity_id();
197
198    if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
199        return SessionSupport::ActiveSession(session);
200    };
201
202    let Some(language) = get_language(editor.clone(), cx) else {
203        return SessionSupport::Unsupported;
204    };
205
206    let worktree_id = worktree_id_for_editor(editor.clone(), cx);
207
208    let Some(worktree_id) = worktree_id else {
209        return SessionSupport::Unsupported;
210    };
211
212    let kernelspec = store
213        .read(cx)
214        .active_kernelspec(worktree_id, Some(language.clone()), cx);
215
216    match kernelspec {
217        Some(kernelspec) => SessionSupport::Inactive(kernelspec),
218        None => {
219            if language_supported(&language.clone()) {
220                SessionSupport::RequiresSetup(language.name())
221            } else {
222                SessionSupport::Unsupported
223            }
224        }
225    }
226}
227
228pub fn clear_outputs(editor: WeakEntity<Editor>, cx: &mut App) {
229    let store = ReplStore::global(cx);
230    let entity_id = editor.entity_id();
231    let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
232        return;
233    };
234    session.update(cx, |session, cx| {
235        session.clear_outputs(cx);
236        cx.notify();
237    });
238}
239
240pub fn interrupt(editor: WeakEntity<Editor>, cx: &mut App) {
241    let store = ReplStore::global(cx);
242    let entity_id = editor.entity_id();
243    let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
244        return;
245    };
246
247    session.update(cx, |session, cx| {
248        session.interrupt(cx);
249        cx.notify();
250    });
251}
252
253pub fn shutdown(editor: WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
254    let store = ReplStore::global(cx);
255    let entity_id = editor.entity_id();
256    let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
257        return;
258    };
259
260    session.update(cx, |session, cx| {
261        session.shutdown(window, cx);
262        cx.notify();
263    });
264}
265
266pub fn restart(editor: WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
267    let Some(editor) = editor.upgrade() else {
268        return;
269    };
270
271    let entity_id = editor.entity_id();
272
273    let Some(session) = ReplStore::global(cx)
274        .read(cx)
275        .get_session(entity_id)
276        .cloned()
277    else {
278        return;
279    };
280
281    session.update(cx, |session, cx| {
282        session.restart(window, cx);
283        cx.notify();
284    });
285}
286
287pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEntity<Editor>) {
288    editor
289        .register_action({
290            let editor_handle = editor_handle.clone();
291            move |_: &ClearOutputs, _, cx| {
292                if !JupyterSettings::enabled(cx) {
293                    return;
294                }
295
296                crate::clear_outputs(editor_handle.clone(), cx);
297            }
298        })
299        .detach();
300
301    editor
302        .register_action({
303            let editor_handle = editor_handle.clone();
304            move |_: &Interrupt, _, cx| {
305                if !JupyterSettings::enabled(cx) {
306                    return;
307                }
308
309                crate::interrupt(editor_handle.clone(), cx);
310            }
311        })
312        .detach();
313
314    editor
315        .register_action({
316            let editor_handle = editor_handle.clone();
317            move |_: &Shutdown, window, cx| {
318                if !JupyterSettings::enabled(cx) {
319                    return;
320                }
321
322                crate::shutdown(editor_handle.clone(), window, cx);
323            }
324        })
325        .detach();
326
327    editor
328        .register_action({
329            let editor_handle = editor_handle.clone();
330            move |_: &Restart, window, cx| {
331                if !JupyterSettings::enabled(cx) {
332                    return;
333                }
334
335                crate::restart(editor_handle.clone(), window, cx);
336            }
337        })
338        .detach();
339}
340
341fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
342    let mut snippet_end_row = end_row;
343    while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
344        snippet_end_row -= 1;
345    }
346    Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
347}
348
349// Returns the ranges of the snippets in the buffer and the next point for moving the cursor to
350fn jupytext_cells(
351    buffer: &BufferSnapshot,
352    range: Range<Point>,
353) -> (Vec<Range<Point>>, Option<Point>) {
354    let mut current_row = range.start.row;
355
356    let Some(language) = buffer.language() else {
357        return (Vec::new(), None);
358    };
359
360    let default_scope = language.default_scope();
361    let comment_prefixes = default_scope.line_comment_prefixes();
362    if comment_prefixes.is_empty() {
363        return (Vec::new(), None);
364    }
365
366    let jupytext_prefixes = comment_prefixes
367        .iter()
368        .map(|comment_prefix| format!("{comment_prefix}%%"))
369        .collect::<Vec<_>>();
370
371    let mut snippet_start_row = None;
372    loop {
373        if jupytext_prefixes
374            .iter()
375            .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
376        {
377            snippet_start_row = Some(current_row);
378            break;
379        } else if current_row > 0 {
380            current_row -= 1;
381        } else {
382            break;
383        }
384    }
385
386    let mut snippets = Vec::new();
387    if let Some(mut snippet_start_row) = snippet_start_row {
388        for current_row in range.start.row + 1..=buffer.max_point().row {
389            if jupytext_prefixes
390                .iter()
391                .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
392            {
393                snippets.push(cell_range(buffer, snippet_start_row, current_row - 1));
394
395                if current_row <= range.end.row {
396                    snippet_start_row = current_row;
397                } else {
398                    // Return our snippets as well as the next point for moving the cursor to
399                    return (snippets, Some(Point::new(current_row, 0)));
400                }
401            }
402        }
403
404        // Go to the end of the buffer (no more jupytext cells found)
405        snippets.push(cell_range(
406            buffer,
407            snippet_start_row,
408            buffer.max_point().row,
409        ));
410    }
411
412    (snippets, None)
413}
414
415fn runnable_ranges(
416    buffer: &BufferSnapshot,
417    range: Range<Point>,
418) -> (Vec<Range<Point>>, Option<Point>) {
419    if let Some(language) = buffer.language() {
420        if language.name() == "Markdown".into() {
421            return (markdown_code_blocks(buffer, range.clone()), None);
422        }
423    }
424
425    let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone());
426    if !jupytext_snippets.is_empty() {
427        return (jupytext_snippets, next_cursor);
428    }
429
430    let snippet_range = cell_range(buffer, range.start.row, range.end.row);
431    let start_language = buffer.language_at(snippet_range.start);
432    let end_language = buffer.language_at(snippet_range.end);
433
434    if start_language
435        .zip(end_language)
436        .map_or(false, |(start, end)| start == end)
437    {
438        (vec![snippet_range], None)
439    } else {
440        (Vec::new(), None)
441    }
442}
443
444// We allow markdown code blocks to end in a trailing newline in order to render the output
445// below the final code fence. This is different than our behavior for selections and Jupytext cells.
446fn markdown_code_blocks(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Range<Point>> {
447    buffer
448        .injections_intersecting_range(range)
449        .filter(|(_, language)| language_supported(language))
450        .map(|(content_range, _)| {
451            buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
452        })
453        .collect()
454}
455
456fn language_supported(language: &Arc<Language>) -> bool {
457    match language.name().as_ref() {
458        "TypeScript" | "Python" => true,
459        _ => false,
460    }
461}
462
463fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
464    editor
465        .update(cx, |editor, cx| {
466            let selection = editor.selections.newest::<usize>(cx);
467            let buffer = editor.buffer().read(cx).snapshot(cx);
468            buffer.language_at(selection.head()).cloned()
469        })
470        .ok()
471        .flatten()
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use gpui::App;
478    use indoc::indoc;
479    use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
480
481    #[gpui::test]
482    fn test_snippet_ranges(cx: &mut App) {
483        // Create a test language
484        let test_language = Arc::new(Language::new(
485            LanguageConfig {
486                name: "TestLang".into(),
487                line_comments: vec!["# ".into()],
488                ..Default::default()
489            },
490            None,
491        ));
492
493        let buffer = cx.new(|cx| {
494            Buffer::local(
495                indoc! { r#"
496                    print(1 + 1)
497                    print(2 + 2)
498
499                    print(4 + 4)
500
501
502                "# },
503                cx,
504            )
505            .with_language(test_language, cx)
506        });
507        let snapshot = buffer.read(cx).snapshot();
508
509        // Single-point selection
510        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
511        let snippets = snippets
512            .into_iter()
513            .map(|range| snapshot.text_for_range(range).collect::<String>())
514            .collect::<Vec<_>>();
515        assert_eq!(snippets, vec!["print(1 + 1)"]);
516
517        // Multi-line selection
518        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
519        let snippets = snippets
520            .into_iter()
521            .map(|range| snapshot.text_for_range(range).collect::<String>())
522            .collect::<Vec<_>>();
523        assert_eq!(
524            snippets,
525            vec![indoc! { r#"
526                print(1 + 1)
527                print(2 + 2)"# }]
528        );
529
530        // Trimming multiple trailing blank lines
531        let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
532
533        let snippets = snippets
534            .into_iter()
535            .map(|range| snapshot.text_for_range(range).collect::<String>())
536            .collect::<Vec<_>>();
537        assert_eq!(
538            snippets,
539            vec![indoc! { r#"
540                print(1 + 1)
541                print(2 + 2)
542
543                print(4 + 4)"# }]
544        );
545    }
546
547    #[gpui::test]
548    fn test_jupytext_snippet_ranges(cx: &mut App) {
549        // Create a test language
550        let test_language = Arc::new(Language::new(
551            LanguageConfig {
552                name: "TestLang".into(),
553                line_comments: vec!["# ".into()],
554                ..Default::default()
555            },
556            None,
557        ));
558
559        let buffer = cx.new(|cx| {
560            Buffer::local(
561                indoc! { r#"
562                    # Hello!
563                    # %% [markdown]
564                    # This is some arithmetic
565                    print(1 + 1)
566                    print(2 + 2)
567
568                    # %%
569                    print(3 + 3)
570                    print(4 + 4)
571
572                    print(5 + 5)
573
574
575
576                "# },
577                cx,
578            )
579            .with_language(test_language, cx)
580        });
581        let snapshot = buffer.read(cx).snapshot();
582
583        // Jupytext snippet surrounding an empty selection
584        let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
585
586        let snippets = snippets
587            .into_iter()
588            .map(|range| snapshot.text_for_range(range).collect::<String>())
589            .collect::<Vec<_>>();
590        assert_eq!(
591            snippets,
592            vec![indoc! { r#"
593                # %% [markdown]
594                # This is some arithmetic
595                print(1 + 1)
596                print(2 + 2)"# }]
597        );
598
599        // Jupytext snippets intersecting a non-empty selection
600        let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
601        let snippets = snippets
602            .into_iter()
603            .map(|range| snapshot.text_for_range(range).collect::<String>())
604            .collect::<Vec<_>>();
605        assert_eq!(
606            snippets,
607            vec![
608                indoc! { r#"
609                    # %% [markdown]
610                    # This is some arithmetic
611                    print(1 + 1)
612                    print(2 + 2)"#
613                },
614                indoc! { r#"
615                    # %%
616                    print(3 + 3)
617                    print(4 + 4)
618
619                    print(5 + 5)"#
620                }
621            ]
622        );
623    }
624
625    #[gpui::test]
626    fn test_markdown_code_blocks(cx: &mut App) {
627        let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
628        let typescript = languages::language(
629            "typescript",
630            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
631        );
632        let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
633        let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
634        language_registry.add(markdown.clone());
635        language_registry.add(typescript.clone());
636        language_registry.add(python.clone());
637
638        // Two code blocks intersecting with selection
639        let buffer = cx.new(|cx| {
640            let mut buffer = Buffer::local(
641                indoc! { r#"
642                    Hey this is Markdown!
643
644                    ```typescript
645                    let foo = 999;
646                    console.log(foo + 1999);
647                    ```
648
649                    ```typescript
650                    console.log("foo")
651                    ```
652                    "#
653                },
654                cx,
655            );
656            buffer.set_language_registry(language_registry.clone());
657            buffer.set_language(Some(markdown.clone()), cx);
658            buffer
659        });
660        let snapshot = buffer.read(cx).snapshot();
661
662        let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
663        let snippets = snippets
664            .into_iter()
665            .map(|range| snapshot.text_for_range(range).collect::<String>())
666            .collect::<Vec<_>>();
667
668        assert_eq!(
669            snippets,
670            vec![
671                indoc! { r#"
672                    let foo = 999;
673                    console.log(foo + 1999);
674                    "#
675                },
676                "console.log(\"foo\")\n"
677            ]
678        );
679
680        // Three code blocks intersecting with selection
681        let buffer = cx.new(|cx| {
682            let mut buffer = Buffer::local(
683                indoc! { r#"
684                    Hey this is Markdown!
685
686                    ```typescript
687                    let foo = 999;
688                    console.log(foo + 1999);
689                    ```
690
691                    ```ts
692                    console.log("foo")
693                    ```
694
695                    ```typescript
696                    console.log("another code block")
697                    ```
698                "# },
699                cx,
700            );
701            buffer.set_language_registry(language_registry.clone());
702            buffer.set_language(Some(markdown.clone()), cx);
703            buffer
704        });
705        let snapshot = buffer.read(cx).snapshot();
706
707        let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
708        let snippets = snippets
709            .into_iter()
710            .map(|range| snapshot.text_for_range(range).collect::<String>())
711            .collect::<Vec<_>>();
712
713        assert_eq!(
714            snippets,
715            vec![
716                indoc! { r#"
717                    let foo = 999;
718                    console.log(foo + 1999);
719                    "#
720                },
721                "console.log(\"foo\")\n",
722                "console.log(\"another code block\")\n",
723            ]
724        );
725
726        // Python code block
727        let buffer = cx.new(|cx| {
728            let mut buffer = Buffer::local(
729                indoc! { r#"
730                    Hey this is Markdown!
731
732                    ```python
733                    print("hello there")
734                    print("hello there")
735                    print("hello there")
736                    ```
737                "# },
738                cx,
739            );
740            buffer.set_language_registry(language_registry.clone());
741            buffer.set_language(Some(markdown.clone()), cx);
742            buffer
743        });
744        let snapshot = buffer.read(cx).snapshot();
745
746        let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
747        let snippets = snippets
748            .into_iter()
749            .map(|range| snapshot.text_for_range(range).collect::<String>())
750            .collect::<Vec<_>>();
751
752        assert_eq!(
753            snippets,
754            vec![indoc! { r#"
755                print("hello there")
756                print("hello there")
757                print("hello there")
758                "#
759            },]
760        );
761    }
762}