From f54129461fa280df8297ed7a7b08de8d731d8720 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 12 Jun 2025 20:23:16 +0530 Subject: [PATCH] editor: Improve completions sort order for Tailwind classes (#32612) Closes #32532 Before: Image After: image Release Notes: - Improved auto-complete suggestions for Tailwind classes. --- crates/editor/src/code_completion_tests.rs | 87 ++++++++++++++-------- crates/editor/src/code_context_menus.rs | 28 +++---- 2 files changed, 66 insertions(+), 49 deletions(-) diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 1cfb7050a306bb1070db449595b7732040479076..22ab28174d9d321a23a6eab856b03fdefc6d2c64 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -19,7 +19,7 @@ async fn test_sort_matches_local_variable_over_global_variable(cx: &mut TestAppC CompletionBuilder::constant("floorf16", "80000000"), CompletionBuilder::constant("floorf128", "80000000"), ]; - let matches = sort_matches("foo", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "foo_bar_qux"); assert_eq!(matches[1], "foo_bar_baz"); assert_eq!(matches[2], "floorf16"); @@ -30,7 +30,7 @@ async fn test_sort_matches_local_variable_over_global_variable(cx: &mut TestAppC CompletionBuilder::constant("foo_bar_baz", "7fffffff"), CompletionBuilder::variable("foo_bar_qux", "7ffffffe"), ]; - let matches = sort_matches("foobar", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("foobar", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "foo_bar_qux"); assert_eq!(matches[1], "foo_bar_baz"); } @@ -44,7 +44,7 @@ async fn test_sort_matches_local_variable_over_global_enum(cx: &mut TestAppConte CompletionBuilder::constant("simd_select", "80000000"), CompletionBuilder::keyword("while let", "7fffffff"), ]; - let matches = sort_matches("ele", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("ele", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "element_type"); assert_eq!(matches[1], "ElementType"); @@ -54,16 +54,19 @@ async fn test_sort_matches_local_variable_over_global_enum(cx: &mut TestAppConte CompletionBuilder::variable("element_type", "7ffffffe"), CompletionBuilder::constant("REPLACEMENT_CHARACTER", "80000000"), ]; - let matches = sort_matches("eleme", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("eleme", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "element_type"); assert_eq!(matches[1], "ElementType"); +} - // Case 3: "Elem" +#[gpui::test] +async fn test_sort_matches_capitalization(cx: &mut TestAppContext) { + // Case 1: "Elem" let completions = vec![ CompletionBuilder::constant("ElementType", "7fffffff"), CompletionBuilder::variable("element_type", "7ffffffe"), ]; - let matches = sort_matches("Elem", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "ElementType"); assert_eq!(matches[1], "element_type"); } @@ -77,7 +80,7 @@ async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) { CompletionBuilder::function("unchecked_rem", "80000000"), CompletionBuilder::function("unreachable_unchecked", "80000000"), ]; - let matches = sort_matches("unre", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("unre", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "unreachable!(…)"); // Case 2: "unrea" @@ -86,7 +89,7 @@ async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) { CompletionBuilder::function("unreachable!(…)", "7fffffff"), CompletionBuilder::function("unreachable_unchecked", "80000000"), ]; - let matches = sort_matches("unrea", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("unrea", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "unreachable!(…)"); // Case 3: "unreach" @@ -95,7 +98,7 @@ async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) { CompletionBuilder::function("unreachable!(…)", "7fffffff"), CompletionBuilder::function("unreachable_unchecked", "80000000"), ]; - let matches = sort_matches("unreach", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("unreach", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "unreachable!(…)"); // Case 4: "unreachabl" @@ -104,7 +107,7 @@ async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) { CompletionBuilder::function("unreachable!(…)", "7fffffff"), CompletionBuilder::function("unreachable_unchecked", "80000000"), ]; - let matches = sort_matches("unreachable", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "unreachable!(…)"); // Case 5: "unreachable" @@ -113,7 +116,7 @@ async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) { CompletionBuilder::function("unreachable!(…)", "7fffffff"), CompletionBuilder::function("unreachable_unchecked", "80000000"), ]; - let matches = sort_matches("unreachable", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "unreachable!(…)"); } @@ -124,7 +127,7 @@ async fn test_sort_matches_variable_and_constants_over_function(cx: &mut TestApp CompletionBuilder::function("var", "7fffffff"), CompletionBuilder::variable("var", "7fffffff"), ]; - let matches = sort_matches("var", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "var"); assert_eq!(matches[1], "var"); @@ -133,7 +136,7 @@ async fn test_sort_matches_variable_and_constants_over_function(cx: &mut TestApp CompletionBuilder::function("var", "7fffffff"), CompletionBuilder::constant("var", "7fffffff"), ]; - let matches = sort_matches("var", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "var"); assert_eq!(matches[1], "var"); } @@ -149,7 +152,7 @@ async fn test_sort_matches_for_jsx_event_handler(cx: &mut TestAppContext) { CompletionBuilder::function("style?", "12"), CompletionBuilder::function("className?", "12"), ]; - let matches = sort_matches("on", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("on", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "onCut?"); assert_eq!(matches[1], "onPlay?"); @@ -168,7 +171,7 @@ async fn test_sort_matches_for_jsx_event_handler(cx: &mut TestAppContext) { CompletionBuilder::function("onWaiting?", "12"), CompletionBuilder::function("onCanPlay?", "12"), ]; - let matches = sort_matches("ona", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "onAbort?"); assert_eq!(matches[1], "onAuxClick?"); } @@ -180,7 +183,7 @@ async fn test_sort_matches_for_snippets(cx: &mut TestAppContext) { CompletionBuilder::constant("println", "80000000"), CompletionBuilder::snippet("println!(…)", "80000000"), ]; - let matches = sort_matches("prin", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "println!(…)"); } @@ -200,10 +203,10 @@ async fn test_sort_matches_for_exact_match(cx: &mut TestAppContext) { CompletionBuilder::function("select_to_start_of_next_excerpt", "7fffffff"), CompletionBuilder::function("select_to_end_of_previous_excerpt", "7fffffff"), ]; - let matches = sort_matches("set_text", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "set_text"); assert_eq!(matches[1], "set_text_style_refinement"); - assert_eq!(matches[2], "set_placeholder_text"); + assert_eq!(matches[2], "set_context_menu_options"); } #[gpui::test] @@ -220,7 +223,7 @@ async fn test_sort_matches_for_prefix_matches(cx: &mut TestAppContext) { CompletionBuilder::function("select_left", "7fffffff"), CompletionBuilder::function("select_down", "7fffffff"), ]; - let matches = sort_matches("set", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("set", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "set_autoindent"); assert_eq!(matches[1], "set_collapse_matches"); assert_eq!(matches[2], "set_all_diagnostics_active"); @@ -240,7 +243,7 @@ async fn test_sort_matches_for_await(cx: &mut TestAppContext) { CompletionBuilder::function("await.map", "80000006"), CompletionBuilder::function("await.take", "7ffffff8"), ]; - let matches = sort_matches("awa", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("awa", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "await"); // Case 2: "await" @@ -255,7 +258,7 @@ async fn test_sort_matches_for_await(cx: &mut TestAppContext) { CompletionBuilder::function("await.map", "80000006"), CompletionBuilder::function("await.take", "7ffffff8"), ]; - let matches = sort_matches("await", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("await", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "await"); } @@ -270,7 +273,7 @@ async fn test_sort_matches_for_python_init(cx: &mut TestAppContext) { CompletionBuilder::function("__instancecheck__", "05.0005"), CompletionBuilder::function("__init_subclass__", "05.0004"), ]; - let matches = sort_matches("__in", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("__in", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "__init__"); assert_eq!(matches[1], "__init__"); @@ -281,7 +284,7 @@ async fn test_sort_matches_for_python_init(cx: &mut TestAppContext) { CompletionBuilder::function("__init_subclass__", "05.0003.__init_subclass__"), CompletionBuilder::function("__init_subclass__", "05.0003"), ]; - let matches = sort_matches("__ini", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("__ini", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "__init__"); assert_eq!(matches[1], "__init__"); @@ -292,7 +295,7 @@ async fn test_sort_matches_for_python_init(cx: &mut TestAppContext) { CompletionBuilder::function("__init_subclass__", "05.0001.__init_subclass__"), CompletionBuilder::function("__init_subclass__", "05.0001"), ]; - let matches = sort_matches("__init", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("__init", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "__init__"); assert_eq!(matches[1], "__init__"); @@ -303,7 +306,7 @@ async fn test_sort_matches_for_python_init(cx: &mut TestAppContext) { CompletionBuilder::function("__init_subclass__", "05.0000.__init_subclass__"), CompletionBuilder::function("__init_subclass__", "05.0000"), ]; - let matches = sort_matches("__init_", completions, SnippetSortOrder::Top, cx).await; + let matches = sort_matches("__init_", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0], "__init__"); assert_eq!(matches[1], "__init__"); } @@ -319,7 +322,7 @@ async fn test_sort_matches_for_rust_into(cx: &mut TestAppContext) { CompletionBuilder::function("into_searcher", "80000000"), CompletionBuilder::snippet("eprintln", "80000004"), ]; - let matches = sort_matches("int", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("int", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "into"); // Case 2: "into" @@ -331,7 +334,7 @@ async fn test_sort_matches_for_rust_into(cx: &mut TestAppContext) { CompletionBuilder::function("split_terminator", "7fffffff"), CompletionBuilder::function("rsplit_terminator", "7fffffff"), ]; - let matches = sort_matches("into", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("into", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "into"); } @@ -345,7 +348,7 @@ async fn test_sort_matches_for_variable_over_function(cx: &mut TestAppContext) { CompletionBuilder::function("serialize_version", "80000000"), CompletionBuilder::function("deserialize", "80000000"), ]; - let matches = sort_matches("serial", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("serial", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "serialization_key"); assert_eq!(matches[1], "serialize"); assert_eq!(matches[2], "serialize"); @@ -364,7 +367,7 @@ async fn test_sort_matches_for_local_methods_over_library(cx: &mut TestAppContex CompletionBuilder::variable("setIsRefreshing", "11"), CompletionBuilder::function("setFips", "16"), ]; - let matches = sort_matches("setis", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("setis", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "setIsRefreshing"); assert_eq!(matches[1], "setISODay"); assert_eq!(matches[2], "setISOWeek"); @@ -379,13 +382,35 @@ async fn test_sort_matches_for_prioritize_not_exact_match(cx: &mut TestAppContex CompletionBuilder::variable("items", "11"), CompletionBuilder::function("ItemText", "16"), ]; - let matches = sort_matches("item", completions, SnippetSortOrder::default(), cx).await; + let matches = sort_matches("item", &completions, SnippetSortOrder::default(), cx).await; assert_eq!(matches[0], "items"); assert_eq!(matches[1], "Item"); assert_eq!(matches[2], "Item"); assert_eq!(matches[3], "ItemText"); } +#[gpui::test] +async fn test_sort_matches_for_tailwind_classes(cx: &mut TestAppContext) { + let completions = vec![ + CompletionBuilder::function("rounded-full", "15788"), + CompletionBuilder::variable("rounded-t-full", "15846"), + CompletionBuilder::variable("rounded-b-full", "15731"), + CompletionBuilder::function("rounded-tr-full", "15866"), + ]; + // Case 1: "rounded-full" + let matches = sort_matches( + "rounded-full", + &completions, + SnippetSortOrder::default(), + cx, + ) + .await; + assert_eq!(matches[0], "rounded-full"); + // Case 2: "roundedfull" + let matches = sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!(matches[0], "rounded-full"); +} + struct CompletionBuilder; impl CompletionBuilder { @@ -440,7 +465,7 @@ impl CompletionBuilder { async fn sort_matches( query: &str, - completions: Vec, + completions: &Vec, snippet_sort_order: SnippetSortOrder, cx: &mut TestAppContext, ) -> Vec { diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 2c17f9566ab337e70653d0680655995bbe5a60e1..542e3689c9cd8820c40110511ca1d8475b1d0913 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1055,7 +1055,8 @@ impl CompletionsMenu { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] enum MatchTier<'a> { WordStartMatch { - sort_mixed_case_prefix_length: Reverse, + sort_capitalize: Reverse, + sort_positions: Vec, sort_snippet: Reverse, sort_kind: usize, sort_fuzzy_bracket: Reverse, @@ -1126,28 +1127,19 @@ impl CompletionsMenu { SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }), SnippetSortOrder::Inline => Reverse(0), }; - let sort_mixed_case_prefix_length = Reverse( + let sort_capitalize = Reverse( query .as_ref() - .map(|q| { - q.chars() - .zip(string_match.string.chars()) - .enumerate() - .take_while(|(i, (q_char, match_char))| { - if *i == 0 { - // Case-sensitive comparison for first character - q_char == match_char - } else { - // Case-insensitive comparison for other characters - q_char.to_lowercase().eq(match_char.to_lowercase()) - } - }) - .count() - }) + .and_then(|q| q.chars().next()) + .zip(string_match.string.chars().next()) + .map(|(q_char, s_char)| if q_char == s_char { 1 } else { 0 }) .unwrap_or(0), ); + let sort_positions = string_match.positions.clone(); + MatchTier::WordStartMatch { - sort_mixed_case_prefix_length, + sort_capitalize, + sort_positions, sort_snippet, sort_kind, sort_fuzzy_bracket,