1//! REPL operations on an [`Editor`].
2
3use std::ops::Range;
4use std::sync::Arc;
5
6use anyhow::{Context, Result};
7use editor::{Anchor, Editor, RangeToAnchorExt};
8use gpui::{prelude::*, AppContext, View, WeakView, WindowContext};
9use language::{Language, Point};
10use multi_buffer::MultiBufferRow;
11
12use crate::repl_store::ReplStore;
13use crate::session::SessionEvent;
14use crate::{KernelSpecification, Session};
15
16pub fn run(editor: WeakView<Editor>, cx: &mut WindowContext) -> Result<()> {
17 let store = ReplStore::global(cx);
18 if !store.read(cx).is_enabled() {
19 return Ok(());
20 }
21
22 let Some((selected_text, language, anchor_range)) = snippet(editor.clone(), cx) else {
23 return Ok(());
24 };
25
26 let entity_id = editor.entity_id();
27
28 let kernel_specification = store.update(cx, |store, cx| {
29 store
30 .kernelspec(&language, cx)
31 .with_context(|| format!("No kernel found for language: {}", language.name()))
32 })?;
33
34 let fs = store.read(cx).fs().clone();
35 let session = if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
36 session
37 } else {
38 let session = cx.new_view(|cx| Session::new(editor.clone(), fs, kernel_specification, cx));
39
40 editor.update(cx, |_editor, cx| {
41 cx.notify();
42
43 cx.subscribe(&session, {
44 let store = store.clone();
45 move |_this, _session, event, cx| match event {
46 SessionEvent::Shutdown(shutdown_event) => {
47 store.update(cx, |store, _cx| {
48 store.remove_session(shutdown_event.entity_id());
49 });
50 }
51 }
52 })
53 .detach();
54 })?;
55
56 store.update(cx, |store, _cx| {
57 store.insert_session(entity_id, session.clone());
58 });
59
60 session
61 };
62
63 session.update(cx, |session, cx| {
64 session.execute(&selected_text, anchor_range, cx);
65 });
66
67 anyhow::Ok(())
68}
69
70pub enum SessionSupport {
71 ActiveSession(View<Session>),
72 Inactive(Box<KernelSpecification>),
73 RequiresSetup(Arc<str>),
74 Unsupported,
75}
76
77pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> SessionSupport {
78 let store = ReplStore::global(cx);
79 let entity_id = editor.entity_id();
80
81 if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
82 return SessionSupport::ActiveSession(session);
83 };
84
85 let Some(language) = get_language(editor, cx) else {
86 return SessionSupport::Unsupported;
87 };
88 let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
89
90 match kernelspec {
91 Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
92 None => match language.name().as_ref() {
93 "TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
94 _ => SessionSupport::Unsupported,
95 },
96 }
97}
98
99pub fn clear_outputs(editor: WeakView<Editor>, cx: &mut WindowContext) {
100 let store = ReplStore::global(cx);
101 let entity_id = editor.entity_id();
102 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
103 return;
104 };
105 session.update(cx, |session, cx| {
106 session.clear_outputs(cx);
107 cx.notify();
108 });
109}
110
111pub fn interrupt(editor: WeakView<Editor>, cx: &mut WindowContext) {
112 let store = ReplStore::global(cx);
113 let entity_id = editor.entity_id();
114 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
115 return;
116 };
117
118 session.update(cx, |session, cx| {
119 session.interrupt(cx);
120 cx.notify();
121 });
122}
123
124pub fn shutdown(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
131 session.update(cx, |session, cx| {
132 session.shutdown(cx);
133 cx.notify();
134 });
135}
136
137fn snippet(
138 editor: WeakView<Editor>,
139 cx: &mut WindowContext,
140) -> Option<(String, Arc<Language>, Range<Anchor>)> {
141 let editor = editor.upgrade()?;
142 let editor = editor.read(cx);
143
144 let buffer = editor.buffer().read(cx).snapshot(cx);
145
146 let selection = editor.selections.newest::<usize>(cx);
147 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
148
149 let range = if selection.is_empty() {
150 let cursor = selection.head();
151
152 let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
153 let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
154
155 let end_point = Point::new(
156 cursor_row,
157 multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
158 );
159 let end_offset = start_offset.saturating_add(end_point.column as usize);
160
161 // Create a range from the start to the end of the line
162 start_offset..end_offset
163 } else {
164 selection.range()
165 };
166
167 let anchor_range = range.to_anchors(&multi_buffer_snapshot);
168
169 let selected_text = buffer
170 .text_for_range(anchor_range.clone())
171 .collect::<String>();
172
173 let start_language = buffer.language_at(anchor_range.start)?;
174 let end_language = buffer.language_at(anchor_range.end)?;
175 if start_language != end_language {
176 return None;
177 }
178
179 Some((selected_text, start_language.clone(), anchor_range))
180}
181
182fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Language>> {
183 let editor = editor.upgrade()?;
184 let selection = editor.read(cx).selections.newest::<usize>(cx);
185 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
186 buffer.language_at(selection.head()).cloned()
187}