1use crate::{Editor, RangeToAnchorExt};
2use gpui::{Context, Window};
3use language::CursorShape;
4
5enum MatchingBracketHighlight {}
6
7pub fn refresh_matching_bracket_highlights(
8 editor: &mut Editor,
9 window: &mut Window,
10 cx: &mut Context<Editor>,
11) {
12 editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
13
14 let newest_selection = editor.selections.newest::<usize>(cx);
15 // Don't highlight brackets if the selection isn't empty
16 if !newest_selection.is_empty() {
17 return;
18 }
19
20 let snapshot = editor.snapshot(window, cx);
21 let head = newest_selection.head();
22 if head > snapshot.buffer_snapshot.len() {
23 log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
24 return;
25 }
26
27 let mut tail = head;
28 if (editor.cursor_shape == CursorShape::Block || editor.cursor_shape == CursorShape::Hollow)
29 && head < snapshot.buffer_snapshot.len()
30 {
31 tail += 1;
32 }
33
34 if let Some((opening_range, closing_range)) = snapshot
35 .buffer_snapshot
36 .innermost_enclosing_bracket_ranges(head..tail, None)
37 {
38 editor.highlight_background::<MatchingBracketHighlight>(
39 &[
40 opening_range.to_anchors(&snapshot.buffer_snapshot),
41 closing_range.to_anchors(&snapshot.buffer_snapshot),
42 ],
43 |theme| theme.colors().editor_document_highlight_bracket_background,
44 cx,
45 )
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
53 use indoc::indoc;
54 use language::{BracketPair, BracketPairConfig, Language, LanguageConfig, LanguageMatcher};
55
56 #[gpui::test]
57 async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
58 init_test(cx, |_| {});
59
60 let mut cx = EditorLspTestContext::new(
61 Language::new(
62 LanguageConfig {
63 name: "Rust".into(),
64 matcher: LanguageMatcher {
65 path_suffixes: vec!["rs".to_string()],
66 ..Default::default()
67 },
68 brackets: BracketPairConfig {
69 pairs: vec![
70 BracketPair {
71 start: "{".to_string(),
72 end: "}".to_string(),
73 close: false,
74 surround: false,
75 newline: true,
76 },
77 BracketPair {
78 start: "(".to_string(),
79 end: ")".to_string(),
80 close: false,
81 surround: false,
82 newline: true,
83 },
84 ],
85 ..Default::default()
86 },
87 ..Default::default()
88 },
89 Some(tree_sitter_rust::LANGUAGE.into()),
90 )
91 .with_brackets_query(indoc! {r#"
92 ("{" @open "}" @close)
93 ("(" @open ")" @close)
94 "#})
95 .unwrap(),
96 Default::default(),
97 cx,
98 )
99 .await;
100
101 // positioning cursor inside bracket highlights both
102 cx.set_state(indoc! {r#"
103 pub fn test("Test ˇargument") {
104 another_test(1, 2, 3);
105 }
106 "#});
107 cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
108 pub fn test«(»"Test argument"«)» {
109 another_test(1, 2, 3);
110 }
111 "#});
112
113 cx.set_state(indoc! {r#"
114 pub fn test("Test argument") {
115 another_test(1, ˇ2, 3);
116 }
117 "#});
118 cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
119 pub fn test("Test argument") {
120 another_test«(»1, 2, 3«)»;
121 }
122 "#});
123
124 cx.set_state(indoc! {r#"
125 pub fn test("Test argument") {
126 anotherˇ_test(1, 2, 3);
127 }
128 "#});
129 cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
130 pub fn test("Test argument") «{»
131 another_test(1, 2, 3);
132 «}»
133 "#});
134
135 // positioning outside of brackets removes highlight
136 cx.set_state(indoc! {r#"
137 pub fˇn test("Test argument") {
138 another_test(1, 2, 3);
139 }
140 "#});
141 cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
142 pub fn test("Test argument") {
143 another_test(1, 2, 3);
144 }
145 "#});
146
147 // non empty selection dismisses highlight
148 cx.set_state(indoc! {r#"
149 pub fn test("Te«st argˇ»ument") {
150 another_test(1, 2, 3);
151 }
152 "#});
153 cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
154 pub fn test("Test argument") {
155 another_test(1, 2, 3);
156 }
157 "#});
158 }
159}