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 let newest_selection = self.selections.newest::<MultiBufferOffset>(&snapshot);
15 // Don't highlight brackets if the selection isn't empty
16 if !newest_selection.is_empty() {
17 self.clear_highlights(HighlightKey::MatchingBracket, cx);
18 return;
19 }
20
21 let buffer_snapshot = snapshot.buffer_snapshot();
22 let head = newest_selection.head();
23 if head > buffer_snapshot.len() {
24 log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
25 return;
26 }
27
28 let mut tail = head;
29 if (self.cursor_shape == CursorShape::Block || self.cursor_shape == CursorShape::Hollow)
30 && head < buffer_snapshot.len()
31 {
32 if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
33 tail += tail_ch.len_utf8();
34 }
35 }
36 let task = cx.background_spawn({
37 let buffer_snapshot = buffer_snapshot.clone();
38 async move { buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None) }
39 });
40 self.refresh_matching_bracket_highlights_task = cx.spawn({
41 let buffer_snapshot = buffer_snapshot.clone();
42 async move |this, cx| {
43 let bracket_ranges = task.await;
44 let current_ranges = this
45 .read_with(cx, |editor, cx| {
46 editor
47 .display_map
48 .read(cx)
49 .text_highlights(HighlightKey::MatchingBracket)
50 .map(|(_, ranges)| ranges.to_vec())
51 })
52 .ok()
53 .flatten();
54 let new_ranges = bracket_ranges.map(|(opening_range, closing_range)| {
55 vec![
56 opening_range.to_anchors(&buffer_snapshot),
57 closing_range.to_anchors(&buffer_snapshot),
58 ]
59 });
60
61 if current_ranges != new_ranges {
62 this.update(cx, |editor, cx| {
63 editor.clear_highlights(HighlightKey::MatchingBracket, cx);
64 if let Some(new_ranges) = new_ranges {
65 editor.highlight_text(
66 HighlightKey::MatchingBracket,
67 new_ranges,
68 HighlightStyle {
69 background_color: Some(
70 cx.theme()
71 .colors()
72 .editor_document_highlight_bracket_background,
73 ),
74 ..Default::default()
75 },
76 cx,
77 )
78 }
79 })
80 .ok();
81 }
82 }
83 });
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
91 use indoc::indoc;
92 use language::{BracketPair, BracketPairConfig, Language, LanguageConfig, LanguageMatcher};
93
94 #[gpui::test]
95 async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
96 init_test(cx, |_| {});
97
98 let mut cx = EditorLspTestContext::new(
99 Language::new(
100 LanguageConfig {
101 name: "Rust".into(),
102 matcher: LanguageMatcher {
103 path_suffixes: vec!["rs".to_string()],
104 ..Default::default()
105 },
106 brackets: BracketPairConfig {
107 pairs: vec![
108 BracketPair {
109 start: "{".to_string(),
110 end: "}".to_string(),
111 close: false,
112 surround: false,
113 newline: true,
114 },
115 BracketPair {
116 start: "(".to_string(),
117 end: ")".to_string(),
118 close: false,
119 surround: false,
120 newline: true,
121 },
122 ],
123 ..Default::default()
124 },
125 ..Default::default()
126 },
127 Some(tree_sitter_rust::LANGUAGE.into()),
128 )
129 .with_brackets_query(indoc! {r#"
130 ("{" @open "}" @close)
131 ("(" @open ")" @close)
132 "#})
133 .unwrap(),
134 Default::default(),
135 cx,
136 )
137 .await;
138
139 // positioning cursor inside bracket highlights both
140 cx.set_state(indoc! {r#"
141 pub fn test("Test ˇargument") {
142 another_test(1, 2, 3);
143 }
144 "#});
145 cx.run_until_parked();
146 cx.assert_editor_text_highlights(
147 HighlightKey::MatchingBracket,
148 indoc! {r#"
149 pub fn test«(»"Test argument"«)» {
150 another_test(1, 2, 3);
151 }
152 "#},
153 );
154
155 cx.set_state(indoc! {r#"
156 pub fn test("Test argument") {
157 another_test(1, ˇ2, 3);
158 }
159 "#});
160 cx.run_until_parked();
161 cx.assert_editor_text_highlights(
162 HighlightKey::MatchingBracket,
163 indoc! {r#"
164 pub fn test("Test argument") {
165 another_test«(»1, 2, 3«)»;
166 }
167 "#},
168 );
169
170 cx.set_state(indoc! {r#"
171 pub fn 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 // positioning outside of brackets removes highlight
186 cx.set_state(indoc! {r#"
187 pub fˇn test("Test argument") {
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 // non empty selection dismisses highlight
202 cx.set_state(indoc! {r#"
203 pub fn test("Te«st argˇ»ument") {
204 another_test(1, 2, 3);
205 }
206 "#});
207 cx.run_until_parked();
208 cx.assert_editor_text_highlights(
209 HighlightKey::MatchingBracket,
210 indoc! {r#"
211 pub fn test«("Test argument") {
212 another_test(1, 2, 3);
213 }
214 "#},
215 );
216 }
217}