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