1use gpui::Model;
2use indoc::indoc;
3use inline_completion::InlineCompletionProvider;
4use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
5use std::ops::Range;
6use text::{Point, ToOffset};
7use ui::Context;
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_model(|_| 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, cx| editor.update_visible_inline_completion(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_model(|_| 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, cx| editor.update_visible_inline_completion(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_model(|_| 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, cx| editor.update_visible_inline_completion(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, cx| editor.update_visible_inline_completion(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_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
128 init_test(cx, |_| {});
129
130 let mut cx = EditorTestContext::new(cx).await;
131 let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
132 assign_editor_completion_provider(provider.clone(), &mut cx);
133
134 // Cursor is 3+ lines above the proposed edit
135 cx.set_state(indoc! {"
136 line 0
137 line ˇ1
138 line 2
139 line 3
140 line 4
141 line
142 "});
143 let edit_location = Point::new(5, 3);
144
145 propose_edits(
146 &provider,
147 vec![(edit_location..edit_location, " 5")],
148 &mut cx,
149 );
150
151 cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
152 assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
153 assert_eq!(move_target.to_point(&snapshot), edit_location);
154 });
155
156 // If we move *towards* the completion, it stays active
157 cx.set_selections_state(indoc! {"
158 line 0
159 line 1
160 line ˇ2
161 line 3
162 line 4
163 line
164 "});
165 assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
166 assert_eq!(move_target.to_point(&snapshot), edit_location);
167 });
168
169 // If we move *away* from the completion, it is discarded
170 cx.set_selections_state(indoc! {"
171 line ˇ0
172 line 1
173 line 2
174 line 3
175 line 4
176 line
177 "});
178 cx.editor(|editor, _| {
179 assert!(editor.active_inline_completion.is_none());
180 });
181
182 // Cursor is 3+ lines below the proposed edit
183 cx.set_state(indoc! {"
184 line
185 line 1
186 line 2
187 line 3
188 line ˇ4
189 line 5
190 "});
191 let edit_location = Point::new(0, 3);
192
193 propose_edits(
194 &provider,
195 vec![(edit_location..edit_location, " 0")],
196 &mut cx,
197 );
198
199 cx.update_editor(|editor, cx| editor.update_visible_inline_completion(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
207 line 1
208 line 2
209 line ˇ3
210 line 4
211 line 5
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
220 line 1
221 line 2
222 line 3
223 line 4
224 line ˇ5
225 "});
226 cx.editor(|editor, _| {
227 assert!(editor.active_inline_completion.is_none());
228 });
229}
230
231fn assert_editor_active_edit_completion(
232 cx: &mut EditorTestContext,
233 assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
234) {
235 cx.editor(|editor, cx| {
236 let completion_state = editor
237 .active_inline_completion
238 .as_ref()
239 .expect("editor has no active completion");
240
241 if let InlineCompletion::Edit(edits) = &completion_state.completion {
242 assert(editor.buffer().read(cx).snapshot(cx), edits);
243 } else {
244 panic!("expected edit completion");
245 }
246 })
247}
248
249fn assert_editor_active_move_completion(
250 cx: &mut EditorTestContext,
251 assert: impl FnOnce(MultiBufferSnapshot, Anchor),
252) {
253 cx.editor(|editor, cx| {
254 let completion_state = editor
255 .active_inline_completion
256 .as_ref()
257 .expect("editor has no active completion");
258
259 if let InlineCompletion::Move(anchor) = &completion_state.completion {
260 assert(editor.buffer().read(cx).snapshot(cx), *anchor);
261 } else {
262 panic!("expected move completion");
263 }
264 })
265}
266
267fn accept_completion(cx: &mut EditorTestContext) {
268 cx.update_editor(|editor, cx| {
269 editor.accept_inline_completion(&crate::AcceptInlineCompletion, cx)
270 })
271}
272
273fn propose_edits<T: ToOffset>(
274 provider: &Model<FakeInlineCompletionProvider>,
275 edits: Vec<(Range<T>, &str)>,
276 cx: &mut EditorTestContext,
277) {
278 let snapshot = cx.buffer_snapshot();
279 let edits = edits.into_iter().map(|(range, text)| {
280 let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
281 (range, text.into())
282 });
283
284 cx.update(|cx| {
285 provider.update(cx, |provider, _| {
286 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
287 edits: edits.collect(),
288 }))
289 })
290 });
291}
292
293fn assign_editor_completion_provider(
294 provider: Model<FakeInlineCompletionProvider>,
295 cx: &mut EditorTestContext,
296) {
297 cx.update_editor(|editor, cx| {
298 editor.set_inline_completion_provider(Some(provider), cx);
299 })
300}
301
302#[derive(Default, Clone)]
303struct FakeInlineCompletionProvider {
304 completion: Option<inline_completion::InlineCompletion>,
305}
306
307impl FakeInlineCompletionProvider {
308 pub fn set_inline_completion(
309 &mut self,
310 completion: Option<inline_completion::InlineCompletion>,
311 ) {
312 self.completion = completion;
313 }
314}
315
316impl InlineCompletionProvider for FakeInlineCompletionProvider {
317 fn name() -> &'static str {
318 "fake-completion-provider"
319 }
320
321 fn is_enabled(
322 &self,
323 _buffer: &gpui::Model<language::Buffer>,
324 _cursor_position: language::Anchor,
325 _cx: &gpui::AppContext,
326 ) -> bool {
327 true
328 }
329
330 fn refresh(
331 &mut self,
332 _buffer: gpui::Model<language::Buffer>,
333 _cursor_position: language::Anchor,
334 _debounce: bool,
335 _cx: &mut gpui::ModelContext<Self>,
336 ) {
337 }
338
339 fn cycle(
340 &mut self,
341 _buffer: gpui::Model<language::Buffer>,
342 _cursor_position: language::Anchor,
343 _direction: inline_completion::Direction,
344 _cx: &mut gpui::ModelContext<Self>,
345 ) {
346 }
347
348 fn accept(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
349
350 fn discard(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
351
352 fn suggest<'a>(
353 &mut self,
354 _buffer: &gpui::Model<language::Buffer>,
355 _cursor_position: language::Anchor,
356 _cx: &mut gpui::ModelContext<Self>,
357 ) -> Option<inline_completion::InlineCompletion> {
358 self.completion.clone()
359 }
360}