code_completion_tests.rs

  1use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder};
  2use fuzzy::{StringMatch, StringMatchCandidate};
  3use gpui::TestAppContext;
  4use language::CodeLabel;
  5use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};
  6use project::{Completion, CompletionSource};
  7use std::sync::Arc;
  8use std::sync::atomic::AtomicBool;
  9use text::Anchor;
 10
 11#[gpui::test]
 12async fn test_sort_kind(cx: &mut TestAppContext) {
 13    let completions = vec![
 14        CompletionBuilder::function("floorf128", None, "80000000"),
 15        CompletionBuilder::constant("foo_bar_baz", None, "80000000"),
 16        CompletionBuilder::variable("foo_bar_qux", None, "80000000"),
 17    ];
 18    let matches =
 19        filter_and_sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;
 20
 21    // variable takes precedence over constant
 22    // constant take precedence over function
 23    assert_eq!(
 24        matches
 25            .iter()
 26            .map(|m| m.string.as_str())
 27            .collect::<Vec<_>>(),
 28        vec!["foo_bar_qux", "foo_bar_baz", "floorf128"]
 29    );
 30
 31    // fuzzy score should match for first two items as query is common prefix
 32    assert_eq!(matches[0].score, matches[1].score);
 33}
 34
 35#[gpui::test]
 36async fn test_fuzzy_score(cx: &mut TestAppContext) {
 37    // first character sensitive over sort_text and sort_kind
 38    {
 39        let completions = vec![
 40            CompletionBuilder::variable("element_type", None, "7ffffffe"),
 41            CompletionBuilder::constant("ElementType", None, "7fffffff"),
 42        ];
 43        let matches =
 44            filter_and_sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;
 45        assert_eq!(
 46            matches
 47                .iter()
 48                .map(|m| m.string.as_str())
 49                .collect::<Vec<_>>(),
 50            vec!["ElementType", "element_type"]
 51        );
 52        assert!(matches[0].score > matches[1].score);
 53    }
 54
 55    // fuzzy takes over sort_text and sort_kind
 56    {
 57        let completions = vec![
 58            CompletionBuilder::function("onAbort?", None, "12"),
 59            CompletionBuilder::function("onAuxClick?", None, "12"),
 60            CompletionBuilder::variable("onPlay?", None, "12"),
 61            CompletionBuilder::variable("onLoad?", None, "12"),
 62            CompletionBuilder::variable("onDrag?", None, "12"),
 63            CompletionBuilder::function("onPause?", None, "10"),
 64            CompletionBuilder::function("onPaste?", None, "10"),
 65            CompletionBuilder::function("onAnimationEnd?", None, "12"),
 66            CompletionBuilder::function("onAbortCapture?", None, "12"),
 67            CompletionBuilder::constant("onChange?", None, "12"),
 68            CompletionBuilder::constant("onWaiting?", None, "12"),
 69            CompletionBuilder::function("onCanPlay?", None, "12"),
 70        ];
 71        let matches =
 72            filter_and_sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;
 73        for i in 0..4 {
 74            assert!(matches[i].string.to_lowercase().starts_with("ona"));
 75        }
 76    }
 77
 78    // plain fuzzy prefix match
 79    {
 80        let completions = vec![
 81            CompletionBuilder::function("set_text", None, "7fffffff"),
 82            CompletionBuilder::function("set_placeholder_text", None, "7fffffff"),
 83            CompletionBuilder::function("set_text_style_refinement", None, "7fffffff"),
 84            CompletionBuilder::function("set_context_menu_options", None, "7fffffff"),
 85            CompletionBuilder::function("select_to_next_word_end", None, "7fffffff"),
 86            CompletionBuilder::function("select_to_next_subword_end", None, "7fffffff"),
 87            CompletionBuilder::function("set_custom_context_menu", None, "7fffffff"),
 88            CompletionBuilder::function("select_to_end_of_excerpt", None, "7fffffff"),
 89            CompletionBuilder::function("select_to_start_of_excerpt", None, "7fffffff"),
 90            CompletionBuilder::function("select_to_start_of_next_excerpt", None, "7fffffff"),
 91            CompletionBuilder::function("select_to_end_of_previous_excerpt", None, "7fffffff"),
 92        ];
 93        let matches =
 94            filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
 95        assert_eq!(matches[0].string, "set_text");
 96        assert_eq!(matches[1].string, "set_text_style_refinement");
 97        assert_eq!(matches[2].string, "set_placeholder_text");
 98    }
 99
100    // fuzzy filter text over label, sort_text and sort_kind
101    {
102        // Case 1: "awa"
103        let completions = vec![
104            CompletionBuilder::method("await", Some("await"), "7fffffff"),
105            CompletionBuilder::method("await.ne", Some("ne"), "80000010"),
106            CompletionBuilder::method("await.eq", Some("eq"), "80000010"),
107            CompletionBuilder::method("await.or", Some("or"), "7ffffff8"),
108            CompletionBuilder::method("await.zip", Some("zip"), "80000006"),
109            CompletionBuilder::method("await.xor", Some("xor"), "7ffffff8"),
110            CompletionBuilder::method("await.and", Some("and"), "80000006"),
111            CompletionBuilder::method("await.map", Some("map"), "80000006"),
112        ];
113
114        test_for_each_prefix("await", &completions, cx, |matches| {
115            // for each prefix, first item should always be one with lower sort_text
116            assert_eq!(matches[0].string, "await");
117        })
118        .await;
119    }
120}
121
122#[gpui::test]
123async fn test_sort_text(cx: &mut TestAppContext) {
124    // sort text takes precedance over sort_kind, when fuzzy is same
125    {
126        let completions = vec![
127            CompletionBuilder::variable("unreachable", None, "80000000"),
128            CompletionBuilder::function("unreachable!(…)", None, "7fffffff"),
129            CompletionBuilder::function("unchecked_rem", None, "80000010"),
130            CompletionBuilder::function("unreachable_unchecked", None, "80000020"),
131        ];
132
133        test_for_each_prefix("unreachabl", &completions, cx, |matches| {
134            // for each prefix, first item should always be one with lower sort_text
135            assert_eq!(matches[0].string, "unreachable!(…)");
136            assert_eq!(matches[1].string, "unreachable");
137
138            // fuzzy score should match for first two items as query is common prefix
139            assert_eq!(matches[0].score, matches[1].score);
140        })
141        .await;
142
143        let matches =
144            filter_and_sort_matches("unreachable", &completions, SnippetSortOrder::Top, cx).await;
145        // exact match comes first
146        assert_eq!(matches[0].string, "unreachable");
147        assert_eq!(matches[1].string, "unreachable!(…)");
148
149        // fuzzy score should match for first two items as query is common prefix
150        assert_eq!(matches[0].score, matches[1].score);
151    }
152}
153
154#[gpui::test]
155async fn test_sort_snippet(cx: &mut TestAppContext) {
156    let completions = vec![
157        CompletionBuilder::constant("println", None, "7fffffff"),
158        CompletionBuilder::snippet("println!(…)", None, "80000000"),
159    ];
160    let matches = filter_and_sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;
161
162    // snippet take precedence over sort_text and sort_kind
163    assert_eq!(matches[0].string, "println!(…)");
164}
165
166#[gpui::test]
167async fn test_sort_exact(cx: &mut TestAppContext) {
168    // sort_text takes over if no exact match
169    let completions = vec![
170        CompletionBuilder::function("into", None, "80000004"),
171        CompletionBuilder::function("try_into", None, "80000004"),
172        CompletionBuilder::snippet("println", None, "80000004"),
173        CompletionBuilder::function("clone_into", None, "80000004"),
174        CompletionBuilder::function("into_searcher", None, "80000000"),
175        CompletionBuilder::snippet("eprintln", None, "80000004"),
176    ];
177    let matches =
178        filter_and_sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
179    assert_eq!(matches[0].string, "into_searcher");
180
181    // exact match takes over sort_text
182    let completions = vec![
183        CompletionBuilder::function("into", None, "80000004"),
184        CompletionBuilder::function("try_into", None, "80000004"),
185        CompletionBuilder::function("clone_into", None, "80000004"),
186        CompletionBuilder::function("into_searcher", None, "80000000"),
187        CompletionBuilder::function("split_terminator", None, "7fffffff"),
188        CompletionBuilder::function("rsplit_terminator", None, "7fffffff"),
189    ];
190    let matches =
191        filter_and_sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
192    assert_eq!(matches[0].string, "into");
193}
194
195#[gpui::test]
196async fn test_sort_positions(cx: &mut TestAppContext) {
197    // positions take precedence over fuzzy score and sort_text
198    let completions = vec![
199        CompletionBuilder::function("rounded-full", None, "15788"),
200        CompletionBuilder::variable("rounded-t-full", None, "15846"),
201        CompletionBuilder::variable("rounded-b-full", None, "15731"),
202        CompletionBuilder::function("rounded-tr-full", None, "15866"),
203    ];
204
205    let matches = filter_and_sort_matches(
206        "rounded-full",
207        &completions,
208        SnippetSortOrder::default(),
209        cx,
210    )
211    .await;
212    assert_eq!(matches[0].string, "rounded-full");
213
214    let matches =
215        filter_and_sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;
216    assert_eq!(matches[0].string, "rounded-full");
217}
218
219#[gpui::test]
220async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
221    let completions = vec![
222        CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score
223        CompletionBuilder::function(
224            "language_servers_running_disk_based_diagnostics",
225            None,
226            "7fffffff",
227        ), // 0.168 fuzzy score
228        CompletionBuilder::function("code_lens", None, "7fffffff"),           // 3.2 fuzzy score
229        CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"),       // 3.2 fuzzy score
230        CompletionBuilder::function("fetch_code_lens", None, "7fffffff"),     // 3.2 fuzzy score
231    ];
232
233    let matches =
234        filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await;
235
236    assert_eq!(matches[0].string, "code_lens");
237    assert_eq!(matches[1].string, "lsp_code_lens");
238    assert_eq!(matches[2].string, "fetch_code_lens");
239}
240
241async fn test_for_each_prefix<F>(
242    target: &str,
243    completions: &Vec<Completion>,
244    cx: &mut TestAppContext,
245    mut test_fn: F,
246) where
247    F: FnMut(Vec<StringMatch>),
248{
249    for i in 1..=target.len() {
250        let prefix = &target[..i];
251        let matches =
252            filter_and_sort_matches(prefix, completions, SnippetSortOrder::default(), cx).await;
253        test_fn(matches);
254    }
255}
256
257struct CompletionBuilder;
258
259impl CompletionBuilder {
260    fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
261        Self::new(label, filter_text, sort_text, CompletionItemKind::CONSTANT)
262    }
263
264    fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
265        Self::new(label, filter_text, sort_text, CompletionItemKind::FUNCTION)
266    }
267
268    fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
269        Self::new(label, filter_text, sort_text, CompletionItemKind::METHOD)
270    }
271
272    fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
273        Self::new(label, filter_text, sort_text, CompletionItemKind::VARIABLE)
274    }
275
276    fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
277        Self::new(label, filter_text, sort_text, CompletionItemKind::SNIPPET)
278    }
279
280    fn new(
281        label: &str,
282        filter_text: Option<&str>,
283        sort_text: &str,
284        kind: CompletionItemKind,
285    ) -> Completion {
286        Completion {
287            replace_range: Anchor::MIN..Anchor::MAX,
288            new_text: label.to_string(),
289            label: CodeLabel::plain(label.to_string(), filter_text),
290            documentation: None,
291            source: CompletionSource::Lsp {
292                insert_range: None,
293                server_id: LanguageServerId(0),
294                lsp_completion: Box::new(CompletionItem {
295                    label: label.to_string(),
296                    kind: Some(kind),
297                    sort_text: Some(sort_text.to_string()),
298                    filter_text: filter_text.map(|text| text.to_string()),
299                    ..Default::default()
300                }),
301                lsp_defaults: None,
302                resolved: false,
303            },
304            icon_path: None,
305            insert_text_mode: None,
306            confirm: None,
307        }
308    }
309}
310
311async fn filter_and_sort_matches(
312    query: &str,
313    completions: &Vec<Completion>,
314    snippet_sort_order: SnippetSortOrder,
315    cx: &mut TestAppContext,
316) -> Vec<StringMatch> {
317    let candidates: Arc<[StringMatchCandidate]> = completions
318        .iter()
319        .enumerate()
320        .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
321        .collect();
322    let cancel_flag = Arc::new(AtomicBool::new(false));
323    let background_executor = cx.executor();
324    let matches = fuzzy::match_strings(
325        &candidates,
326        query,
327        query.chars().any(|c| c.is_uppercase()),
328        false,
329        100,
330        &cancel_flag,
331        background_executor,
332    )
333    .await;
334    CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions)
335}