diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 3211f0b818eb3079007db4bf268e84bd53d3cbf1..b3d05e23e574867de91d46adfeb4aadefe075161 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -217,6 +217,77 @@ async fn test_sort_positions(cx: &mut TestAppContext) { assert_eq!(matches[0].string, "rounded-full"); } +#[gpui::test] +async fn test_case_sensitive_match_tie_breaker(cx: &mut TestAppContext) { + let completions = vec![ + CompletionBuilder::variable("abc", None, "11"), + CompletionBuilder::variable("ABC", None, "11"), + ]; + + let matches = filter_and_sort_matches("a", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!( + matches + .iter() + .map(|m| m.string.as_str()) + .collect::>(), + vec!["abc", "ABC"] + ); + + let matches = filter_and_sort_matches("A", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!( + matches + .iter() + .map(|m| m.string.as_str()) + .collect::>(), + vec!["ABC", "abc"] + ); + + let matches = + filter_and_sort_matches("ab", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!( + matches + .iter() + .map(|m| m.string.as_str()) + .collect::>(), + vec!["abc", "ABC"] + ); + + let matches = + filter_and_sort_matches("AB", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!( + matches + .iter() + .map(|m| m.string.as_str()) + .collect::>(), + vec!["ABC", "abc"] + ); + + let completions = vec![ + CompletionBuilder::variable("aBc", None, "11"), + CompletionBuilder::variable("Abc", None, "11"), + ]; + + let matches = + filter_and_sort_matches("Ab", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!( + matches + .iter() + .map(|m| m.string.as_str()) + .collect::>(), + vec!["Abc", "aBc"] + ); + + let matches = + filter_and_sort_matches("aB", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!( + matches + .iter() + .map(|m| m.string.as_str()) + .collect::>(), + vec!["aBc", "Abc"] + ); +} + #[gpui::test] async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) { let completions = vec![ diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 2db2086eef422a87a0825c4a4ad820d422b160e9..2c609e5ba81a000e44983c0a0d8709f2a4ad3556 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1256,6 +1256,7 @@ impl CompletionsMenu { sort_snippet: Reverse, sort_score: Reverse>, sort_positions: Vec, + sort_exact_case_matches: Reverse, sort_text: Option<&'a str>, sort_kind: usize, sort_label: &'a str, @@ -1311,6 +1312,10 @@ impl CompletionsMenu { SnippetSortOrder::None => Reverse(0), }; let sort_positions = string_match.positions.clone(); + let sort_exact_case_matches = Reverse(exact_case_match_count( + query.unwrap_or_default(), + string_match, + )); // This exact matching won't work for multi-word snippets, but it's fine let sort_exact = Reverse(if Some(completion.label.filter_text()) == query { 1 @@ -1323,6 +1328,7 @@ impl CompletionsMenu { sort_snippet, sort_score, sort_positions, + sort_exact_case_matches, sort_text, sort_kind, sort_label, @@ -1379,6 +1385,30 @@ impl CompletionsMenu { } } +fn exact_case_match_count(query: &str, string_match: &StringMatch) -> usize { + let mut exact_matches = 0; + let mut query_chars = query.chars(); + let mut next_query_char = query_chars.next(); + let mut matched_positions = string_match.positions.iter().copied().peekable(); + + for (index, candidate_char) in string_match.string.char_indices() { + if matched_positions.peek() == Some(&index) { + let Some(query_char) = next_query_char else { + break; + }; + + if query_char == candidate_char { + exact_matches += 1; + } + + matched_positions.next(); + next_query_char = query_chars.next(); + } + } + + exact_matches +} + #[derive(Clone)] pub struct AvailableCodeAction { pub action: CodeAction,