Fix matching braces in jsx/tsx tags (#32196)

Julia Ryan and Conrad Irwin created

Closes #27998

Also fixed an issue where jumping back from closing to opening tags
didn't work in javascript due to missing brackets in our tree-sitter
query.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/languages/src/javascript/brackets.scm          |  2 
crates/vim/src/motion.rs                              | 34 +++++++++++++
crates/vim/src/test/neovim_backed_test_context.rs     | 24 +++++++++
crates/vim/test_data/test_matching_braces_in_tag.json |  3 +
4 files changed, 63 insertions(+)

Detailed changes

crates/vim/src/motion.rs ๐Ÿ”—

@@ -2279,6 +2279,17 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
         line_end = map.max_point().to_point(map);
     }
 
+    if let Some((opening_range, closing_range)) = map
+        .buffer_snapshot
+        .innermost_enclosing_bracket_ranges(offset..offset, None)
+    {
+        if opening_range.contains(&offset) {
+            return closing_range.start.to_display_point(map);
+        } else if closing_range.contains(&offset) {
+            return opening_range.start.to_display_point(map);
+        }
+    }
+
     let line_range = map.prev_line_boundary(point).0..line_end;
     let visible_line_range =
         line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
@@ -3242,6 +3253,29 @@ mod test {
         </a>"#});
     }
 
+    #[gpui::test]
+    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
+
+        // test brackets within tags
+        cx.set_shared_state(indoc! {r"function f() {
+            return (
+                <div rules={ห‡[{ a: 1 }]}>
+                    <h1>test</h1>
+                </div>
+            );
+        }"})
+            .await;
+        cx.simulate_shared_keystrokes("%").await;
+        cx.shared_state().await.assert_eq(indoc! {r"function f() {
+            return (
+                <div rules={[{ a: 1 }ห‡]}>
+                    <h1>test</h1>
+                </div>
+            );
+        }"});
+    }
+
     #[gpui::test]
     async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/test/neovim_backed_test_context.rs ๐Ÿ”—

@@ -183,6 +183,30 @@ impl NeovimBackedTestContext {
         }
     }
 
+    pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
+        #[cfg(feature = "neovim")]
+        cx.executor().allow_parking();
+        // rust stores the name of the test on the current thread.
+        // We use this to automatically name a file that will store
+        // the neovim connection's requests/responses so that we can
+        // run without neovim on CI.
+        let thread = thread::current();
+        let test_name = thread
+            .name()
+            .expect("thread is not named")
+            .split(':')
+            .next_back()
+            .unwrap()
+            .to_string();
+        Self {
+            cx: VimTestContext::new_typescript(cx).await,
+            neovim: NeovimConnection::new(test_name).await,
+
+            last_set_state: None,
+            recent_keystrokes: Default::default(),
+        }
+    }
+
     pub async fn set_shared_state(&mut self, marked_text: &str) {
         let mode = if marked_text.contains('ยป') {
             Mode::Visual

crates/vim/test_data/test_matching_braces_in_tag.json ๐Ÿ”—

@@ -0,0 +1,3 @@
+{"Put":{"state":"function f() {\n    return (\n        <div rules={ห‡[{ a: 1 }]}>\n            <h1>test</h1>\n        </div>\n    );\n}"}}
+{"Key":"%"}
+{"Get":{"state":"function f() {\n    return (\n        <div rules={[{ a: 1 }ห‡]}>\n            <h1>test</h1>\n        </div>\n    );\n}","mode":"Normal"}}