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