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