highlight_matching_bracket.rs

  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}