highlight_matching_bracket.rs

  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}