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}