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        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}