inline_completion_tests.rs

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