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 is_enabled(
321 &self,
322 _buffer: &gpui::Model<language::Buffer>,
323 _cursor_position: language::Anchor,
324 _cx: &gpui::AppContext,
325 ) -> bool {
326 true
327 }
328
329 fn refresh(
330 &mut self,
331 _buffer: gpui::Model<language::Buffer>,
332 _cursor_position: language::Anchor,
333 _debounce: bool,
334 _cx: &mut gpui::ModelContext<Self>,
335 ) {
336 }
337
338 fn cycle(
339 &mut self,
340 _buffer: gpui::Model<language::Buffer>,
341 _cursor_position: language::Anchor,
342 _direction: inline_completion::Direction,
343 _cx: &mut gpui::ModelContext<Self>,
344 ) {
345 }
346
347 fn accept(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
348
349 fn discard(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
350
351 fn suggest<'a>(
352 &mut self,
353 _buffer: &gpui::Model<language::Buffer>,
354 _cursor_position: language::Anchor,
355 _cx: &mut gpui::ModelContext<Self>,
356 ) -> Option<inline_completion::InlineCompletion> {
357 self.completion.clone()
358 }
359}