inline_completion_tests.rs

  1use gpui::{prelude::*, Entity};
  2use indoc::indoc;
  3use inline_completion::EditPredictionProvider;
  4use language::{Language, LanguageConfig};
  5use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
  6use project::Project;
  7use std::{num::NonZeroU32, ops::Range, sync::Arc};
  8use text::{Point, ToOffset};
  9
 10use crate::{
 11    editor_tests::init_test, test::editor_test_context::EditorTestContext, InlineCompletion,
 12};
 13
 14#[gpui::test]
 15async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) {
 16    init_test(cx, |_| {});
 17
 18    let mut cx = EditorTestContext::new(cx).await;
 19    let provider = cx.new(|_| FakeInlineCompletionProvider::default());
 20    assign_editor_completion_provider(provider.clone(), &mut cx);
 21    cx.set_state("let absolute_zero_celsius = ˇ;");
 22
 23    propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx);
 24    cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
 25
 26    assert_editor_active_edit_completion(&mut cx, |_, edits| {
 27        assert_eq!(edits.len(), 1);
 28        assert_eq!(edits[0].1.as_str(), "-273.15");
 29    });
 30
 31    accept_completion(&mut cx);
 32
 33    cx.assert_editor_state("let absolute_zero_celsius = -273.15ˇ;")
 34}
 35
 36#[gpui::test]
 37async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) {
 38    init_test(cx, |_| {});
 39
 40    let mut cx = EditorTestContext::new(cx).await;
 41    let provider = cx.new(|_| FakeInlineCompletionProvider::default());
 42    assign_editor_completion_provider(provider.clone(), &mut cx);
 43    cx.set_state("let pi = ˇ\"foo\";");
 44
 45    propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx);
 46    cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
 47
 48    assert_editor_active_edit_completion(&mut cx, |_, edits| {
 49        assert_eq!(edits.len(), 1);
 50        assert_eq!(edits[0].1.as_str(), "3.14159");
 51    });
 52
 53    accept_completion(&mut cx);
 54
 55    cx.assert_editor_state("let pi = 3.14159ˇ;")
 56}
 57
 58#[gpui::test]
 59async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
 60    init_test(cx, |_| {});
 61
 62    let mut cx = EditorTestContext::new(cx).await;
 63    let provider = cx.new(|_| FakeInlineCompletionProvider::default());
 64    assign_editor_completion_provider(provider.clone(), &mut cx);
 65
 66    // Cursor is 2+ lines above the proposed edit
 67    cx.set_state(indoc! {"
 68        line 0
 69        line ˇ1
 70        line 2
 71        line 3
 72        line
 73    "});
 74
 75    propose_edits(
 76        &provider,
 77        vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
 78        &mut cx,
 79    );
 80
 81    cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
 82    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
 83        assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3));
 84    });
 85
 86    // When accepting, cursor is moved to the proposed location
 87    accept_completion(&mut cx);
 88    cx.assert_editor_state(indoc! {"
 89        line 0
 90        line 1
 91        line 2
 92        line 3
 93        linˇe
 94    "});
 95
 96    // Cursor is 2+ lines below the proposed edit
 97    cx.set_state(indoc! {"
 98        line 0
 99        line
100        line 2
101        line 3
102        line ˇ4
103    "});
104
105    propose_edits(
106        &provider,
107        vec![(Point::new(1, 3)..Point::new(1, 3), " 1")],
108        &mut cx,
109    );
110
111    cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
112    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
113        assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3));
114    });
115
116    // When accepting, cursor is moved to the proposed location
117    accept_completion(&mut cx);
118    cx.assert_editor_state(indoc! {"
119        line 0
120        linˇe
121        line 2
122        line 3
123        line 4
124    "});
125}
126
127#[gpui::test]
128async fn test_indentation(cx: &mut gpui::TestAppContext) {
129    init_test(cx, |settings| {
130        settings.defaults.tab_size = NonZeroU32::new(4)
131    });
132
133    let language = Arc::new(
134        Language::new(
135            LanguageConfig::default(),
136            Some(tree_sitter_rust::LANGUAGE.into()),
137        )
138        .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
139        .unwrap(),
140    );
141
142    let mut cx = EditorTestContext::new(cx).await;
143    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
144    let provider = cx.new(|_| FakeInlineCompletionProvider::default());
145    assign_editor_completion_provider(provider.clone(), &mut cx);
146
147    cx.set_state(indoc! {"
148        const a: A = (
149        ˇ
150        );
151    "});
152
153    propose_edits(
154        &provider,
155        vec![(Point::new(1, 0)..Point::new(1, 0), "    const function()")],
156        &mut cx,
157    );
158    cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
159
160    assert_editor_active_edit_completion(&mut cx, |_, edits| {
161        assert_eq!(edits.len(), 1);
162        assert_eq!(edits[0].1.as_str(), "    const function()");
163    });
164
165    // When the cursor is before the suggested indentation level, accepting a
166    // completion should just indent.
167    accept_completion(&mut cx);
168    cx.assert_editor_state(indoc! {"
169        const a: A = (
170            ˇ
171        );
172    "});
173}
174
175#[gpui::test]
176async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
177    init_test(cx, |_| {});
178
179    let mut cx = EditorTestContext::new(cx).await;
180    let provider = cx.new(|_| FakeInlineCompletionProvider::default());
181    assign_editor_completion_provider(provider.clone(), &mut cx);
182
183    // Cursor is 3+ lines above the proposed edit
184    cx.set_state(indoc! {"
185        line 0
186        line ˇ1
187        line 2
188        line 3
189        line 4
190        line
191    "});
192    let edit_location = Point::new(5, 3);
193
194    propose_edits(
195        &provider,
196        vec![(edit_location..edit_location, " 5")],
197        &mut cx,
198    );
199
200    cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
201    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
202        assert_eq!(move_target.to_point(&snapshot), edit_location);
203    });
204
205    // If we move *towards* the completion, it stays active
206    cx.set_selections_state(indoc! {"
207        line 0
208        line 1
209        line ˇ2
210        line 3
211        line 4
212        line
213    "});
214    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
215        assert_eq!(move_target.to_point(&snapshot), edit_location);
216    });
217
218    // If we move *away* from the completion, it is discarded
219    cx.set_selections_state(indoc! {"
220        line ˇ0
221        line 1
222        line 2
223        line 3
224        line 4
225        line
226    "});
227    cx.editor(|editor, _, _| {
228        assert!(editor.active_inline_completion.is_none());
229    });
230
231    // Cursor is 3+ lines below the proposed edit
232    cx.set_state(indoc! {"
233        line
234        line 1
235        line 2
236        line 3
237        line ˇ4
238        line 5
239    "});
240    let edit_location = Point::new(0, 3);
241
242    propose_edits(
243        &provider,
244        vec![(edit_location..edit_location, " 0")],
245        &mut cx,
246    );
247
248    cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
249    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
250        assert_eq!(move_target.to_point(&snapshot), edit_location);
251    });
252
253    // If we move *towards* the completion, it stays active
254    cx.set_selections_state(indoc! {"
255        line
256        line 1
257        line 2
258        line ˇ3
259        line 4
260        line 5
261    "});
262    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
263        assert_eq!(move_target.to_point(&snapshot), edit_location);
264    });
265
266    // If we move *away* from the completion, it is discarded
267    cx.set_selections_state(indoc! {"
268        line
269        line 1
270        line 2
271        line 3
272        line 4
273        line ˇ5
274    "});
275    cx.editor(|editor, _, _| {
276        assert!(editor.active_inline_completion.is_none());
277    });
278}
279
280fn assert_editor_active_edit_completion(
281    cx: &mut EditorTestContext,
282    assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
283) {
284    cx.editor(|editor, _, cx| {
285        let completion_state = editor
286            .active_inline_completion
287            .as_ref()
288            .expect("editor has no active completion");
289
290        if let InlineCompletion::Edit { edits, .. } = &completion_state.completion {
291            assert(editor.buffer().read(cx).snapshot(cx), edits);
292        } else {
293            panic!("expected edit completion");
294        }
295    })
296}
297
298fn assert_editor_active_move_completion(
299    cx: &mut EditorTestContext,
300    assert: impl FnOnce(MultiBufferSnapshot, Anchor),
301) {
302    cx.editor(|editor, _, cx| {
303        let completion_state = editor
304            .active_inline_completion
305            .as_ref()
306            .expect("editor has no active completion");
307
308        if let InlineCompletion::Move { target, .. } = &completion_state.completion {
309            assert(editor.buffer().read(cx).snapshot(cx), *target);
310        } else {
311            panic!("expected move completion");
312        }
313    })
314}
315
316fn accept_completion(cx: &mut EditorTestContext) {
317    cx.update_editor(|editor, window, cx| {
318        editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
319    })
320}
321
322fn propose_edits<T: ToOffset>(
323    provider: &Entity<FakeInlineCompletionProvider>,
324    edits: Vec<(Range<T>, &str)>,
325    cx: &mut EditorTestContext,
326) {
327    let snapshot = cx.buffer_snapshot();
328    let edits = edits.into_iter().map(|(range, text)| {
329        let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
330        (range, text.into())
331    });
332
333    cx.update(|_, cx| {
334        provider.update(cx, |provider, _| {
335            provider.set_inline_completion(Some(inline_completion::InlineCompletion {
336                id: None,
337                edits: edits.collect(),
338                edit_preview: None,
339            }))
340        })
341    });
342}
343
344fn assign_editor_completion_provider(
345    provider: Entity<FakeInlineCompletionProvider>,
346    cx: &mut EditorTestContext,
347) {
348    cx.update_editor(|editor, window, cx| {
349        editor.set_edit_prediction_provider(Some(provider), window, cx);
350    })
351}
352
353#[derive(Default, Clone)]
354struct FakeInlineCompletionProvider {
355    completion: Option<inline_completion::InlineCompletion>,
356}
357
358impl FakeInlineCompletionProvider {
359    pub fn set_inline_completion(
360        &mut self,
361        completion: Option<inline_completion::InlineCompletion>,
362    ) {
363        self.completion = completion;
364    }
365}
366
367impl EditPredictionProvider for FakeInlineCompletionProvider {
368    fn name() -> &'static str {
369        "fake-completion-provider"
370    }
371
372    fn display_name() -> &'static str {
373        "Fake Completion Provider"
374    }
375
376    fn show_completions_in_menu() -> bool {
377        false
378    }
379
380    fn is_enabled(
381        &self,
382        _buffer: &gpui::Entity<language::Buffer>,
383        _cursor_position: language::Anchor,
384        _cx: &gpui::App,
385    ) -> bool {
386        true
387    }
388
389    fn is_refreshing(&self) -> bool {
390        false
391    }
392
393    fn refresh(
394        &mut self,
395        _project: Option<Entity<Project>>,
396        _buffer: gpui::Entity<language::Buffer>,
397        _cursor_position: language::Anchor,
398        _debounce: bool,
399        _cx: &mut gpui::Context<Self>,
400    ) {
401    }
402
403    fn cycle(
404        &mut self,
405        _buffer: gpui::Entity<language::Buffer>,
406        _cursor_position: language::Anchor,
407        _direction: inline_completion::Direction,
408        _cx: &mut gpui::Context<Self>,
409    ) {
410    }
411
412    fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
413
414    fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
415
416    fn suggest<'a>(
417        &mut self,
418        _buffer: &gpui::Entity<language::Buffer>,
419        _cursor_position: language::Anchor,
420        _cx: &mut gpui::Context<Self>,
421    ) -> Option<inline_completion::InlineCompletion> {
422        self.completion.clone()
423    }
424}