446 lines · 16.1 KB · Rust
RawDownload
use crate::code_context_menus::CompletionsMenu;use fuzzy::{StringMatch, StringMatchCandidate};use gpui::TestAppContext;use language::CodeLabel;use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};use project::{Completion, CompletionSource};use settings::SnippetSortOrder;use std::sync::Arc;use std::sync::atomic::AtomicBool;use text::Anchor; #[gpui::test]async fn test_sort_kind(cx: &mut TestAppContext) {    let completions = vec![        CompletionBuilder::function("floorf128", None, "80000000"),        CompletionBuilder::constant("foo_bar_baz", None, "80000000"),        CompletionBuilder::variable("foo_bar_qux", None, "80000000"),    ];    let matches =        filter_and_sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;     // variable takes precedence over constant    // constant take precedence over function    assert_eq!(        matches            .iter()            .map(|m| m.string.as_str())            .collect::<Vec<_>>(),        vec!["foo_bar_qux", "foo_bar_baz", "floorf128"]    );     // fuzzy score should match for first two items as query is common prefix    assert_eq!(matches[0].score, matches[1].score);} #[gpui::test]async fn test_fuzzy_score(cx: &mut TestAppContext) {    // first character sensitive over sort_text and sort_kind    {        let completions = vec![            CompletionBuilder::variable("element_type", None, "7ffffffe"),            CompletionBuilder::constant("ElementType", None, "7fffffff"),        ];        let matches =            filter_and_sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;        assert_eq!(            matches                .iter()                .map(|m| m.string.as_str())                .collect::<Vec<_>>(),            vec!["ElementType", "element_type"]        );        assert!(matches[0].score > matches[1].score);    }     // fuzzy takes over sort_text and sort_kind    {        let completions = vec![            CompletionBuilder::function("onAbort?", None, "12"),            CompletionBuilder::function("onAuxClick?", None, "12"),            CompletionBuilder::variable("onPlay?", None, "12"),            CompletionBuilder::variable("onLoad?", None, "12"),            CompletionBuilder::variable("onDrag?", None, "12"),            CompletionBuilder::function("onPause?", None, "10"),            CompletionBuilder::function("onPaste?", None, "10"),            CompletionBuilder::function("onAnimationEnd?", None, "12"),            CompletionBuilder::function("onAbortCapture?", None, "12"),            CompletionBuilder::constant("onChange?", None, "12"),            CompletionBuilder::constant("onWaiting?", None, "12"),            CompletionBuilder::function("onCanPlay?", None, "12"),        ];        let matches =            filter_and_sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;        for i in 0..4 {            assert!(matches[i].string.to_lowercase().starts_with("ona"));        }    }     // plain fuzzy prefix match    {        let completions = vec![            CompletionBuilder::function("set_text", None, "7fffffff"),            CompletionBuilder::function("set_placeholder_text", None, "7fffffff"),            CompletionBuilder::function("set_text_style_refinement", None, "7fffffff"),            CompletionBuilder::function("set_context_menu_options", None, "7fffffff"),            CompletionBuilder::function("select_to_next_word_end", None, "7fffffff"),            CompletionBuilder::function("select_to_next_subword_end", None, "7fffffff"),            CompletionBuilder::function("set_custom_context_menu", None, "7fffffff"),            CompletionBuilder::function("select_to_end_of_excerpt", None, "7fffffff"),            CompletionBuilder::function("select_to_start_of_excerpt", None, "7fffffff"),            CompletionBuilder::function("select_to_start_of_next_excerpt", None, "7fffffff"),            CompletionBuilder::function("select_to_end_of_previous_excerpt", None, "7fffffff"),        ];        let matches =            filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;        assert_eq!(matches[0].string, "set_text");        assert_eq!(matches[1].string, "set_text_style_refinement");        assert_eq!(matches[2].string, "set_placeholder_text");    }     // fuzzy filter text over label, sort_text and sort_kind    {        // Case 1: "awa"        let completions = vec![            CompletionBuilder::method("await", Some("await"), "7fffffff"),            CompletionBuilder::method("await.ne", Some("ne"), "80000010"),            CompletionBuilder::method("await.eq", Some("eq"), "80000010"),            CompletionBuilder::method("await.or", Some("or"), "7ffffff8"),            CompletionBuilder::method("await.zip", Some("zip"), "80000006"),            CompletionBuilder::method("await.xor", Some("xor"), "7ffffff8"),            CompletionBuilder::method("await.and", Some("and"), "80000006"),            CompletionBuilder::method("await.map", Some("map"), "80000006"),        ];         test_for_each_prefix("await", &completions, cx, |matches| {            // for each prefix, first item should always be one with lower sort_text            assert_eq!(matches[0].string, "await");        })        .await;    }} #[gpui::test]async fn test_sort_text(cx: &mut TestAppContext) {    // sort text takes precedance over sort_kind, when fuzzy is same    {        let completions = vec![            CompletionBuilder::variable("unreachable", None, "80000000"),            CompletionBuilder::function("unreachable!(…)", None, "7fffffff"),            CompletionBuilder::function("unchecked_rem", None, "80000010"),            CompletionBuilder::function("unreachable_unchecked", None, "80000020"),        ];         test_for_each_prefix("unreachabl", &completions, cx, |matches| {            // for each prefix, first item should always be one with lower sort_text            assert_eq!(matches[0].string, "unreachable!(…)");            assert_eq!(matches[1].string, "unreachable");             // fuzzy score should match for first two items as query is common prefix            assert_eq!(matches[0].score, matches[1].score);        })        .await;         let matches =            filter_and_sort_matches("unreachable", &completions, SnippetSortOrder::Top, cx).await;        // exact match comes first        assert_eq!(matches[0].string, "unreachable");        assert_eq!(matches[1].string, "unreachable!(…)");         // fuzzy score should match for first two items as query is common prefix        assert_eq!(matches[0].score, matches[1].score);    }} #[gpui::test]async fn test_sort_snippet(cx: &mut TestAppContext) {    let completions = vec![        CompletionBuilder::constant("println", None, "7fffffff"),        CompletionBuilder::snippet("println!(…)", None, "80000000"),    ];    let matches = filter_and_sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;     // snippet take precedence over sort_text and sort_kind    assert_eq!(matches[0].string, "println!(…)");} #[gpui::test]async fn test_sort_exact(cx: &mut TestAppContext) {    // sort_text takes over if no exact match    let completions = vec![        CompletionBuilder::function("into", None, "80000004"),        CompletionBuilder::function("try_into", None, "80000004"),        CompletionBuilder::snippet("println", None, "80000004"),        CompletionBuilder::function("clone_into", None, "80000004"),        CompletionBuilder::function("into_searcher", None, "80000000"),        CompletionBuilder::snippet("eprintln", None, "80000004"),    ];    let matches =        filter_and_sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;    assert_eq!(matches[0].string, "into_searcher");     // exact match takes over sort_text    let completions = vec![        CompletionBuilder::function("into", None, "80000004"),        CompletionBuilder::function("try_into", None, "80000004"),        CompletionBuilder::function("clone_into", None, "80000004"),        CompletionBuilder::function("into_searcher", None, "80000000"),        CompletionBuilder::function("split_terminator", None, "7fffffff"),        CompletionBuilder::function("rsplit_terminator", None, "7fffffff"),    ];    let matches =        filter_and_sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;    assert_eq!(matches[0].string, "into");} #[gpui::test]async fn test_sort_positions(cx: &mut TestAppContext) {    // positions take precedence over fuzzy score and sort_text    let completions = vec![        CompletionBuilder::function("rounded-full", None, "15788"),        CompletionBuilder::variable("rounded-t-full", None, "15846"),        CompletionBuilder::variable("rounded-b-full", None, "15731"),        CompletionBuilder::function("rounded-tr-full", None, "15866"),    ];     let matches = filter_and_sort_matches(        "rounded-full",        &completions,        SnippetSortOrder::default(),        cx,    )    .await;    assert_eq!(matches[0].string, "rounded-full");     let matches =        filter_and_sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;    assert_eq!(matches[0].string, "rounded-full");} #[gpui::test]async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {    let completions = vec![        CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score        CompletionBuilder::function(            "language_servers_running_disk_based_diagnostics",            None,            "7fffffff",        ), // 0.168 fuzzy score        CompletionBuilder::function("code_lens", None, "7fffffff"),           // 3.2 fuzzy score        CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"),       // 3.2 fuzzy score        CompletionBuilder::function("fetch_code_lens", None, "7fffffff"),     // 3.2 fuzzy score    ];     let matches =        filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await;     assert_eq!(matches[0].string, "code_lens");    assert_eq!(matches[1].string, "lsp_code_lens");    assert_eq!(matches[2].string, "fetch_code_lens");} #[gpui::test]async fn test_semver_label_sort_by_latest_version(cx: &mut TestAppContext) {    let mut versions = [        "10.4.112",        "10.4.22",        "10.4.2",        "10.4.20",        "10.4.21",        "10.4.12",        // Pre-release versions        "10.4.22-alpha",        "10.4.22-beta.1",        "10.4.22-rc.1",        // Build metadata versions        "10.4.21+build.123",        "10.4.20+20210327",    ];    versions.sort_by(|a, b| {        match (            semver::Version::parse(a).ok(),            semver::Version::parse(b).ok(),        ) {            (Some(a_ver), Some(b_ver)) => b_ver.cmp(&a_ver),            _ => std::cmp::Ordering::Equal,        }    });    let completions: Vec<_> = versions        .iter()        .enumerate()        .map(|(i, version)| {            // This sort text would come from the LSP            let sort_text = format!("{:08}", i);            CompletionBuilder::new(version, None, &sort_text, None)        })        .collect();     // Case 1: User types just the major and minor version    let matches =        filter_and_sort_matches("10.4.", &completions, SnippetSortOrder::default(), cx).await;    // Versions are ordered by recency (latest first)    let expected_versions = [        "10.4.112",        "10.4.22",        "10.4.22-rc.1",        "10.4.22-beta.1",        "10.4.22-alpha",        "10.4.21+build.123",        "10.4.21",        "10.4.20+20210327",        "10.4.20",        "10.4.12",        "10.4.2",    ];    for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {        assert_eq!(match_item.string.as_ref() as &str, *expected);    }     // Case 2: User types the major, minor, and patch version    let matches =        filter_and_sort_matches("10.4.2", &completions, SnippetSortOrder::default(), cx).await;    let expected_versions = [        // Exact match comes first        "10.4.2",        // Ordered by recency with exact major, minor, and patch versions        "10.4.22",        "10.4.22-rc.1",        "10.4.22-beta.1",        "10.4.22-alpha",        "10.4.21+build.123",        "10.4.21",        "10.4.20+20210327",        "10.4.20",        // Versions with non-exact patch versions are ordered by fuzzy score        // Higher fuzzy score than 112 patch version since "2" appears before "1"        // in "12", making it rank higher than "112"        "10.4.12",        "10.4.112",    ];    for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {        assert_eq!(match_item.string.as_ref() as &str, *expected);    }} async fn test_for_each_prefix<F>(    target: &str,    completions: &Vec<Completion>,    cx: &mut TestAppContext,    mut test_fn: F,) where    F: FnMut(Vec<StringMatch>),{    for i in 1..=target.len() {        let prefix = &target[..i];        let matches =            filter_and_sort_matches(prefix, completions, SnippetSortOrder::default(), cx).await;        test_fn(matches);    }} struct CompletionBuilder; impl CompletionBuilder {    fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {        Self::new(            label,            filter_text,            sort_text,            Some(CompletionItemKind::CONSTANT),        )    }     fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {        Self::new(            label,            filter_text,            sort_text,            Some(CompletionItemKind::FUNCTION),        )    }     fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {        Self::new(            label,            filter_text,            sort_text,            Some(CompletionItemKind::METHOD),        )    }     fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {        Self::new(            label,            filter_text,            sort_text,            Some(CompletionItemKind::VARIABLE),        )    }     fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {        Self::new(            label,            filter_text,            sort_text,            Some(CompletionItemKind::SNIPPET),        )    }     fn new(        label: &str,        filter_text: Option<&str>,        sort_text: &str,        kind: Option<CompletionItemKind>,    ) -> Completion {        Completion {            replace_range: Anchor::MIN..Anchor::MAX,            new_text: label.to_string(),            label: CodeLabel::plain(label.to_string(), filter_text),            documentation: None,            source: CompletionSource::Lsp {                insert_range: None,                server_id: LanguageServerId(0),                lsp_completion: Box::new(CompletionItem {                    label: label.to_string(),                    kind: kind,                    sort_text: Some(sort_text.to_string()),                    filter_text: filter_text.map(|text| text.to_string()),                    ..Default::default()                }),                lsp_defaults: None,                resolved: false,            },            icon_path: None,            insert_text_mode: None,            confirm: None,            match_start: None,            snippet_deduplication_key: None,        }    }} async fn filter_and_sort_matches(    query: &str,    completions: &Vec<Completion>,    snippet_sort_order: SnippetSortOrder,    cx: &mut TestAppContext,) -> Vec<StringMatch> {    let candidates: Arc<[StringMatchCandidate]> = completions        .iter()        .enumerate()        .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))        .collect();    let cancel_flag = Arc::new(AtomicBool::new(false));    let background_executor = cx.executor();    let matches = fuzzy::match_strings(        &candidates,        query,        query.chars().any(|c| c.is_uppercase()),        false,        100,        &cancel_flag,        background_executor,    )    .await;    CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions)}