supermaven_completion_provider.rs

  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}