From 07b44803966f0a78b53071a0d679d17bb70e5061 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 5 May 2025 20:48:52 +0530 Subject: [PATCH] editor: Handle more completions sort cases in Rust and Python (#29926) Closes #29725 Adds 3 more tests for Rust `into` and `await` cases, and Python `__init__` case. Tweaks sort logic to accommodate them. Release Notes: - Improved code completion sort order, handling more cases with Rust and Python. --- crates/editor/src/code_completion_tests.rs | 685 ++++++++++++++++++++- crates/editor/src/code_context_menus.rs | 17 + 2 files changed, 693 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 5fd2ddf0fa8da39457d8c5ec1428187257f5cc57..14d2b4372a24417d1fb876782b825e7b2a8ac2c2 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -517,8 +517,8 @@ fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); assert_eq!( matches[0].string_match.string.as_str(), - "unreachable!(…)", - "Match order not expected" + "unreachable", + "Perfect fuzzy match should be preferred over others" ); } @@ -1306,13 +1306,6 @@ fn test_sort_matches_for_prefix_matches(_cx: &mut TestAppContext) { }, ]; CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - println!( - "{:?}", - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - ); assert_eq!( matches .iter() @@ -1331,3 +1324,677 @@ fn test_sort_matches_for_prefix_matches(_cx: &mut TestAppContext) { ] ); } + +#[gpui::test] +fn test_sort_matches_for_await(_cx: &mut TestAppContext) { + // Case 1: "awa" + let query: Option<&str> = Some("awa"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.6000000000000001, + positions: vec![], + string: "await".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (0, "await"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 35, + score: 0.375, + positions: vec![], + string: "await.ne".to_string(), + }, + is_snippet: false, + sort_text: Some("80000010"), + sort_key: (3, "await.ne"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 34, + score: 0.375, + positions: vec![], + string: "await.eq".to_string(), + }, + is_snippet: false, + sort_text: Some("80000010"), + sort_key: (3, "await.eq"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 18, + score: 0.375, + positions: vec![], + string: "await.or".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffff8"), + sort_key: (3, "await.or"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 21, + score: 0.3333333333333333, + positions: vec![], + string: "await.zip".to_string(), + }, + is_snippet: false, + sort_text: Some("80000006"), + sort_key: (3, "await.zip"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 20, + score: 0.3333333333333333, + positions: vec![], + string: "await.xor".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffff8"), + sort_key: (3, "await.xor"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 15, + score: 0.3333333333333333, + positions: vec![], + string: "await.and".to_string(), + }, + is_snippet: false, + sort_text: Some("80000006"), + sort_key: (3, "await.and"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 9, + score: 0.3333333333333333, + positions: vec![], + string: "await.map".to_string(), + }, + is_snippet: false, + sort_text: Some("80000006"), + sort_key: (3, "await.map"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 47, + score: 0.30000000000000004, + positions: vec![], + string: "await.take".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffff8"), + sort_key: (3, "await.take"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); + assert_eq!( + matches + .iter() + .map(|m| m.string_match.string.as_str()) + .collect::>(), + vec![ + "await", + "await.or", + "await.xor", + "await.take", + "await.and", + "await.map", + "await.zip", + "await.eq", + "await.ne" + ] + ); + // Case 2: "await" + let query: Option<&str> = Some("await"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 1.0, + positions: vec![], + string: "await".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (0, "await"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 35, + score: 0.625, + positions: vec![], + string: "await.ne".to_string(), + }, + is_snippet: false, + sort_text: Some("80000010"), + sort_key: (3, "await.ne"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 34, + score: 0.625, + positions: vec![], + string: "await.eq".to_string(), + }, + is_snippet: false, + sort_text: Some("80000010"), + sort_key: (3, "await.eq"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 18, + score: 0.625, + positions: vec![], + string: "await.or".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffff8"), + sort_key: (3, "await.or"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 21, + score: 0.5555555555555556, + positions: vec![], + string: "await.zip".to_string(), + }, + is_snippet: false, + sort_text: Some("80000006"), + sort_key: (3, "await.zip"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 20, + score: 0.5555555555555556, + positions: vec![], + string: "await.xor".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffff8"), + sort_key: (3, "await.xor"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 15, + score: 0.5555555555555556, + positions: vec![], + string: "await.and".to_string(), + }, + is_snippet: false, + sort_text: Some("80000006"), + sort_key: (3, "await.and"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 9, + score: 0.5555555555555556, + positions: vec![], + string: "await.map".to_string(), + }, + is_snippet: false, + sort_text: Some("80000006"), + sort_key: (3, "await.map"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 47, + score: 0.5, + positions: vec![], + string: "await.take".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffff8"), + sort_key: (3, "await.take"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); + assert_eq!( + matches + .iter() + .map(|m| m.string_match.string.as_str()) + .collect::>(), + vec![ + "await", + "await.or", + "await.xor", + "await.take", + "await.and", + "await.map", + "await.zip", + "await.eq", + "await.ne" + ] + ); +} + +#[gpui::test] +fn test_sort_matches_for_python_init(_cx: &mut TestAppContext) { + // Case 1: "__in" + let query: Option<&str> = Some("__in"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 211, + score: 0.5, + positions: vec![], + string: "__init__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0003.__init__"), + sort_key: (3, "__init__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.5, + positions: vec![], + string: "__init__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0003"), + sort_key: (3, "__init__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 215, + score: 0.23529411764705882, + positions: vec![], + string: "__instancecheck__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0005.__instancecheck__"), + sort_key: (3, "__instancecheck__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 213, + score: 0.23529411764705882, + positions: vec![], + string: "__init_subclass__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0004.__init_subclass__"), + sort_key: (3, "__init_subclass__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 4, + score: 0.23529411764705882, + positions: vec![], + string: "__instancecheck__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0005"), + sort_key: (3, "__instancecheck__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 2, + score: 0.23529411764705882, + positions: vec![], + string: "__init_subclass__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0004"), + sort_key: (3, "__init_subclass__"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); + assert_eq!( + matches + .iter() + .map(|m| m.string_match.string.as_str()) + .collect::>(), + vec![ + "__init__", + "__init__", + "__init_subclass__", + "__init_subclass__", + "__instancecheck__", + "__instancecheck__", + ] + ); + // Case 2: "__ini" + let query: Option<&str> = Some("__ini"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 9, + score: 0.625, + positions: vec![], + string: "__init__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0004.__init__"), + sort_key: (3, "__init__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.625, + positions: vec![], + string: "__init__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0004"), + sort_key: (3, "__init__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 10, + score: 0.29411764705882354, + positions: vec![], + string: "__init_subclass__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0003.__init_subclass__"), + sort_key: (3, "__init_subclass__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 1, + score: 0.29411764705882354, + positions: vec![], + string: "__init_subclass__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0003"), + sort_key: (3, "__init_subclass__"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); + assert_eq!( + matches + .iter() + .map(|m| m.string_match.string.as_str()) + .collect::>(), + vec![ + "__init__", + "__init__", + "__init_subclass__", + "__init_subclass__", + ] + ); + // Case 3: "__init" + let query: Option<&str> = Some("__init"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 7, + score: 0.75, + positions: vec![], + string: "__init__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0000.__init__"), + sort_key: (3, "__init__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.75, + positions: vec![], + string: "__init__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0000"), + sort_key: (3, "__init__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 8, + score: 0.3529411764705882, + positions: vec![], + string: "__init_subclass__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0001.__init_subclass__"), + sort_key: (3, "__init_subclass__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 1, + score: 0.3529411764705882, + positions: vec![], + string: "__init_subclass__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0001"), + sort_key: (3, "__init_subclass__"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); + assert_eq!( + matches + .iter() + .map(|m| m.string_match.string.as_str()) + .collect::>(), + vec![ + "__init__", + "__init__", + "__init_subclass__", + "__init_subclass__", + ] + ); + // Case 4: "__init_" + let query: Option<&str> = Some("__init_"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 4, + score: 0.875, + positions: vec![], + string: "__init__".to_string(), + }, + is_snippet: false, + sort_text: Some("11.9999.__init__"), + sort_key: (3, "__init__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.875, + positions: vec![], + string: "__init__".to_string(), + }, + is_snippet: false, + sort_text: Some("11.9999"), + sort_key: (3, "__init__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 5, + score: 0.4117647058823529, + positions: vec![], + string: "__init_subclass__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0000.__init_subclass__"), + sort_key: (3, "__init_subclass__"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 1, + score: 0.4117647058823529, + positions: vec![], + string: "__init_subclass__".to_string(), + }, + is_snippet: false, + sort_text: Some("05.0000"), + sort_key: (3, "__init_subclass__"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); + assert_eq!( + matches + .iter() + .map(|m| m.string_match.string.as_str()) + .collect::>(), + vec![ + "__init__", + "__init__", + "__init_subclass__", + "__init_subclass__", + ] + ); +} + +#[gpui::test] +fn test_sort_matches_for_rust_into(_cx: &mut TestAppContext) { + // Case 1: "int" + let query: Option<&str> = Some("int"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 67, + score: 0.75, + positions: vec![], + string: "into".to_string(), + }, + is_snippet: false, + sort_text: Some("80000004"), + sort_key: (3, "into"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 68, + score: 0.30000000000000004, + positions: vec![], + string: "try_into".to_string(), + }, + is_snippet: false, + sort_text: Some("80000004"), + sort_key: (3, "try_into"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 108, + score: 0.2571428571428571, + positions: vec![], + string: "println".to_string(), + }, + is_snippet: true, + sort_text: Some("80000004"), + sort_key: (3, "println"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 73, + score: 0.24, + positions: vec![], + string: "clone_into".to_string(), + }, + is_snippet: false, + sort_text: Some("80000004"), + sort_key: (3, "clone_into"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 1, + score: 0.23076923076923078, + positions: vec![], + string: "into_searcher".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (3, "into_searcher"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 109, + score: 0.22499999999999998, + positions: vec![], + string: "eprintln".to_string(), + }, + is_snippet: true, + sort_text: Some("80000004"), + sort_key: (3, "eprintln"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); + assert_eq!( + matches[0].string_match.string.as_str(), + "into", + "Match order not expected" + ); + // Case 2: "into" + let query: Option<&str> = Some("into"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 65, + score: 1.0, + positions: vec![], + string: "into".to_string(), + }, + is_snippet: false, + sort_text: Some("80000004"), + sort_key: (3, "into"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 66, + score: 0.4, + positions: vec![], + string: "try_into".to_string(), + }, + is_snippet: false, + sort_text: Some("80000004"), + sort_key: (3, "try_into"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 71, + score: 0.32, + positions: vec![], + string: "clone_into".to_string(), + }, + is_snippet: false, + sort_text: Some("80000004"), + sort_key: (3, "clone_into"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.3076923076923077, + positions: vec![], + string: "into_searcher".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (3, "into_searcher"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 27, + score: 0.09, + positions: vec![], + string: "split_terminator".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "split_terminator"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 28, + score: 0.08470588235294117, + positions: vec![], + string: "rsplit_terminator".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "rsplit_terminator"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); + assert_eq!( + matches[0].string_match.string.as_str(), + "into", + "Match order not expected" + ); +} diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index e85297e1e6f54efc68ed22d1cc9695b1713a024a..84c8042d74447e79e43e48cef5dc2bba560bede9 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -674,6 +674,7 @@ impl CompletionsMenu { enum MatchTier<'a> { WordStartMatch { sort_prefix: Reverse, + sort_fuzzy_bracket: Reverse, sort_snippet: Reverse, sort_text: Option<&'a str>, sort_score: Reverse>, @@ -687,6 +688,14 @@ impl CompletionsMenu { // Our goal here is to intelligently sort completion suggestions. We want to // balance the raw fuzzy match score with hints from the language server + // In a fuzzy bracket, matches with a score of 1.0 are prioritized. + // The remaining matches are partitioned into two groups at 2/3 of the max_score. + let max_score = matches + .iter() + .map(|mat| mat.string_match.score) + .fold(0.0, f64::max); + let second_bracket_threshold = max_score * (2.0 / 3.0); + let query_start_lower = query .and_then(|q| q.chars().next()) .and_then(|c| c.to_lowercase().next()); @@ -709,6 +718,13 @@ impl CompletionsMenu { if query_start_doesnt_match_split_words { MatchTier::OtherMatch { sort_score } } else { + let sort_fuzzy_bracket = Reverse(if score == 1.0 { + 2 + } else if score >= second_bracket_threshold { + 1 + } else { + 0 + }); let sort_snippet = match snippet_sort_order { SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }), SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }), @@ -735,6 +751,7 @@ impl CompletionsMenu { ); MatchTier::WordStartMatch { sort_prefix: mixed_case_prefix_length, + sort_fuzzy_bracket, sort_snippet, sort_text: mat.sort_text, sort_score,