diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs new file mode 100644 index 0000000000..632b16d4cc --- /dev/null +++ b/crates/editor/src/code_completion_tests.rs @@ -0,0 +1,1005 @@ +use crate::code_context_menus::{CompletionsMenu, SortableMatch}; +use fuzzy::StringMatch; +use gpui::TestAppContext; + +#[gpui::test] +fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContext) { + // Case 1: "foo" + let query: Option<&str> = Some("foo"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2727272727272727, + positions: vec![], + string: "foo_bar_baz".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (2, "foo_bar_baz"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2727272727272727, + positions: vec![], + string: "foo_bar_qux".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffffe"), + sort_key: (1, "foo_bar_qux"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.22499999999999998, + positions: vec![], + string: "floorf64".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "floorf64"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.22499999999999998, + positions: vec![], + string: "floorf32".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "floorf32"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.22499999999999998, + positions: vec![], + string: "floorf16".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "floorf16"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2, + positions: vec![], + string: "floorf128".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "floorf128"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "foo_bar_qux", + "Match order not expected" + ); + assert_eq!( + matches[1].string_match.string.as_str(), + "foo_bar_baz", + "Match order not expected" + ); + assert_eq!( + matches[2].string_match.string.as_str(), + "floorf128", + "Match order not expected" + ); + assert_eq!( + matches[3].string_match.string.as_str(), + "floorf16", + "Match order not expected" + ); + + // Case 2: "foobar" + let query: Option<&str> = Some("foobar"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.4363636363636364, + positions: vec![], + string: "foo_bar_baz".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (2, "foo_bar_baz"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.4363636363636364, + positions: vec![], + string: "foo_bar_qux".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffffe"), + sort_key: (1, "foo_bar_qux"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "foo_bar_qux", + "Match order not expected" + ); + assert_eq!( + matches[1].string_match.string.as_str(), + "foo_bar_baz", + "Match order not expected" + ); +} + +#[gpui::test] +fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) { + // Case 1: "ele" + let query: Option<&str> = Some("ele"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2727272727272727, + positions: vec![], + string: "ElementType".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (2, "ElementType"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.25, + positions: vec![], + string: "element_type".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffffe"), + sort_key: (1, "element_type"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.16363636363636364, + positions: vec![], + string: "simd_select".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "simd_select"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.16, + positions: vec![], + string: "while let".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (0, "while let"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "element_type", + "Match order not expected" + ); + assert_eq!( + matches[1].string_match.string.as_str(), + "ElementType", + "Match order not expected" + ); + + // Case 2: "eleme" + let query: Option<&str> = Some("eleme"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.4545454545454546, + positions: vec![], + string: "ElementType".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (2, "ElementType"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.41666666666666663, + positions: vec![], + string: "element_type".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffffe"), + sort_key: (1, "element_type"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.04714285714285713, + positions: vec![], + string: "REPLACEMENT_CHARACTER".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "REPLACEMENT_CHARACTER"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "element_type", + "Match order not expected" + ); + assert_eq!( + matches[1].string_match.string.as_str(), + "ElementType", + "Match order not expected" + ); + + // Case 3: "Elem" + let query: Option<&str> = Some("Elem"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.36363636363636365, + positions: vec![], + string: "ElementType".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (2, "ElementType"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.0003333333333333333, + positions: vec![], + string: "element_type".to_string(), + }, + is_snippet: false, + sort_text: Some("7ffffffe"), + sort_key: (1, "element_type"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "ElementType", + "Match order not expected" + ); + assert_eq!( + matches[1].string_match.string.as_str(), + "element_type", + "Match order not expected" + ); +} + +#[gpui::test] +fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { + // Case 1: "unre" + let query: Option<&str> = Some("unre"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.36363636363636365, + positions: vec![], + string: "unreachable".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "unreachable"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.26666666666666666, + positions: vec![], + string: "unreachable!(…)".to_string(), + }, + is_snippet: true, + sort_text: Some("7fffffff"), + sort_key: (2, "unreachable!(…)"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.24615384615384617, + positions: vec![], + string: "unchecked_rem".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "unchecked_rem"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.19047619047619047, + positions: vec![], + string: "unreachable_unchecked".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "unreachable_unchecked"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "unreachable!(…)", + "Match order not expected" + ); + + // Case 2: "unrea" + let query: Option<&str> = Some("unrea"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.4545454545454546, + positions: vec![], + string: "unreachable".to_string(), + }, + is_snippet: true, + sort_text: Some("80000000"), + sort_key: (3, "unreachable"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.3333333333333333, + positions: vec![], + string: "unreachable!(…)".to_string(), + }, + is_snippet: true, + sort_text: Some("7fffffff"), + sort_key: (3, "unreachable!(…)"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.23809523809523808, + positions: vec![], + string: "unreachable_unchecked".to_string(), + }, + is_snippet: true, + sort_text: Some("80000000"), + sort_key: (3, "unreachable_unchecked"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "unreachable!(…)", + "Match order not expected" + ); + + // Case 3: "unreach" + let query: Option<&str> = Some("unreach"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.6363636363636364, + positions: vec![], + string: "unreachable".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "unreachable"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.4666666666666667, + positions: vec![], + string: "unreachable!(…)".to_string(), + }, + is_snippet: true, + sort_text: Some("7fffffff"), + sort_key: (2, "unreachable!(…)"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.3333333333333333, + positions: vec![], + string: "unreachable_unchecked".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "unreachable_unchecked"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "unreachable!(…)", + "Match order not expected" + ); + + // Case 4: "unreachable" + let query: Option<&str> = Some("unreachable"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 1.0, + positions: vec![], + string: "unreachable".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "unreachable"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.7333333333333333, + positions: vec![], + string: "unreachable!(…)".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (2, "unreachable!(…)"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.5238095238095237, + positions: vec![], + string: "unreachable_unchecked".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "unreachable_unchecked"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "unreachable!(…)", + "Match order not expected" + ); +} + +#[gpui::test] +fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppContext) { + // Case 1: "var" as variable + let query: Option<&str> = Some("var"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 1.0, + positions: vec![], + string: "var".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "var"), // function + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 1, + score: 1.0, + positions: vec![], + string: "var".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (1, "var"), // variable + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.candidate_id, 1, + "Match order not expected" + ); + assert_eq!( + matches[1].string_match.candidate_id, 0, + "Match order not expected" + ); + + // Case 2: "var" as constant + let query: Option<&str> = Some("var"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 1.0, + positions: vec![], + string: "var".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (3, "var"), // function + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 1, + score: 1.0, + positions: vec![], + string: "var".to_string(), + }, + is_snippet: false, + sort_text: Some("7fffffff"), + sort_key: (2, "var"), // constant + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.candidate_id, 1, + "Match order not expected" + ); + assert_eq!( + matches[1].string_match.candidate_id, 0, + "Match order not expected" + ); +} + +#[gpui::test] +fn test_sort_matches_jsx_event_handler(_cx: &mut TestAppContext) { + // Case 1: "on" + let query: Option<&str> = Some("on"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.3333333333333333, + positions: vec![], + string: "onCut?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onCut?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2857142857142857, + positions: vec![], + string: "onPlay?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onPlay?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.25, + positions: vec![], + string: "color?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "color?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.25, + positions: vec![], + string: "defaultValue?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "defaultValue?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.25, + positions: vec![], + string: "style?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "style?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.20, + positions: vec![], + string: "className?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "className?"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string, "onCut?", + "Match order not expected" + ); + assert_eq!( + matches[1].string_match.string, "onPlay?", + "Match order not expected" + ); + + // Case 2: "ona" + let query: Option<&str> = Some("ona"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.375, + positions: vec![], + string: "onAbort?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onAbort?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2727272727272727, + positions: vec![], + string: "onAuxClick?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onAuxClick?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.23571428571428565, + positions: vec![], + string: "onPlay?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onPlay?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.23571428571428565, + positions: vec![], + string: "onLoad?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onLoad?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.23571428571428565, + positions: vec![], + string: "onDrag?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onDrag?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.22499999999999998, + positions: vec![], + string: "onPause?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onPause?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.22499999999999998, + positions: vec![], + string: "onPaste?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onPaste?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2, + positions: vec![], + string: "onAnimationEnd?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onAnimationEnd?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2, + positions: vec![], + string: "onAbortCapture?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onAbortCapture?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.1833333333333333, + positions: vec![], + string: "onChange?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onChange?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.18, + positions: vec![], + string: "onWaiting?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onWaiting?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.18, + positions: vec![], + string: "onCanPlay?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onCanPlay?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.1764705882352941, + positions: vec![], + string: "onAnimationStart?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onAnimationStart?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.16666666666666666, + positions: vec![], + string: "onAuxClickCapture?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onAuxClickCapture?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.16499999999999998, + positions: vec![], + string: "onStalled?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onStalled?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.16499999999999998, + positions: vec![], + string: "onPlaying?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onPlaying?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.16499999999999998, + positions: vec![], + string: "onDragEnd?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onDragEnd?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.15000000000000002, + positions: vec![], + string: "onInvalid?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onInvalid?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.15, + positions: vec![], + string: "onDragOver?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onDragOver?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.15, + positions: vec![], + string: "onDragExit?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onDragExit?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.14285714285714285, + positions: vec![], + string: "onAnimationIteration?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onAnimationIteration?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13846153846153847, + positions: vec![], + string: "onRateChange?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onRateChange?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13749999999999996, + positions: vec![], + string: "onLoadStart?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onLoadStart?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13749999999999996, + positions: vec![], + string: "onDragStart?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onDragStart?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13749999999999996, + positions: vec![], + string: "onDragLeave?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onDragLeave?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13749999999999996, + positions: vec![], + string: "onDragEnter?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onDragEnter?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.13636363636363635, + positions: vec![], + string: "onAnimationEndCapture?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onAnimationEndCapture?"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.12692307692307692, + positions: vec![], + string: "onLoadedData?".to_string(), + }, + is_snippet: false, + sort_text: Some("12"), + sort_key: (3, "onLoadedData?"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches + .iter() + .take(12) + .map(|m| m.string_match.string.as_str()) + .collect::>(), + vec![ + "onAbort?", + "onAbortCapture?", + "onAnimationEnd?", + "onAnimationEndCapture?", + "onAnimationIteration?", + "onAnimationStart?", + "onAuxClick?", + "onAuxClickCapture?", + "onCanPlay?", + "onChange?", + "onDrag?", + "onDragEnd?", + ] + ); +} + +#[gpui::test] +fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) { + // Case 1: "prin" + let query: Option<&str> = Some("prin"); + let mut matches: Vec> = vec![ + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2, + positions: vec![], + string: "println".to_string(), + }, + is_snippet: false, + sort_text: Some("80000000"), + sort_key: (2, "unreachable"), + }, + SortableMatch { + string_match: StringMatch { + candidate_id: 0, + score: 0.2, + positions: vec![], + string: "println!(…)".to_string(), + }, + is_snippet: true, + sort_text: Some("80000000"), + sort_key: (2, "println!(…)"), + }, + ]; + CompletionsMenu::sort_matches(&mut matches, query); + assert_eq!( + matches[0].string_match.string.as_str(), + "println!(…)", + "Match order not expected" + ); +} diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index e60eea2c29..8fc7b3c49e 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -27,8 +27,8 @@ use util::ResultExt; use crate::hover_popover::{hover_markdown_style, open_markdown_url}; use crate::{ - CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, - ResolvedTasks, + CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor, + EditorStyle, ResolvedTasks, actions::{ConfirmCodeAction, ConfirmCompletion}, split_words, styled_runs_for_code_label, }; @@ -657,6 +657,63 @@ impl CompletionsMenu { ) } + pub fn sort_matches(matches: &mut Vec>, query: Option<&str>) { + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] + enum MatchTier<'a> { + WordStartMatch { + sort_score_int: Reverse, + sort_snippet: Reverse, + sort_text: Option<&'a str>, + sort_key: (usize, &'a str), + }, + OtherMatch { + sort_score: Reverse>, + }, + } + + // Our goal here is to intelligently sort completion suggestions. We want to + // balance the raw fuzzy match score with hints from the language server + // + // We first primary sort using fuzzy score by putting matches into two buckets + // strong one and weak one. Among these buckets matches are then compared by + // various criteria like snippet, LSP hints, kind, label text etc. + // + const FUZZY_THRESHOLD: f64 = 0.1317; + + let query_start_lower = query + .and_then(|q| q.chars().next()) + .and_then(|c| c.to_lowercase().next()); + + matches.sort_unstable_by_key(|mat| { + let score = mat.string_match.score; + + let is_other_match = query_start_lower + .map(|query_char| { + !split_words(&mat.string_match.string).any(|word| { + word.chars() + .next() + .and_then(|c| c.to_lowercase().next()) + .map_or(false, |word_char| word_char == query_char) + }) + }) + .unwrap_or(false); + + if is_other_match { + let sort_score = Reverse(OrderedFloat(score)); + MatchTier::OtherMatch { sort_score } + } else { + let sort_score_int = Reverse(if score >= FUZZY_THRESHOLD { 1 } else { 0 }); + let sort_snippet = Reverse(if mat.is_snippet { 1 } else { 0 }); + MatchTier::WordStartMatch { + sort_score_int, + sort_snippet, + sort_text: mat.sort_text, + sort_key: mat.sort_key, + } + } + }); + } + pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) { let mut matches = if let Some(query) = query { fuzzy::match_strings( @@ -681,85 +738,45 @@ impl CompletionsMenu { .collect() }; - let mut additional_matches = Vec::new(); - // Deprioritize all candidates where the query's start does not match the start of any word in the candidate - if let Some(query) = query { - if let Some(query_start) = query.chars().next() { - let (primary, secondary) = matches.into_iter().partition(|string_match| { - split_words(&string_match.string).any(|word| { - // Check that the first codepoint of the word as lowercase matches the first - // codepoint of the query as lowercase - word.chars() - .flat_map(|codepoint| codepoint.to_lowercase()) - .zip(query_start.to_lowercase()) - .all(|(word_cp, query_cp)| word_cp == query_cp) - }) - }); - matches = primary; - additional_matches = secondary; - } - } - - let completions = self.completions.borrow_mut(); if self.sort_completions { - matches.sort_unstable_by_key(|mat| { - // We do want to strike a balance here between what the language server tells us - // to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type - // `Creat` and there is a local variable called `CreateComponent`). - // So what we do is: we bucket all matches into two buckets - // - Strong matches - // - Weak matches - // Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches) - // and the Weak matches are the rest. - // - // For the strong matches, we sort by our fuzzy-finder score first and for the weak - // matches, we prefer language-server sort_text first. - // - // The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score). - // Rest of the matches(weak) can be sorted as language-server expects. + let completions = self.completions.borrow(); - #[derive(PartialEq, Eq, PartialOrd, Ord)] - enum MatchScore<'a> { - Strong { - score: Reverse>, - sort_text: Option<&'a str>, - sort_key: (usize, &'a str), - }, - Weak { - sort_text: Option<&'a str>, - score: Reverse>, - sort_key: (usize, &'a str), - }, - } + let mut sortable_items: Vec> = matches + .into_iter() + .map(|string_match| { + let completion = &completions[string_match.candidate_id]; - let completion = &completions[mat.candidate_id]; - let sort_key = completion.sort_key(); - let sort_text = - if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source { - lsp_completion.sort_text.as_deref() - } else { - None - }; - let score = Reverse(OrderedFloat(mat.score)); + let is_snippet = matches!( + &completion.source, + CompletionSource::Lsp { lsp_completion, .. } + if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) + ); - if mat.score >= 0.2 { - MatchScore::Strong { - score, + let sort_text = + if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source { + lsp_completion.sort_text.as_deref() + } else { + None + }; + + let sort_key = completion.sort_key(); + + SortableMatch { + string_match, + is_snippet, sort_text, sort_key, } - } else { - MatchScore::Weak { - sort_text, - score, - sort_key, - } - } - }); + }) + .collect(); + + Self::sort_matches(&mut sortable_items, query); + + matches = sortable_items + .into_iter() + .map(|sortable| sortable.string_match) + .collect(); } - drop(completions); - - matches.extend(additional_matches); *self.entries.borrow_mut() = matches; self.selected_item = 0; @@ -768,6 +785,14 @@ impl CompletionsMenu { } } +#[derive(Debug)] +pub struct SortableMatch<'a> { + pub string_match: StringMatch, + pub is_snippet: bool, + pub sort_text: Option<&'a str>, + pub sort_key: (usize, &'a str), +} + #[derive(Clone)] pub struct AvailableCodeAction { pub excerpt_id: ExcerptId, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index eb777d6349..abaae331f5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -39,6 +39,8 @@ pub mod scroll; mod selections_collection; pub mod tasks; +#[cfg(test)] +mod code_completion_tests; #[cfg(test)] mod editor_tests; #[cfg(test)] diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index c733d51356..685efc28f7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10704,7 +10704,7 @@ async fn test_completion(cx: &mut TestAppContext) { .confirm_completion(&ConfirmCompletion::default(), window, cx) .unwrap() }); - cx.assert_editor_state("editor.closeˇ"); + cx.assert_editor_state("editor.clobberˇ"); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); } @@ -11266,76 +11266,6 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_completion_sort(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - cx.lsp - .set_request_handler::(move |_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "Range".into(), - sort_text: Some("a".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "r".into(), - sort_text: Some("b".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "ret".into(), - sort_text: Some("c".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "return".into(), - sort_text: Some("d".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "slice".into(), - sort_text: Some("d".into()), - ..Default::default() - }, - ]))) - }); - cx.set_state("rˇ"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, window, cx| { - editor.show_completions( - &ShowCompletions { - trigger: Some("r".into()), - }, - window, - cx, - ); - }); - cx.executor().run_until_parked(); - - cx.update_editor(|editor, _, _| { - if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() - { - assert_eq!( - completion_menu_entries(&menu), - &["r", "ret", "Range", "return"] - ); - } else { - panic!("expected completion menu to be open"); - } - }); -} - #[gpui::test] async fn test_as_is_completions(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -14061,7 +13991,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA { assert_eq!( completion_menu_entries(&menu), - &["bg-red", "bg-blue", "bg-yellow"] + &["bg-blue", "bg-red", "bg-yellow"] ); } else { panic!("expected completion menu to be open"); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c27d3bac25..070b235302 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5006,7 +5006,7 @@ impl Completion { /// A key that can be used to sort completions when displaying /// them to the user. pub fn sort_key(&self) -> (usize, &str) { - const DEFAULT_KIND_KEY: usize = 2; + const DEFAULT_KIND_KEY: usize = 3; let kind_key = self .source // `lsp::CompletionListItemDefaults` has no `kind` field @@ -5015,6 +5015,7 @@ impl Completion { .and_then(|lsp_completion_kind| match lsp_completion_kind { lsp::CompletionItemKind::KEYWORD => Some(0), lsp::CompletionItemKind::VARIABLE => Some(1), + lsp::CompletionItemKind::CONSTANT => Some(2), _ => None, }) .unwrap_or(DEFAULT_KIND_KEY);