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;
 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!["element_type", "ElementType"]
 52        );
 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        .await;
139
140        let matches =
141            filter_and_sort_matches("unreachable", &completions, SnippetSortOrder::Top, cx).await;
142        // exact match comes first
143        assert_eq!(matches[0].string, "unreachable");
144        assert_eq!(matches[1].string, "unreachable!(…)");
145    }
146}
147
148#[gpui::test]
149async fn test_sort_snippet(cx: &mut TestAppContext) {
150    let completions = vec![
151        CompletionBuilder::constant("println", None, "7fffffff"),
152        CompletionBuilder::snippet("println!(…)", None, "80000000"),
153    ];
154    let matches = filter_and_sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;
155
156    // snippet take precedence over sort_text and sort_kind
157    assert_eq!(matches[0].string, "println!(…)");
158}
159
160#[gpui::test]
161async fn test_sort_exact(cx: &mut TestAppContext) {
162    // sort_text takes over if no exact match
163    let completions = vec![
164        CompletionBuilder::function("into", None, "80000004"),
165        CompletionBuilder::function("try_into", None, "80000004"),
166        CompletionBuilder::snippet("println", None, "80000004"),
167        CompletionBuilder::function("clone_into", None, "80000004"),
168        CompletionBuilder::function("into_searcher", None, "80000000"),
169        CompletionBuilder::snippet("eprintln", None, "80000004"),
170    ];
171    let matches =
172        filter_and_sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
173    assert_eq!(matches[0].string, "into_searcher");
174
175    // exact match takes over sort_text
176    let completions = vec![
177        CompletionBuilder::function("into", None, "80000004"),
178        CompletionBuilder::function("try_into", None, "80000004"),
179        CompletionBuilder::function("clone_into", None, "80000004"),
180        CompletionBuilder::function("into_searcher", None, "80000000"),
181        CompletionBuilder::function("split_terminator", None, "7fffffff"),
182        CompletionBuilder::function("rsplit_terminator", None, "7fffffff"),
183    ];
184    let matches =
185        filter_and_sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
186    assert_eq!(matches[0].string, "into");
187}
188
189#[gpui::test]
190async fn test_sort_positions(cx: &mut TestAppContext) {
191    // positions take precedence over fuzzy score and sort_text
192    let completions = vec![
193        CompletionBuilder::function("rounded-full", None, "15788"),
194        CompletionBuilder::variable("rounded-t-full", None, "15846"),
195        CompletionBuilder::variable("rounded-b-full", None, "15731"),
196        CompletionBuilder::function("rounded-tr-full", None, "15866"),
197    ];
198
199    let matches = filter_and_sort_matches(
200        "rounded-full",
201        &completions,
202        SnippetSortOrder::default(),
203        cx,
204    )
205    .await;
206    assert_eq!(matches[0].string, "rounded-full");
207
208    let matches =
209        filter_and_sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;
210    assert_eq!(matches[0].string, "rounded-full");
211}
212
213#[gpui::test]
214async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
215    let completions = vec![
216        CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score
217        CompletionBuilder::function(
218            "language_servers_running_disk_based_diagnostics",
219            None,
220            "7fffffff",
221        ), // 0.168 fuzzy score
222        CompletionBuilder::function("code_lens", None, "7fffffff"),           // 3.2 fuzzy score
223        CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"),       // 3.2 fuzzy score
224        CompletionBuilder::function("fetch_code_lens", None, "7fffffff"),     // 3.2 fuzzy score
225    ];
226
227    let matches =
228        filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await;
229
230    assert_eq!(matches[0].string, "code_lens");
231    assert_eq!(matches[1].string, "lsp_code_lens");
232    assert_eq!(matches[2].string, "fetch_code_lens");
233}
234
235async fn test_for_each_prefix<F>(
236    target: &str,
237    completions: &Vec<Completion>,
238    cx: &mut TestAppContext,
239    mut test_fn: F,
240) where
241    F: FnMut(Vec<StringMatch>),
242{
243    for i in 1..=target.len() {
244        let prefix = &target[..i];
245        let matches =
246            filter_and_sort_matches(prefix, completions, SnippetSortOrder::default(), cx).await;
247        test_fn(matches);
248    }
249}
250
251struct CompletionBuilder;
252
253impl CompletionBuilder {
254    fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
255        Self::new(label, filter_text, sort_text, CompletionItemKind::CONSTANT)
256    }
257
258    fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
259        Self::new(label, filter_text, sort_text, CompletionItemKind::FUNCTION)
260    }
261
262    fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
263        Self::new(label, filter_text, sort_text, CompletionItemKind::METHOD)
264    }
265
266    fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
267        Self::new(label, filter_text, sort_text, CompletionItemKind::VARIABLE)
268    }
269
270    fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
271        Self::new(label, filter_text, sort_text, CompletionItemKind::SNIPPET)
272    }
273
274    fn new(
275        label: &str,
276        filter_text: Option<&str>,
277        sort_text: &str,
278        kind: CompletionItemKind,
279    ) -> Completion {
280        Completion {
281            replace_range: Anchor::MIN..Anchor::MAX,
282            new_text: label.to_string(),
283            label: CodeLabel::plain(label.to_string(), filter_text),
284            documentation: None,
285            source: CompletionSource::Lsp {
286                insert_range: None,
287                server_id: LanguageServerId(0),
288                lsp_completion: Box::new(CompletionItem {
289                    label: label.to_string(),
290                    kind: Some(kind),
291                    sort_text: Some(sort_text.to_string()),
292                    filter_text: filter_text.map(|text| text.to_string()),
293                    ..Default::default()
294                }),
295                lsp_defaults: None,
296                resolved: false,
297            },
298            icon_path: None,
299            insert_text_mode: None,
300            confirm: None,
301        }
302    }
303}
304
305async fn filter_and_sort_matches(
306    query: &str,
307    completions: &Vec<Completion>,
308    snippet_sort_order: SnippetSortOrder,
309    cx: &mut TestAppContext,
310) -> Vec<StringMatch> {
311    let candidates: Arc<[StringMatchCandidate]> = completions
312        .iter()
313        .enumerate()
314        .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
315        .collect();
316    let cancel_flag = Arc::new(AtomicBool::new(false));
317    let background_executor = cx.executor();
318    let matches = fuzzy::match_strings(
319        &candidates,
320        query,
321        true,
322        false,
323        100,
324        &cancel_flag,
325        background_executor,
326    )
327    .await;
328    dbg!(CompletionsMenu::sort_string_matches(
329        matches,
330        Some(query),
331        snippet_sort_order,
332        completions
333    ))
334}