1use crate::{Supermaven, SupermavenCompletionStateId};
2use anyhow::Result;
3use client::telemetry::Telemetry;
4use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
5use futures::StreamExt as _;
6use gpui::{AppContext, EntityId, Model, ModelContext, Task};
7use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot};
8use std::{
9 ops::{AddAssign, Range},
10 path::Path,
11 sync::Arc,
12 time::Duration,
13};
14use text::{ToOffset, ToPoint};
15use unicode_segmentation::UnicodeSegmentation;
16
17pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
18
19pub struct SupermavenCompletionProvider {
20 supermaven: Model<Supermaven>,
21 buffer_id: Option<EntityId>,
22 completion_id: Option<SupermavenCompletionStateId>,
23 file_extension: Option<String>,
24 pending_refresh: Task<Result<()>>,
25 telemetry: Option<Arc<Telemetry>>,
26}
27
28impl SupermavenCompletionProvider {
29 pub fn new(supermaven: Model<Supermaven>) -> Self {
30 Self {
31 supermaven,
32 buffer_id: None,
33 completion_id: None,
34 file_extension: None,
35 pending_refresh: Task::ready(Ok(())),
36 telemetry: None,
37 }
38 }
39
40 pub fn with_telemetry(mut self, telemetry: Arc<Telemetry>) -> Self {
41 self.telemetry = Some(telemetry);
42 self
43 }
44}
45
46// Computes the completion state from the difference between the completion text.
47// this is defined by greedily matching the buffer text against the completion text, with any leftover buffer placed at the end.
48// for example, given the completion text "moo cows are cool" and the buffer text "cowsre pool", the completion state would be
49// the inlays "moo ", " a", and "cool" which will render as "[moo ]cows[ a]re [cool]pool" in the editor.
50fn completion_state_from_diff(
51 snapshot: BufferSnapshot,
52 completion_text: &str,
53 position: Anchor,
54 delete_range: Range<Anchor>,
55) -> CompletionProposal {
56 let buffer_text = snapshot
57 .text_for_range(delete_range.clone())
58 .collect::<String>();
59
60 let mut inlays: Vec<InlayProposal> = Vec::new();
61
62 let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect();
63 let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect();
64
65 let mut offset = position.to_offset(&snapshot);
66
67 let mut i = 0;
68 let mut j = 0;
69 while i < completion_graphemes.len() && j < buffer_graphemes.len() {
70 // find the next instance of the buffer text in the completion text.
71 let k = completion_graphemes[i..]
72 .iter()
73 .position(|c| *c == buffer_graphemes[j]);
74 match k {
75 Some(k) => {
76 if k != 0 {
77 // the range from the current position to item is an inlay.
78 inlays.push(InlayProposal::Suggestion(
79 snapshot.anchor_after(offset),
80 completion_graphemes[i..i + k].join("").into(),
81 ));
82 }
83 i += k + 1;
84 j += 1;
85 offset.add_assign(buffer_graphemes[j - 1].len());
86 }
87 None => {
88 // there are no more matching completions, so drop the remaining
89 // completion text as an inlay.
90 break;
91 }
92 }
93 }
94
95 if j == buffer_graphemes.len() && i < completion_graphemes.len() {
96 // there is leftover completion text, so drop it as an inlay.
97 inlays.push(InlayProposal::Suggestion(
98 snapshot.anchor_after(offset),
99 completion_graphemes[i..].join("").into(),
100 ));
101 }
102
103 CompletionProposal {
104 inlays,
105 text: completion_text.into(),
106 delete_range: Some(delete_range),
107 }
108}
109
110impl InlineCompletionProvider for SupermavenCompletionProvider {
111 fn name() -> &'static str {
112 "supermaven"
113 }
114
115 fn is_enabled(&self, buffer: &Model<Buffer>, cursor_position: Anchor, cx: &AppContext) -> bool {
116 if !self.supermaven.read(cx).is_enabled() {
117 return false;
118 }
119
120 let buffer = buffer.read(cx);
121 let file = buffer.file();
122 let language = buffer.language_at(cursor_position);
123 let settings = all_language_settings(file, cx);
124 settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
125 }
126
127 fn refresh(
128 &mut self,
129 buffer_handle: Model<Buffer>,
130 cursor_position: Anchor,
131 debounce: bool,
132 cx: &mut ModelContext<Self>,
133 ) {
134 let Some(mut completion) = self.supermaven.update(cx, |supermaven, cx| {
135 supermaven.complete(&buffer_handle, cursor_position, cx)
136 }) else {
137 return;
138 };
139
140 self.pending_refresh = cx.spawn(|this, mut cx| async move {
141 if debounce {
142 cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
143 }
144
145 while let Some(()) = completion.updates.next().await {
146 this.update(&mut cx, |this, cx| {
147 this.completion_id = Some(completion.id);
148 this.buffer_id = Some(buffer_handle.entity_id());
149 this.file_extension = buffer_handle.read(cx).file().and_then(|file| {
150 Some(
151 Path::new(file.file_name(cx))
152 .extension()?
153 .to_str()?
154 .to_string(),
155 )
156 });
157 cx.notify();
158 })?;
159 }
160 Ok(())
161 });
162 }
163
164 fn cycle(
165 &mut self,
166 _buffer: Model<Buffer>,
167 _cursor_position: Anchor,
168 _direction: Direction,
169 _cx: &mut ModelContext<Self>,
170 ) {
171 }
172
173 fn accept(&mut self, _cx: &mut ModelContext<Self>) {
174 if self.completion_id.is_some() {
175 if let Some(telemetry) = self.telemetry.as_ref() {
176 telemetry.report_inline_completion_event(
177 Self::name().to_string(),
178 true,
179 self.file_extension.clone(),
180 );
181 }
182 }
183 self.pending_refresh = Task::ready(Ok(()));
184 self.completion_id = None;
185 }
186
187 fn discard(
188 &mut self,
189 should_report_inline_completion_event: bool,
190 _cx: &mut ModelContext<Self>,
191 ) {
192 if should_report_inline_completion_event && self.completion_id.is_some() {
193 if let Some(telemetry) = self.telemetry.as_ref() {
194 telemetry.report_inline_completion_event(
195 Self::name().to_string(),
196 false,
197 self.file_extension.clone(),
198 );
199 }
200 }
201
202 self.pending_refresh = Task::ready(Ok(()));
203 self.completion_id = None;
204 }
205
206 fn active_completion_text<'a>(
207 &'a self,
208 buffer: &Model<Buffer>,
209 cursor_position: Anchor,
210 cx: &'a AppContext,
211 ) -> Option<CompletionProposal> {
212 let completion_text = self
213 .supermaven
214 .read(cx)
215 .completion(buffer, cursor_position, cx)?;
216
217 let completion_text = trim_to_end_of_line_unless_leading_newline(completion_text);
218
219 let completion_text = completion_text.trim_end();
220
221 if !completion_text.trim().is_empty() {
222 let snapshot = buffer.read(cx).snapshot();
223 let mut point = cursor_position.to_point(&snapshot);
224 point.column = snapshot.line_len(point.row);
225 let range = cursor_position..snapshot.anchor_after(point);
226 Some(completion_state_from_diff(
227 snapshot,
228 completion_text,
229 cursor_position,
230 range,
231 ))
232 } else {
233 None
234 }
235 }
236}
237
238fn trim_to_end_of_line_unless_leading_newline(text: &str) -> &str {
239 if has_leading_newline(text) {
240 text
241 } else if let Some(i) = text.find('\n') {
242 &text[..i]
243 } else {
244 text
245 }
246}
247
248fn has_leading_newline(text: &str) -> bool {
249 for c in text.chars() {
250 if c == '\n' {
251 return true;
252 }
253 if !c.is_whitespace() {
254 return false;
255 }
256 }
257 false
258}