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}