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};
 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}