supermaven_completion_provider.rs

  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}