1use crate::code_context_menus::CompletionsMenu;
2use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::TestAppContext;
4use language::CodeLabel;
5use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};
6use project::{Completion, CompletionSource};
7use settings::SnippetSortOrder;
8use std::sync::Arc;
9use std::sync::atomic::AtomicBool;
10use text::{Anchor, BufferId};
11
12#[gpui::test]
13async fn test_sort_kind(cx: &mut TestAppContext) {
14 let completions = vec![
15 CompletionBuilder::function("floorf128", None, "80000000"),
16 CompletionBuilder::constant("foo_bar_baz", None, "80000000"),
17 CompletionBuilder::variable("foo_bar_qux", None, "80000000"),
18 ];
19 let matches =
20 filter_and_sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;
21
22 // variable takes precedence over constant
23 // constant take precedence over function
24 assert_eq!(
25 matches
26 .iter()
27 .map(|m| m.string.as_str())
28 .collect::<Vec<_>>(),
29 vec!["foo_bar_qux", "foo_bar_baz", "floorf128"]
30 );
31
32 // fuzzy score should match for first two items as query is common prefix
33 assert_eq!(matches[0].score, matches[1].score);
34}
35
36#[gpui::test]
37async fn test_fuzzy_score(cx: &mut TestAppContext) {
38 // first character sensitive over sort_text and sort_kind
39 {
40 let completions = vec![
41 CompletionBuilder::variable("element_type", None, "7ffffffe"),
42 CompletionBuilder::constant("ElementType", None, "7fffffff"),
43 ];
44 let matches =
45 filter_and_sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;
46 assert_eq!(
47 matches
48 .iter()
49 .map(|m| m.string.as_str())
50 .collect::<Vec<_>>(),
51 vec!["ElementType", "element_type"]
52 );
53 assert!(matches[0].score > matches[1].score);
54 }
55
56 // fuzzy takes over sort_text and sort_kind
57 {
58 let completions = vec![
59 CompletionBuilder::function("onAbort?", None, "12"),
60 CompletionBuilder::function("onAuxClick?", None, "12"),
61 CompletionBuilder::variable("onPlay?", None, "12"),
62 CompletionBuilder::variable("onLoad?", None, "12"),
63 CompletionBuilder::variable("onDrag?", None, "12"),
64 CompletionBuilder::function("onPause?", None, "10"),
65 CompletionBuilder::function("onPaste?", None, "10"),
66 CompletionBuilder::function("onAnimationEnd?", None, "12"),
67 CompletionBuilder::function("onAbortCapture?", None, "12"),
68 CompletionBuilder::constant("onChange?", None, "12"),
69 CompletionBuilder::constant("onWaiting?", None, "12"),
70 CompletionBuilder::function("onCanPlay?", None, "12"),
71 ];
72 let matches =
73 filter_and_sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;
74 for i in 0..4 {
75 assert!(matches[i].string.to_lowercase().starts_with("ona"));
76 }
77 }
78
79 // plain fuzzy prefix match
80 {
81 let completions = vec![
82 CompletionBuilder::function("set_text", None, "7fffffff"),
83 CompletionBuilder::function("set_placeholder_text", None, "7fffffff"),
84 CompletionBuilder::function("set_text_style_refinement", None, "7fffffff"),
85 CompletionBuilder::function("set_context_menu_options", None, "7fffffff"),
86 CompletionBuilder::function("select_to_next_word_end", None, "7fffffff"),
87 CompletionBuilder::function("select_to_next_subword_end", None, "7fffffff"),
88 CompletionBuilder::function("set_custom_context_menu", None, "7fffffff"),
89 CompletionBuilder::function("select_to_end_of_excerpt", None, "7fffffff"),
90 CompletionBuilder::function("select_to_start_of_excerpt", None, "7fffffff"),
91 CompletionBuilder::function("select_to_start_of_next_excerpt", None, "7fffffff"),
92 CompletionBuilder::function("select_to_end_of_previous_excerpt", None, "7fffffff"),
93 ];
94 let matches =
95 filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
96 assert_eq!(matches[0].string, "set_text");
97 assert_eq!(matches[1].string, "set_text_style_refinement");
98 assert_eq!(matches[2].string, "set_placeholder_text");
99 }
100
101 // fuzzy filter text over label, sort_text and sort_kind
102 {
103 // Case 1: "awa"
104 let completions = vec![
105 CompletionBuilder::method("await", Some("await"), "7fffffff"),
106 CompletionBuilder::method("await.ne", Some("ne"), "80000010"),
107 CompletionBuilder::method("await.eq", Some("eq"), "80000010"),
108 CompletionBuilder::method("await.or", Some("or"), "7ffffff8"),
109 CompletionBuilder::method("await.zip", Some("zip"), "80000006"),
110 CompletionBuilder::method("await.xor", Some("xor"), "7ffffff8"),
111 CompletionBuilder::method("await.and", Some("and"), "80000006"),
112 CompletionBuilder::method("await.map", Some("map"), "80000006"),
113 ];
114
115 test_for_each_prefix("await", &completions, cx, |matches| {
116 // for each prefix, first item should always be one with lower sort_text
117 assert_eq!(matches[0].string, "await");
118 })
119 .await;
120 }
121}
122
123#[gpui::test]
124async fn test_sort_text(cx: &mut TestAppContext) {
125 // sort text takes precedance over sort_kind, when fuzzy is same
126 {
127 let completions = vec![
128 CompletionBuilder::variable("unreachable", None, "80000000"),
129 CompletionBuilder::function("unreachable!(…)", None, "7fffffff"),
130 CompletionBuilder::function("unchecked_rem", None, "80000010"),
131 CompletionBuilder::function("unreachable_unchecked", None, "80000020"),
132 ];
133
134 test_for_each_prefix("unreachabl", &completions, cx, |matches| {
135 // for each prefix, first item should always be one with lower sort_text
136 assert_eq!(matches[0].string, "unreachable!(…)");
137 assert_eq!(matches[1].string, "unreachable");
138
139 // fuzzy score should match for first two items as query is common prefix
140 assert_eq!(matches[0].score, matches[1].score);
141 })
142 .await;
143
144 let matches =
145 filter_and_sort_matches("unreachable", &completions, SnippetSortOrder::Top, cx).await;
146 // exact match comes first
147 assert_eq!(matches[0].string, "unreachable");
148 assert_eq!(matches[1].string, "unreachable!(…)");
149
150 // fuzzy score should match for first two items as query is common prefix
151 assert_eq!(matches[0].score, matches[1].score);
152 }
153}
154
155#[gpui::test]
156async fn test_sort_snippet(cx: &mut TestAppContext) {
157 let completions = vec![
158 CompletionBuilder::constant("println", None, "7fffffff"),
159 CompletionBuilder::snippet("println!(…)", None, "80000000"),
160 ];
161 let matches = filter_and_sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;
162
163 // snippet take precedence over sort_text and sort_kind
164 assert_eq!(matches[0].string, "println!(…)");
165}
166
167#[gpui::test]
168async fn test_sort_exact(cx: &mut TestAppContext) {
169 // sort_text takes over if no exact match
170 let completions = vec![
171 CompletionBuilder::function("into", None, "80000004"),
172 CompletionBuilder::function("try_into", None, "80000004"),
173 CompletionBuilder::snippet("println", None, "80000004"),
174 CompletionBuilder::function("clone_into", None, "80000004"),
175 CompletionBuilder::function("into_searcher", None, "80000000"),
176 CompletionBuilder::snippet("eprintln", None, "80000004"),
177 ];
178 let matches =
179 filter_and_sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
180 assert_eq!(matches[0].string, "into_searcher");
181
182 // exact match takes over sort_text
183 let completions = vec![
184 CompletionBuilder::function("into", None, "80000004"),
185 CompletionBuilder::function("try_into", None, "80000004"),
186 CompletionBuilder::function("clone_into", None, "80000004"),
187 CompletionBuilder::function("into_searcher", None, "80000000"),
188 CompletionBuilder::function("split_terminator", None, "7fffffff"),
189 CompletionBuilder::function("rsplit_terminator", None, "7fffffff"),
190 ];
191 let matches =
192 filter_and_sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
193 assert_eq!(matches[0].string, "into");
194}
195
196#[gpui::test]
197async fn test_sort_positions(cx: &mut TestAppContext) {
198 // positions take precedence over fuzzy score and sort_text
199 let completions = vec![
200 CompletionBuilder::function("rounded-full", None, "15788"),
201 CompletionBuilder::variable("rounded-t-full", None, "15846"),
202 CompletionBuilder::variable("rounded-b-full", None, "15731"),
203 CompletionBuilder::function("rounded-tr-full", None, "15866"),
204 ];
205
206 let matches = filter_and_sort_matches(
207 "rounded-full",
208 &completions,
209 SnippetSortOrder::default(),
210 cx,
211 )
212 .await;
213 assert_eq!(matches[0].string, "rounded-full");
214
215 let matches =
216 filter_and_sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;
217 assert_eq!(matches[0].string, "rounded-full");
218}
219
220#[gpui::test]
221async fn test_case_sensitive_match_tie_breaker(cx: &mut TestAppContext) {
222 let completions = vec![
223 CompletionBuilder::variable("abc", None, "11"),
224 CompletionBuilder::variable("ABC", None, "11"),
225 ];
226
227 let matches = filter_and_sort_matches("a", &completions, SnippetSortOrder::default(), cx).await;
228 assert_eq!(
229 matches
230 .iter()
231 .map(|m| m.string.as_str())
232 .collect::<Vec<_>>(),
233 vec!["abc", "ABC"]
234 );
235
236 let matches = filter_and_sort_matches("A", &completions, SnippetSortOrder::default(), cx).await;
237 assert_eq!(
238 matches
239 .iter()
240 .map(|m| m.string.as_str())
241 .collect::<Vec<_>>(),
242 vec!["ABC", "abc"]
243 );
244
245 let matches =
246 filter_and_sort_matches("ab", &completions, SnippetSortOrder::default(), cx).await;
247 assert_eq!(
248 matches
249 .iter()
250 .map(|m| m.string.as_str())
251 .collect::<Vec<_>>(),
252 vec!["abc", "ABC"]
253 );
254
255 let matches =
256 filter_and_sort_matches("AB", &completions, SnippetSortOrder::default(), cx).await;
257 assert_eq!(
258 matches
259 .iter()
260 .map(|m| m.string.as_str())
261 .collect::<Vec<_>>(),
262 vec!["ABC", "abc"]
263 );
264
265 let completions = vec![
266 CompletionBuilder::variable("aBc", None, "11"),
267 CompletionBuilder::variable("Abc", None, "11"),
268 ];
269
270 let matches =
271 filter_and_sort_matches("Ab", &completions, SnippetSortOrder::default(), cx).await;
272 assert_eq!(
273 matches
274 .iter()
275 .map(|m| m.string.as_str())
276 .collect::<Vec<_>>(),
277 vec!["Abc", "aBc"]
278 );
279
280 let matches =
281 filter_and_sort_matches("aB", &completions, SnippetSortOrder::default(), cx).await;
282 assert_eq!(
283 matches
284 .iter()
285 .map(|m| m.string.as_str())
286 .collect::<Vec<_>>(),
287 vec!["aBc", "Abc"]
288 );
289}
290
291#[gpui::test]
292async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
293 let completions = vec![
294 CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score
295 CompletionBuilder::function(
296 "language_servers_running_disk_based_diagnostics",
297 None,
298 "7fffffff",
299 ), // 0.168 fuzzy score
300 CompletionBuilder::function("code_lens", None, "7fffffff"), // 3.2 fuzzy score
301 CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"), // 3.2 fuzzy score
302 CompletionBuilder::function("fetch_code_lens", None, "7fffffff"), // 3.2 fuzzy score
303 ];
304
305 let matches =
306 filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await;
307
308 assert_eq!(matches[0].string, "code_lens");
309 assert_eq!(matches[1].string, "lsp_code_lens");
310 assert_eq!(matches[2].string, "fetch_code_lens");
311}
312
313#[gpui::test]
314async fn test_semver_label_sort_by_latest_version(cx: &mut TestAppContext) {
315 let mut versions = [
316 "10.4.112",
317 "10.4.22",
318 "10.4.2",
319 "10.4.20",
320 "10.4.21",
321 "10.4.12",
322 // Pre-release versions
323 "10.4.22-alpha",
324 "10.4.22-beta.1",
325 "10.4.22-rc.1",
326 // Build metadata versions
327 "10.4.21+build.123",
328 "10.4.20+20210327",
329 ];
330 versions.sort_by(|a, b| {
331 match (
332 semver::Version::parse(a).ok(),
333 semver::Version::parse(b).ok(),
334 ) {
335 (Some(a_ver), Some(b_ver)) => b_ver.cmp(&a_ver),
336 _ => std::cmp::Ordering::Equal,
337 }
338 });
339 let completions: Vec<_> = versions
340 .iter()
341 .enumerate()
342 .map(|(i, version)| {
343 // This sort text would come from the LSP
344 let sort_text = format!("{:08}", i);
345 CompletionBuilder::new(version, None, &sort_text, None)
346 })
347 .collect();
348
349 // Case 1: User types just the major and minor version
350 let matches =
351 filter_and_sort_matches("10.4.", &completions, SnippetSortOrder::default(), cx).await;
352 // Versions are ordered by recency (latest first)
353 let expected_versions = [
354 "10.4.112",
355 "10.4.22",
356 "10.4.22-rc.1",
357 "10.4.22-beta.1",
358 "10.4.22-alpha",
359 "10.4.21+build.123",
360 "10.4.21",
361 "10.4.20+20210327",
362 "10.4.20",
363 "10.4.12",
364 "10.4.2",
365 ];
366 for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {
367 assert_eq!(match_item.string.as_ref() as &str, *expected);
368 }
369
370 // Case 2: User types the major, minor, and patch version
371 let matches =
372 filter_and_sort_matches("10.4.2", &completions, SnippetSortOrder::default(), cx).await;
373 let expected_versions = [
374 // Exact match comes first
375 "10.4.2",
376 // Ordered by recency with exact major, minor, and patch versions
377 "10.4.22",
378 "10.4.22-rc.1",
379 "10.4.22-beta.1",
380 "10.4.22-alpha",
381 "10.4.21+build.123",
382 "10.4.21",
383 "10.4.20+20210327",
384 "10.4.20",
385 // Versions with non-exact patch versions are ordered by fuzzy score
386 // Higher fuzzy score than 112 patch version since "2" appears before "1"
387 // in "12", making it rank higher than "112"
388 "10.4.12",
389 "10.4.112",
390 ];
391 for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {
392 assert_eq!(match_item.string.as_ref() as &str, *expected);
393 }
394}
395
396async fn test_for_each_prefix<F>(
397 target: &str,
398 completions: &Vec<Completion>,
399 cx: &mut TestAppContext,
400 mut test_fn: F,
401) where
402 F: FnMut(Vec<StringMatch>),
403{
404 for i in 1..=target.len() {
405 let prefix = &target[..i];
406 let matches =
407 filter_and_sort_matches(prefix, completions, SnippetSortOrder::default(), cx).await;
408 test_fn(matches);
409 }
410}
411
412struct CompletionBuilder;
413
414impl CompletionBuilder {
415 fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
416 Self::new(
417 label,
418 filter_text,
419 sort_text,
420 Some(CompletionItemKind::CONSTANT),
421 )
422 }
423
424 fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
425 Self::new(
426 label,
427 filter_text,
428 sort_text,
429 Some(CompletionItemKind::FUNCTION),
430 )
431 }
432
433 fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
434 Self::new(
435 label,
436 filter_text,
437 sort_text,
438 Some(CompletionItemKind::METHOD),
439 )
440 }
441
442 fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
443 Self::new(
444 label,
445 filter_text,
446 sort_text,
447 Some(CompletionItemKind::VARIABLE),
448 )
449 }
450
451 fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
452 Self::new(
453 label,
454 filter_text,
455 sort_text,
456 Some(CompletionItemKind::SNIPPET),
457 )
458 }
459
460 fn new(
461 label: &str,
462 filter_text: Option<&str>,
463 sort_text: &str,
464 kind: Option<CompletionItemKind>,
465 ) -> Completion {
466 Completion {
467 replace_range: Anchor::min_max_range_for_buffer(BufferId::new(1).unwrap()),
468 new_text: label.to_string(),
469 label: CodeLabel::plain(label.to_string(), filter_text),
470 documentation: None,
471 source: CompletionSource::Lsp {
472 insert_range: None,
473 server_id: LanguageServerId(0),
474 lsp_completion: Box::new(CompletionItem {
475 label: label.to_string(),
476 kind: kind,
477 sort_text: Some(sort_text.to_string()),
478 filter_text: filter_text.map(|text| text.to_string()),
479 ..Default::default()
480 }),
481 lsp_defaults: None,
482 resolved: false,
483 },
484 icon_path: None,
485 insert_text_mode: None,
486 confirm: None,
487 match_start: None,
488 snippet_deduplication_key: None,
489 }
490 }
491}
492
493async fn filter_and_sort_matches(
494 query: &str,
495 completions: &Vec<Completion>,
496 snippet_sort_order: SnippetSortOrder,
497 cx: &mut TestAppContext,
498) -> Vec<StringMatch> {
499 let candidates: Arc<[StringMatchCandidate]> = completions
500 .iter()
501 .enumerate()
502 .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
503 .collect();
504 let cancel_flag = Arc::new(AtomicBool::new(false));
505 let background_executor = cx.executor();
506 let matches = fuzzy::match_strings(
507 &candidates,
508 query,
509 query.chars().any(|c| c.is_uppercase()),
510 false,
511 100,
512 &cancel_flag,
513 background_executor,
514 )
515 .await;
516 CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions)
517}