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 (ranges, next_cell_point) = snippet_ranges(&buffer.read(cx).snapshot(), selected_range);
 31
 32    for range in ranges {
 33        let Some(language) = multibuffer.read(cx).language_at(range.start, cx) else {
 34            continue;
 35        };
 36
 37        let kernel_specification = store.update(cx, |store, cx| {
 38            store
 39                .kernelspec(&language, cx)
 40                .with_context(|| format!("No kernel found for language: {}", language.name()))
 41        })?;
 42
 43        let fs = store.read(cx).fs().clone();
 44        let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
 45        {
 46            session
 47        } else {
 48            let weak_editor = editor.downgrade();
 49            let session = cx.new_view(|cx| Session::new(weak_editor, fs, kernel_specification, cx));
 50
 51            editor.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
 67            store.update(cx, |store, _cx| {
 68                store.insert_session(editor.entity_id(), session.clone());
 69            });
 70
 71            session
 72        };
 73
 74        let selected_text;
 75        let anchor_range;
 76        let next_cursor;
 77        {
 78            let snapshot = multibuffer.read(cx).read(cx);
 79            selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
 80            anchor_range = snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end);
 81            next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point));
 82        }
 83
 84        session.update(cx, |session, cx| {
 85            session.execute(selected_text, anchor_range, next_cursor, cx);
 86        });
 87    }
 88
 89    anyhow::Ok(())
 90}
 91
 92pub enum SessionSupport {
 93    ActiveSession(View<Session>),
 94    Inactive(Box<KernelSpecification>),
 95    RequiresSetup(Arc<str>),
 96    Unsupported,
 97}
 98
 99pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> SessionSupport {
100    let store = ReplStore::global(cx);
101    let entity_id = editor.entity_id();
102
103    if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
104        return SessionSupport::ActiveSession(session);
105    };
106
107    let Some(language) = get_language(editor, cx) else {
108        return SessionSupport::Unsupported;
109    };
110    let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
111
112    match kernelspec {
113        Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
114        None => match language.name().as_ref() {
115            "TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
116            _ => SessionSupport::Unsupported,
117        },
118    }
119}
120
121pub fn clear_outputs(editor: WeakView<Editor>, cx: &mut WindowContext) {
122    let store = ReplStore::global(cx);
123    let entity_id = editor.entity_id();
124    let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
125        return;
126    };
127    session.update(cx, |session, cx| {
128        session.clear_outputs(cx);
129        cx.notify();
130    });
131}
132
133pub fn interrupt(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
140    session.update(cx, |session, cx| {
141        session.interrupt(cx);
142        cx.notify();
143    });
144}
145
146pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
147    let store = ReplStore::global(cx);
148    let entity_id = editor.entity_id();
149    let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
150        return;
151    };
152
153    session.update(cx, |session, cx| {
154        session.shutdown(cx);
155        cx.notify();
156    });
157}
158
159fn snippet_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
160    let mut snippet_end_row = end_row;
161    while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
162        snippet_end_row -= 1;
163    }
164    Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
165}
166
167// Returns the ranges of the snippets in the buffer and the next range for moving the cursor to
168fn jupytext_snippets(
169    buffer: &BufferSnapshot,
170    range: Range<Point>,
171) -> (Vec<Range<Point>>, Option<Point>) {
172    let mut current_row = range.start.row;
173
174    let Some(language) = buffer.language() else {
175        return (Vec::new(), None);
176    };
177
178    let default_scope = language.default_scope();
179    let comment_prefixes = default_scope.line_comment_prefixes();
180    if comment_prefixes.is_empty() {
181        return (Vec::new(), None);
182    }
183
184    let jupytext_prefixes = comment_prefixes
185        .iter()
186        .map(|comment_prefix| format!("{comment_prefix}%%"))
187        .collect::<Vec<_>>();
188
189    let mut snippet_start_row = None;
190    loop {
191        if jupytext_prefixes
192            .iter()
193            .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
194        {
195            snippet_start_row = Some(current_row);
196            break;
197        } else if current_row > 0 {
198            current_row -= 1;
199        } else {
200            break;
201        }
202    }
203
204    let mut snippets = Vec::new();
205    if let Some(mut snippet_start_row) = snippet_start_row {
206        for current_row in range.start.row + 1..=buffer.max_point().row {
207            if jupytext_prefixes
208                .iter()
209                .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
210            {
211                snippets.push(snippet_range(buffer, snippet_start_row, current_row - 1));
212
213                if current_row <= range.end.row {
214                    snippet_start_row = current_row;
215                } else {
216                    // Return our snippets as well as the next range for moving the cursor to
217                    return (snippets, Some(Point::new(current_row, 0)));
218                }
219            }
220        }
221
222        // Go to the end of the buffer (no more jupytext cells found)
223        snippets.push(snippet_range(
224            buffer,
225            snippet_start_row,
226            buffer.max_point().row,
227        ));
228    }
229
230    (snippets, None)
231}
232
233fn snippet_ranges(
234    buffer: &BufferSnapshot,
235    range: Range<Point>,
236) -> (Vec<Range<Point>>, Option<Point>) {
237    let (jupytext_snippets, next_cursor) = jupytext_snippets(buffer, range.clone());
238    if !jupytext_snippets.is_empty() {
239        return (jupytext_snippets, next_cursor);
240    }
241
242    let snippet_range = snippet_range(buffer, range.start.row, range.end.row);
243    let start_language = buffer.language_at(snippet_range.start);
244    let end_language = buffer.language_at(snippet_range.end);
245
246    if let Some((start, end)) = start_language.zip(end_language) {
247        if start == end {
248            return (vec![snippet_range], None);
249        }
250    }
251
252    (Vec::new(), None)
253}
254
255fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Language>> {
256    let editor = editor.upgrade()?;
257    let selection = editor.read(cx).selections.newest::<usize>(cx);
258    let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
259    buffer.language_at(selection.head()).cloned()
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use gpui::Context;
266    use indoc::indoc;
267    use language::{Buffer, Language, LanguageConfig};
268
269    #[gpui::test]
270    fn test_snippet_ranges(cx: &mut AppContext) {
271        // Create a test language
272        let test_language = Arc::new(Language::new(
273            LanguageConfig {
274                name: "TestLang".into(),
275                line_comments: vec!["# ".into()],
276                ..Default::default()
277            },
278            None,
279        ));
280
281        let buffer = cx.new_model(|cx| {
282            Buffer::local(
283                indoc! { r#"
284                    print(1 + 1)
285                    print(2 + 2)
286
287                    print(4 + 4)
288
289
290                "# },
291                cx,
292            )
293            .with_language(test_language, cx)
294        });
295        let snapshot = buffer.read(cx).snapshot();
296
297        // Single-point selection
298        let (snippets, _) = snippet_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
299        let snippets = snippets
300            .into_iter()
301            .map(|range| snapshot.text_for_range(range).collect::<String>())
302            .collect::<Vec<_>>();
303        assert_eq!(snippets, vec!["print(1 + 1)"]);
304
305        // Multi-line selection
306        let (snippets, _) = snippet_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
307        let snippets = snippets
308            .into_iter()
309            .map(|range| snapshot.text_for_range(range).collect::<String>())
310            .collect::<Vec<_>>();
311        assert_eq!(
312            snippets,
313            vec![indoc! { r#"
314                print(1 + 1)
315                print(2 + 2)"# }]
316        );
317
318        // Trimming multiple trailing blank lines
319        let (snippets, _) = snippet_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
320
321        let snippets = snippets
322            .into_iter()
323            .map(|range| snapshot.text_for_range(range).collect::<String>())
324            .collect::<Vec<_>>();
325        assert_eq!(
326            snippets,
327            vec![indoc! { r#"
328                print(1 + 1)
329                print(2 + 2)
330
331                print(4 + 4)"# }]
332        );
333    }
334
335    #[gpui::test]
336    fn test_jupytext_snippet_ranges(cx: &mut AppContext) {
337        // Create a test language
338        let test_language = Arc::new(Language::new(
339            LanguageConfig {
340                name: "TestLang".into(),
341                line_comments: vec!["# ".into()],
342                ..Default::default()
343            },
344            None,
345        ));
346
347        let buffer = cx.new_model(|cx| {
348            Buffer::local(
349                indoc! { r#"
350                    # Hello!
351                    # %% [markdown]
352                    # This is some arithmetic
353                    print(1 + 1)
354                    print(2 + 2)
355
356                    # %%
357                    print(3 + 3)
358                    print(4 + 4)
359
360                    print(5 + 5)
361
362
363
364                "# },
365                cx,
366            )
367            .with_language(test_language, cx)
368        });
369        let snapshot = buffer.read(cx).snapshot();
370
371        // Jupytext snippet surrounding an empty selection
372        let (snippets, _) = snippet_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
373
374        let snippets = snippets
375            .into_iter()
376            .map(|range| snapshot.text_for_range(range).collect::<String>())
377            .collect::<Vec<_>>();
378        assert_eq!(
379            snippets,
380            vec![indoc! { r#"
381                # %% [markdown]
382                # This is some arithmetic
383                print(1 + 1)
384                print(2 + 2)"# }]
385        );
386
387        // Jupytext snippets intersecting a non-empty selection
388        let (snippets, _) = snippet_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
389        let snippets = snippets
390            .into_iter()
391            .map(|range| snapshot.text_for_range(range).collect::<String>())
392            .collect::<Vec<_>>();
393        assert_eq!(
394            snippets,
395            vec![
396                indoc! { r#"
397                    # %% [markdown]
398                    # This is some arithmetic
399                    print(1 + 1)
400                    print(2 + 2)"#
401                },
402                indoc! { r#"
403                    # %%
404                    print(3 + 3)
405                    print(4 + 4)
406
407                    print(5 + 5)"#
408                }
409            ]
410        );
411    }
412}