1use crate::{Supermaven, SupermavenCompletionStateId};
2use anyhow::Result;
3use edit_prediction::{Direction, EditPrediction, EditPredictionProvider};
4use futures::StreamExt as _;
5use gpui::{App, Context, Entity, EntityId, Task};
6use language::{Anchor, Buffer, BufferSnapshot};
7use std::{
8 ops::{AddAssign, Range},
9 path::Path,
10 sync::Arc,
11 time::Duration,
12};
13use text::{ToOffset, ToPoint};
14use unicode_segmentation::UnicodeSegmentation;
15
16pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
17
18pub struct SupermavenCompletionProvider {
19 supermaven: Entity<Supermaven>,
20 buffer_id: Option<EntityId>,
21 completion_id: Option<SupermavenCompletionStateId>,
22 completion_text: Option<String>,
23 file_extension: Option<String>,
24 pending_refresh: Option<Task<Result<()>>>,
25 completion_position: Option<language::Anchor>,
26}
27
28impl SupermavenCompletionProvider {
29 pub fn new(supermaven: Entity<Supermaven>) -> Self {
30 Self {
31 supermaven,
32 buffer_id: None,
33 completion_id: None,
34 completion_text: None,
35 file_extension: None,
36 pending_refresh: None,
37 completion_position: None,
38 }
39 }
40}
41
42// Computes the edit prediction from the difference between the completion text.
43// This is defined by greedily matching the buffer text against the completion text.
44// Inlays are inserted for parts of the completion text that are not present in the buffer text.
45// For example, given the completion text "axbyc" and the buffer text "xy", the rendered output in the editor would be "[a]x[b]y[c]".
46// The parts in brackets are the inlays.
47fn completion_from_diff(
48 snapshot: BufferSnapshot,
49 completion_text: &str,
50 position: Anchor,
51 delete_range: Range<Anchor>,
52) -> EditPrediction {
53 let buffer_text = snapshot.text_for_range(delete_range).collect::<String>();
54
55 let mut edits: Vec<(Range<language::Anchor>, Arc<str>)> = Vec::new();
56
57 let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect();
58 let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect();
59
60 let mut offset = position.to_offset(&snapshot);
61
62 let mut i = 0;
63 let mut j = 0;
64 while i < completion_graphemes.len() && j < buffer_graphemes.len() {
65 // find the next instance of the buffer text in the completion text.
66 let k = completion_graphemes[i..]
67 .iter()
68 .position(|c| *c == buffer_graphemes[j]);
69 match k {
70 Some(k) => {
71 if k != 0 {
72 let offset = snapshot.anchor_after(offset);
73 // the range from the current position to item is an inlay.
74 let edit = (
75 offset..offset,
76 completion_graphemes[i..i + k].join("").into(),
77 );
78 edits.push(edit);
79 }
80 i += k + 1;
81 j += 1;
82 offset.add_assign(buffer_graphemes[j - 1].len());
83 }
84 None => {
85 // there are no more matching completions, so drop the remaining
86 // completion text as an inlay.
87 break;
88 }
89 }
90 }
91
92 if j == buffer_graphemes.len() && i < completion_graphemes.len() {
93 let offset = snapshot.anchor_after(offset);
94 // there is leftover completion text, so drop it as an inlay.
95 let edit_range = offset..offset;
96 let edit_text = completion_graphemes[i..].join("");
97 edits.push((edit_range, edit_text.into()));
98 }
99
100 EditPrediction::Local {
101 id: None,
102 edits,
103 edit_preview: None,
104 }
105}
106
107impl EditPredictionProvider for SupermavenCompletionProvider {
108 fn name() -> &'static str {
109 "supermaven"
110 }
111
112 fn display_name() -> &'static str {
113 "Supermaven"
114 }
115
116 fn show_completions_in_menu() -> bool {
117 true
118 }
119
120 fn show_tab_accept_marker() -> bool {
121 true
122 }
123
124 fn supports_jump_to_edit() -> bool {
125 false
126 }
127
128 fn is_enabled(&self, _buffer: &Entity<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
129 self.supermaven.read(cx).is_enabled()
130 }
131
132 fn is_refreshing(&self) -> bool {
133 self.pending_refresh.is_some() && self.completion_id.is_none()
134 }
135
136 fn refresh(
137 &mut self,
138 buffer_handle: Entity<Buffer>,
139 cursor_position: Anchor,
140 debounce: bool,
141 cx: &mut Context<Self>,
142 ) {
143 // Only make new completion requests when debounce is true (i.e., when text is typed)
144 // When debounce is false (i.e., cursor movement), we should not make new requests
145 if !debounce {
146 return;
147 }
148
149 reset_completion_cache(self, cx);
150
151 let Some(mut completion) = self.supermaven.update(cx, |supermaven, cx| {
152 supermaven.complete(&buffer_handle, cursor_position, cx)
153 }) else {
154 return;
155 };
156
157 self.pending_refresh = Some(cx.spawn(async move |this, cx| {
158 if debounce {
159 cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
160 }
161
162 while let Some(()) = completion.updates.next().await {
163 this.update(cx, |this, cx| {
164 // Get the completion text and cache it
165 if let Some(text) =
166 this.supermaven
167 .read(cx)
168 .completion(&buffer_handle, cursor_position, cx)
169 {
170 this.completion_text = Some(text.to_string());
171
172 this.completion_position = Some(cursor_position);
173 }
174
175 this.completion_id = Some(completion.id);
176 this.buffer_id = Some(buffer_handle.entity_id());
177 this.file_extension = buffer_handle.read(cx).file().and_then(|file| {
178 Some(
179 Path::new(file.file_name(cx))
180 .extension()?
181 .to_str()?
182 .to_string(),
183 )
184 });
185 cx.notify();
186 })?;
187 }
188 Ok(())
189 }));
190 }
191
192 fn cycle(
193 &mut self,
194 _buffer: Entity<Buffer>,
195 _cursor_position: Anchor,
196 _direction: Direction,
197 _cx: &mut Context<Self>,
198 ) {
199 }
200
201 fn accept(&mut self, _cx: &mut Context<Self>) {
202 reset_completion_cache(self, _cx);
203 }
204
205 fn discard(&mut self, _cx: &mut Context<Self>) {
206 reset_completion_cache(self, _cx);
207 }
208
209 fn suggest(
210 &mut self,
211 buffer: &Entity<Buffer>,
212 cursor_position: Anchor,
213 cx: &mut Context<Self>,
214 ) -> Option<EditPrediction> {
215 if self.buffer_id != Some(buffer.entity_id()) {
216 return None;
217 }
218
219 if self.completion_id.is_none() {
220 return None;
221 }
222
223 let completion_text = if let Some(cached_text) = &self.completion_text {
224 cached_text.as_str()
225 } else {
226 let text = self
227 .supermaven
228 .read(cx)
229 .completion(buffer, cursor_position, cx)?;
230 self.completion_text = Some(text.to_string());
231 text
232 };
233
234 // Check if the cursor is still at the same position as the completion request
235 // If we don't have a completion position stored, don't show the completion
236 if let Some(completion_position) = self.completion_position {
237 if cursor_position != completion_position {
238 return None;
239 }
240 } else {
241 return None;
242 }
243
244 let completion_text = trim_to_end_of_line_unless_leading_newline(completion_text);
245
246 let completion_text = completion_text.trim_end();
247
248 if !completion_text.trim().is_empty() {
249 let snapshot = buffer.read(cx).snapshot();
250
251 // Calculate the range from cursor to end of line correctly
252 let cursor_point = cursor_position.to_point(&snapshot);
253 let end_of_line = snapshot.anchor_after(language::Point::new(
254 cursor_point.row,
255 snapshot.line_len(cursor_point.row),
256 ));
257 let delete_range = cursor_position..end_of_line;
258
259 Some(completion_from_diff(
260 snapshot,
261 completion_text,
262 cursor_position,
263 delete_range,
264 ))
265 } else {
266 None
267 }
268 }
269}
270
271fn reset_completion_cache(
272 provider: &mut SupermavenCompletionProvider,
273 _cx: &mut Context<SupermavenCompletionProvider>,
274) {
275 provider.pending_refresh = None;
276 provider.completion_id = None;
277 provider.completion_text = None;
278 provider.completion_position = None;
279 provider.buffer_id = None;
280}
281
282fn trim_to_end_of_line_unless_leading_newline(text: &str) -> &str {
283 if has_leading_newline(text) {
284 text
285 } else if let Some(i) = text.find('\n') {
286 &text[..i]
287 } else {
288 text
289 }
290}
291
292fn has_leading_newline(text: &str) -> bool {
293 for c in text.chars() {
294 if c == '\n' {
295 return true;
296 }
297 if !c.is_whitespace() {
298 return false;
299 }
300 }
301 false
302}