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