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