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