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