code_completion_tests.rs

  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}