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}