1use crate::{Editor, HighlightKey, RangeToAnchorExt, display_map::DisplaySnapshot};
2use gpui::{AppContext, Context, HighlightStyle};
3use language::CursorShape;
4use multi_buffer::MultiBufferOffset;
5use theme::ActiveTheme;
6
7impl Editor {
8 #[ztracing::instrument(skip_all)]
9 pub fn refresh_matching_bracket_highlights(
10 &mut self,
11 snapshot: &DisplaySnapshot,
12 cx: &mut Context<Editor>,
13 ) {
14 self.clear_highlights(HighlightKey::MatchingBracket, cx);
15
16 let newest_selection = self.selections.newest::<MultiBufferOffset>(&snapshot);
17 // Don't highlight brackets if the selection isn't empty
18 if !newest_selection.is_empty() {
19 return;
20 }
21
22 let buffer_snapshot = snapshot.buffer_snapshot();
23 let head = newest_selection.head();
24 if head > buffer_snapshot.len() {
25 log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
26 return;
27 }
28
29 let mut tail = head;
30 if (self.cursor_shape == CursorShape::Block || self.cursor_shape == CursorShape::Hollow)
31 && head < buffer_snapshot.len()
32 {
33 if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
34 tail += tail_ch.len_utf8();
35 }
36 }
37 let task = cx.background_spawn({
38 let buffer_snapshot = buffer_snapshot.clone();
39 async move { buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None) }
40 });
41 self.refresh_matching_bracket_highlights_task = cx.spawn({
42 let buffer_snapshot = buffer_snapshot.clone();
43 async move |editor, cx| {
44 if let Some((opening_range, closing_range)) = task.await {
45 editor
46 .update(cx, |editor, cx| {
47 editor.highlight_text(
48 HighlightKey::MatchingBracket,
49 vec![
50 opening_range.to_anchors(&buffer_snapshot),
51 closing_range.to_anchors(&buffer_snapshot),
52 ],
53 HighlightStyle {
54 background_color: Some(
55 cx.theme()
56 .colors()
57 .editor_document_highlight_bracket_background,
58 ),
59 ..Default::default()
60 },
61 cx,
62 )
63 })
64 .ok();
65 }
66 }
67 });
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
75 use indoc::indoc;
76 use language::{BracketPair, BracketPairConfig, Language, LanguageConfig, LanguageMatcher};
77
78 #[gpui::test]
79 async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
80 init_test(cx, |_| {});
81
82 let mut cx = EditorLspTestContext::new(
83 Language::new(
84 LanguageConfig {
85 name: "Rust".into(),
86 matcher: LanguageMatcher {
87 path_suffixes: vec!["rs".to_string()],
88 ..Default::default()
89 },
90 brackets: BracketPairConfig {
91 pairs: vec![
92 BracketPair {
93 start: "{".to_string(),
94 end: "}".to_string(),
95 close: false,
96 surround: false,
97 newline: true,
98 },
99 BracketPair {
100 start: "(".to_string(),
101 end: ")".to_string(),
102 close: false,
103 surround: false,
104 newline: true,
105 },
106 ],
107 ..Default::default()
108 },
109 ..Default::default()
110 },
111 Some(tree_sitter_rust::LANGUAGE.into()),
112 )
113 .with_brackets_query(indoc! {r#"
114 ("{" @open "}" @close)
115 ("(" @open ")" @close)
116 "#})
117 .unwrap(),
118 Default::default(),
119 cx,
120 )
121 .await;
122
123 // positioning cursor inside bracket highlights both
124 cx.set_state(indoc! {r#"
125 pub fn test("Test ˇargument") {
126 another_test(1, 2, 3);
127 }
128 "#});
129 cx.run_until_parked();
130 cx.assert_editor_text_highlights(
131 HighlightKey::MatchingBracket,
132 indoc! {r#"
133 pub fn test«(»"Test argument"«)» {
134 another_test(1, 2, 3);
135 }
136 "#},
137 );
138
139 cx.set_state(indoc! {r#"
140 pub fn test("Test argument") {
141 another_test(1, ˇ2, 3);
142 }
143 "#});
144 cx.run_until_parked();
145 cx.assert_editor_text_highlights(
146 HighlightKey::MatchingBracket,
147 indoc! {r#"
148 pub fn test("Test argument") {
149 another_test«(»1, 2, 3«)»;
150 }
151 "#},
152 );
153
154 cx.set_state(indoc! {r#"
155 pub fn test("Test argument") {
156 anotherˇ_test(1, 2, 3);
157 }
158 "#});
159 cx.run_until_parked();
160 cx.assert_editor_text_highlights(
161 HighlightKey::MatchingBracket,
162 indoc! {r#"
163 pub fn test("Test argument") «{»
164 another_test(1, 2, 3);
165 «}»
166 "#},
167 );
168
169 // positioning outside of brackets removes highlight
170 cx.set_state(indoc! {r#"
171 pub fˇn test("Test argument") {
172 another_test(1, 2, 3);
173 }
174 "#});
175 cx.run_until_parked();
176 cx.assert_editor_text_highlights(
177 HighlightKey::MatchingBracket,
178 indoc! {r#"
179 pub fn test("Test argument") {
180 another_test(1, 2, 3);
181 }
182 "#},
183 );
184
185 // non empty selection dismisses highlight
186 cx.set_state(indoc! {r#"
187 pub fn test("Te«st argˇ»ument") {
188 another_test(1, 2, 3);
189 }
190 "#});
191 cx.run_until_parked();
192 cx.assert_editor_text_highlights(
193 HighlightKey::MatchingBracket,
194 indoc! {r#"
195 pub fn test«("Test argument") {
196 another_test(1, 2, 3);
197 }
198 "#},
199 );
200 }
201}