highlight_matching_bracket.rs

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