inline_completion_tests.rs

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