@@ -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<_>>(),
+ 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<_>>(),
+ 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<_>>(),
+ 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<_>>(),
+ 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<_>>(),
+ 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<_>>(),
+ vec!["aBc", "Abc"]
+ );
+}
+
#[gpui::test]
async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
let completions = vec![
@@ -1256,6 +1256,7 @@ impl CompletionsMenu {
sort_snippet: Reverse<i32>,
sort_score: Reverse<OrderedFloat<f64>>,
sort_positions: Vec<usize>,
+ sort_exact_case_matches: Reverse<usize>,
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,