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