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