Add support for auto-closing of JSX tags (#25681)

Ben Kunkle , Cole Miller , Max Brunsfeld , Marshall Bowers , Mikayla , and Peter Tripp created

Closes #4271

Implemented by kicking of a task on the main thread at the end of
`Editor::handle_input` which waits for the buffer to be re-parsed before
checking if JSX tag completion possible based on the recent edits, and
if it is then it spawns a task on the background thread to generate the
edits to be auto-applied to the buffer

Release Notes:

- Added support for auto-closing of JSX tags

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>

Change summary

Cargo.lock                                  |   1 
assets/settings/default.json                |   5 
crates/editor/Cargo.toml                    |   1 
crates/editor/src/editor.rs                 |  11 
crates/editor/src/editor_tests.rs           | 239 ++++++++
crates/editor/src/jsx_tag_auto_close.rs     | 616 +++++++++++++++++++++++
crates/language/src/buffer.rs               |  19 
crates/language/src/language.rs             |  38 +
crates/language/src/language_settings.rs    |  16 
crates/language/src/syntax_map.rs           |   6 
crates/languages/src/javascript/config.toml |   6 
crates/languages/src/lib.rs                 | 265 +++++----
crates/languages/src/tsx/config.toml        |   6 
crates/multi_buffer/src/multi_buffer.rs     |   5 
crates/rope/src/rope.rs                     |  82 +++
15 files changed, 1,187 insertions(+), 129 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4154,6 +4154,7 @@ dependencies = [
  "inline_completion",
  "itertools 0.14.0",
  "language",
+ "languages",
  "linkify",
  "log",
  "lsp",

assets/settings/default.json 🔗

@@ -1298,6 +1298,11 @@
     // "semi": false,
     // "singleQuote": true
   },
+  // Settings for auto-closing of JSX tags.
+  "jsx_tag_auto_close": {
+    // // Whether to auto-close JSX tags.
+    // "enabled": true
+  },
   // LSP Specific settings.
   "lsp": {
     // Specify the LSP name as a key here.

crates/editor/Cargo.toml 🔗

@@ -94,6 +94,7 @@ ctor.workspace = true
 env_logger.workspace = true
 gpui = { workspace = true, features = ["test-support"] }
 language = { workspace = true, features = ["test-support"] }
+languages = {workspace = true, features = ["test-support"] }
 lsp = { workspace = true, features = ["test-support"] }
 multi_buffer = { workspace = true, features = ["test-support"] }
 project = { workspace = true, features = ["test-support"] }

crates/editor/src/editor.rs 🔗

@@ -28,6 +28,7 @@ mod hover_popover;
 mod indent_guides;
 mod inlay_hint_cache;
 pub mod items;
+mod jsx_tag_auto_close;
 mod linked_editing_ranges;
 mod lsp_ext;
 mod mouse_context_menu;
@@ -724,6 +725,7 @@ pub struct Editor {
     use_autoclose: bool,
     use_auto_surround: bool,
     auto_replace_emoji_shortcode: bool,
+    jsx_tag_auto_close_enabled_in_any_buffer: bool,
     show_git_blame_gutter: bool,
     show_git_blame_inline: bool,
     show_git_blame_inline_delay_task: Option<Task<()>>,
@@ -1410,6 +1412,7 @@ impl Editor {
             use_autoclose: true,
             use_auto_surround: true,
             auto_replace_emoji_shortcode: false,
+            jsx_tag_auto_close_enabled_in_any_buffer: false,
             leader_peer_id: None,
             remote_id: None,
             hover_state: Default::default(),
@@ -1493,6 +1496,7 @@ impl Editor {
 
         this.end_selection(window, cx);
         this.scroll_manager.show_scrollbar(window, cx);
+        jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
 
         if mode == EditorMode::Full {
             let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
@@ -3101,6 +3105,9 @@ impl Editor {
         drop(snapshot);
 
         self.transact(window, cx, |this, window, cx| {
+            let initial_buffer_versions =
+                jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx);
+
             this.buffer.update(cx, |buffer, cx| {
                 buffer.edit(edits, this.autoindent_mode.clone(), cx);
             });
@@ -3188,6 +3195,7 @@ impl Editor {
             this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
             linked_editing_ranges::refresh_linked_ranges(this, window, cx);
             this.refresh_inline_completion(true, false, window, cx);
+            jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx);
         });
     }
 
@@ -15393,6 +15401,7 @@ impl Editor {
                 let buffer = self.buffer.read(cx);
                 self.registered_buffers
                     .retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
+                jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
                 cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
             }
             multi_buffer::Event::ExcerptsEdited {
@@ -15412,6 +15421,7 @@ impl Editor {
             }
             multi_buffer::Event::Reparsed(buffer_id) => {
                 self.tasks_update_task = Some(self.refresh_runnables(window, cx));
+                jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
 
                 cx.emit(EditorEvent::Reparsed(*buffer_id));
             }
@@ -15420,6 +15430,7 @@ impl Editor {
             }
             multi_buffer::Event::LanguageChanged(buffer_id) => {
                 linked_editing_ranges::refresh_linked_ranges(self, window, cx);
+                jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
                 cx.emit(EditorEvent::Reparsed(*buffer_id));
                 cx.notify();
             }

crates/editor/src/editor_tests.rs 🔗

@@ -16779,6 +16779,245 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
     "});
 }
 
+mod autoclose_tags {
+    use super::*;
+    use language::language_settings::JsxTagAutoCloseSettings;
+    use languages::language;
+
+    async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext {
+        init_test(cx, |settings| {
+            settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
+        });
+
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.update_buffer(|buffer, cx| {
+            let language = language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into());
+
+            buffer.set_language(Some(language), cx)
+        });
+
+        cx
+    }
+
+    macro_rules! check {
+        ($name:ident, $initial:literal + $input:literal => $expected:expr) => {
+            #[gpui::test]
+            async fn $name(cx: &mut TestAppContext) {
+                let mut cx = test_setup(cx).await;
+                cx.set_state($initial);
+                cx.run_until_parked();
+
+                cx.update_editor(|editor, window, cx| {
+                    editor.handle_input($input, window, cx);
+                });
+                cx.run_until_parked();
+                cx.assert_editor_state($expected);
+            }
+        };
+    }
+
+    check!(
+        test_basic,
+        "<divˇ" + ">" => "<div>ˇ</div>"
+    );
+
+    check!(
+        test_basic_nested,
+        "<div><divˇ</div>" + ">" => "<div><div>ˇ</div></div>"
+    );
+
+    check!(
+        test_basic_ignore_already_closed,
+        "<div><divˇ</div></div>" + ">" => "<div><div>ˇ</div></div>"
+    );
+
+    check!(
+        test_doesnt_autoclose_closing_tag,
+        "</divˇ" + ">" => "</div>ˇ"
+    );
+
+    check!(
+        test_jsx_attr,
+        "<div attr={</div>}ˇ" + ">" => "<div attr={</div>}>ˇ</div>"
+    );
+
+    check!(
+        test_ignores_closing_tags_in_expr_block,
+        "<div><divˇ{</div>}</div>" + ">" => "<div><div>ˇ</div>{</div>}</div>"
+    );
+
+    check!(
+        test_doesnt_autoclose_on_gt_in_expr,
+        "<div attr={1 ˇ" + ">" => "<div attr={1 >ˇ"
+    );
+
+    check!(
+        test_ignores_closing_tags_with_different_tag_names,
+        "<div><divˇ</div></span>" + ">" => "<div><div>ˇ</div></div></span>"
+    );
+
+    check!(
+        test_autocloses_in_jsx_expression,
+        "<div>{<divˇ}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
+    );
+
+    check!(
+        test_doesnt_autoclose_already_closed_in_jsx_expression,
+        "<div>{<divˇ</div>}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
+    );
+
+    check!(
+        test_autocloses_fragment,
+        "<ˇ" + ">" => "<>ˇ</>"
+    );
+
+    check!(
+        test_does_not_include_type_argument_in_autoclose_tag_name,
+        "<Component<T> attr={boolean_value}ˇ" + ">" => "<Component<T> attr={boolean_value}>ˇ</Component>"
+    );
+
+    check!(
+        test_does_not_autoclose_doctype,
+        "<!DOCTYPE htmlˇ" + ">" => "<!DOCTYPE html>ˇ"
+    );
+
+    check!(
+        test_does_not_autoclose_comment,
+        "<!-- comment --ˇ" + ">" => "<!-- comment -->ˇ"
+    );
+
+    check!(
+        test_multi_cursor_autoclose_same_tag,
+        r#"
+        <divˇ
+        <divˇ
+        "#
+        + ">" =>
+        r#"
+        <div>ˇ</div>
+        <div>ˇ</div>
+        "#
+    );
+
+    check!(
+        test_multi_cursor_autoclose_different_tags,
+        r#"
+        <divˇ
+        <spanˇ
+        "#
+        + ">" =>
+        r#"
+        <div>ˇ</div>
+        <span>ˇ</span>
+        "#
+    );
+
+    check!(
+        test_multi_cursor_autoclose_some_dont_autoclose_others,
+        r#"
+        <divˇ
+        <div /ˇ
+        <spanˇ</span>
+        <!DOCTYPE htmlˇ
+        </headˇ
+        <Component<T>ˇ
+        ˇ
+        "#
+        + ">" =>
+        r#"
+        <div>ˇ</div>
+        <div />ˇ
+        <span>ˇ</span>
+        <!DOCTYPE html>ˇ
+        </head>ˇ
+        <Component<T>>ˇ</Component>
+        >ˇ
+        "#
+    );
+
+    check!(
+        test_doesnt_mess_up_trailing_text,
+        "<divˇfoobar" + ">" => "<div>ˇ</div>foobar"
+    );
+
+    #[gpui::test]
+    async fn test_multibuffer(cx: &mut TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
+        });
+
+        let buffer_a = cx.new(|cx| {
+            let mut buf = language::Buffer::local("<div", cx);
+            buf.set_language(
+                Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
+                cx,
+            );
+            buf
+        });
+        let buffer_b = cx.new(|cx| {
+            let mut buf = language::Buffer::local("<pre", cx);
+            buf.set_language(
+                Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
+                cx,
+            );
+            buf
+        });
+        let buffer_c = cx.new(|cx| {
+            let buf = language::Buffer::local("<span", cx);
+            buf
+        });
+        let buffer = cx.new(|cx| {
+            let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
+            buf.push_excerpts(
+                buffer_a,
+                [ExcerptRange {
+                    context: text::Anchor::MIN..text::Anchor::MAX,
+                    primary: None,
+                }],
+                cx,
+            );
+            buf.push_excerpts(
+                buffer_b,
+                [ExcerptRange {
+                    context: text::Anchor::MIN..text::Anchor::MAX,
+                    primary: None,
+                }],
+                cx,
+            );
+            buf.push_excerpts(
+                buffer_c,
+                [ExcerptRange {
+                    context: text::Anchor::MIN..text::Anchor::MAX,
+                    primary: None,
+                }],
+                cx,
+            );
+            buf
+        });
+        let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
+
+        let mut cx = EditorTestContext::for_editor(editor, cx).await;
+
+        cx.update_editor(|editor, window, cx| {
+            editor.change_selections(None, window, cx, |selections| {
+                selections.select(vec![
+                    Selection::from_offset(4),
+                    Selection::from_offset(9),
+                    Selection::from_offset(15),
+                ])
+            })
+        });
+        cx.run_until_parked();
+
+        cx.update_editor(|editor, window, cx| {
+            editor.handle_input(">", window, cx);
+        });
+        cx.run_until_parked();
+
+        cx.assert_editor_state("<div>ˇ</div>\n<pre>ˇ</pre>\n<span>ˇ");
+    }
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
     point..point

crates/editor/src/jsx_tag_auto_close.rs 🔗

@@ -0,0 +1,616 @@
+use anyhow::{anyhow, Context as _, Result};
+use collections::HashMap;
+use gpui::{Context, Entity, Window};
+use multi_buffer::{MultiBuffer, ToOffset};
+use std::ops::Range;
+use util::ResultExt as _;
+
+use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};
+use text::{Anchor, OffsetRangeExt as _};
+
+use crate::Editor;
+
+pub struct JsxTagCompletionState {
+    edit_index: usize,
+    open_tag_range: Range<usize>,
+}
+
+/// Index of the named child within an open or close tag
+/// that corresponds to the tag name
+/// Note that this is not configurable, i.e. we assume the first
+/// named child of a tag node is the tag name
+const TS_NODE_TAG_NAME_CHILD_INDEX: usize = 0;
+
+/// Maximum number of parent elements to walk back when checking if an open tag
+/// is already closed.
+///
+/// See the comment in `generate_auto_close_edits` for more details
+const ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT: usize = 2;
+
+pub(crate) fn should_auto_close(
+    buffer: &BufferSnapshot,
+    edited_ranges: &[Range<usize>],
+    config: &JsxTagAutoCloseConfig,
+) -> Option<Vec<JsxTagCompletionState>> {
+    let mut to_auto_edit = vec![];
+    for (index, edited_range) in edited_ranges.iter().enumerate() {
+        let text = buffer
+            .text_for_range(edited_range.clone())
+            .collect::<String>();
+        if !text.ends_with(">") {
+            continue;
+        }
+        let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
+            continue;
+        };
+        let Some(node) = layer
+            .node()
+            .named_descendant_for_byte_range(edited_range.start, edited_range.end)
+        else {
+            continue;
+        };
+        let mut jsx_open_tag_node = node;
+        if node.grammar_name() != config.open_tag_node_name {
+            if let Some(parent) = node.parent() {
+                if parent.grammar_name() == config.open_tag_node_name {
+                    jsx_open_tag_node = parent;
+                }
+            }
+        }
+        if jsx_open_tag_node.grammar_name() != config.open_tag_node_name {
+            continue;
+        }
+
+        let first_two_chars: Option<[char; 2]> = {
+            let mut chars = buffer
+                .text_for_range(jsx_open_tag_node.byte_range())
+                .flat_map(|chunk| chunk.chars());
+            if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) {
+                Some([c1, c2])
+            } else {
+                None
+            }
+        };
+        if let Some(chars) = first_two_chars {
+            if chars[0] != '<' {
+                continue;
+            }
+            if chars[1] == '!' || chars[1] == '/' {
+                continue;
+            }
+        }
+
+        to_auto_edit.push(JsxTagCompletionState {
+            edit_index: index,
+            open_tag_range: jsx_open_tag_node.byte_range(),
+        });
+    }
+    if to_auto_edit.is_empty() {
+        return None;
+    } else {
+        return Some(to_auto_edit);
+    }
+}
+
+pub(crate) fn generate_auto_close_edits(
+    buffer: &BufferSnapshot,
+    ranges: &[Range<usize>],
+    config: &JsxTagAutoCloseConfig,
+    state: Vec<JsxTagCompletionState>,
+) -> Result<Vec<(Range<Anchor>, String)>> {
+    let mut edits = Vec::with_capacity(state.len());
+    for auto_edit in state {
+        let edited_range = ranges[auto_edit.edit_index].clone();
+        let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
+            continue;
+        };
+        let layer_root_node = layer.node();
+        let Some(open_tag) = layer_root_node.descendant_for_byte_range(
+            auto_edit.open_tag_range.start,
+            auto_edit.open_tag_range.end,
+        ) else {
+            continue;
+        };
+        assert!(open_tag.kind() == config.open_tag_node_name);
+        let tag_name = open_tag
+            .named_child(TS_NODE_TAG_NAME_CHILD_INDEX)
+            .filter(|node| node.kind() == config.tag_name_node_name)
+            .map_or("".to_string(), |node| {
+                buffer.text_for_range(node.byte_range()).collect::<String>()
+            });
+
+        /*
+         * Naive check to see if the tag is already closed
+         * Essentially all we do is count the number of open and close tags
+         * with the same tag name as the open tag just entered by the user
+         * The search is limited to some scope determined by
+         * `ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT`
+         *
+         * The limit is preferable to walking up the tree until we find a non-tag node,
+         * and then checking the entire tree, as this is unnecessarily expensive, and
+         * risks false positives
+         * eg. a `</div>` tag without a corresponding opening tag exists 25 lines away
+         *     and the user typed in `<div>`, intuitively we still want to auto-close it because
+         *     the other `</div>` tag is almost certainly not supposed to be the closing tag for the
+         *     current element
+         *
+         * We have to walk up the tree some amount because tree-sitters error correction is not
+         * designed to handle this case, and usually does not represent the tree structure
+         * in the way we might expect,
+         *
+         * We half to walk up the tree until we hit an element with a different open tag name (`doing_deep_search == true`)
+         * because tree-sitter may pair the new open tag with the root of the tree's closing tag leaving the
+         * root's opening tag unclosed.
+         * e.g
+         *      ```
+         *      <div>
+         *          <div>|cursor here|
+         *      </div>
+         *      ```
+         *     in Astro/vue/svelte tree-sitter represented the tree as
+         *      (
+         *          (jsx_element
+         *              (jsx_opening_element
+         *                  "<div>")
+         *          )
+         *          (jsx_element
+         *              (jsx_opening_element
+         *                  "<div>") // <- cursor is here
+         *              (jsx_closing_element
+         *                  "</div>")
+         *          )
+         *      )
+         *     so if we only walked to the first `jsx_element` node,
+         *     we would mistakenly identify the div entered by the
+         *     user as already being closed, despite this clearly
+         *     being false
+         *
+         * The errors with the tree-sitter tree caused by error correction,
+         * are also why the naive algorithm was chosen, as the alternative
+         * approach would be to maintain or construct a full parse tree (like tree-sitter)
+         * that better represents errors in a way that we can simply check
+         * the enclosing scope of the entered tag for a closing tag
+         * This is far more complex and expensive, and was deemed impractical
+         * given that the naive algorithm is sufficient in the majority of cases.
+         */
+        {
+            let tag_node_name_equals = |node: &Node, tag_name_node_name: &str, name: &str| {
+                let is_empty = name.len() == 0;
+                if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) {
+                    if node_name.kind() != tag_name_node_name {
+                        return is_empty;
+                    }
+                    let range = node_name.byte_range();
+                    return buffer.text_for_range(range).equals_str(name);
+                }
+                return is_empty;
+            };
+
+            let tree_root_node = {
+                let mut ancestors = Vec::with_capacity(
+                    // estimate of max, not based on any data,
+                    // but trying to avoid excessive reallocation
+                    16,
+                );
+                ancestors.push(layer_root_node);
+                let mut cur = layer_root_node;
+                // walk down the tree until we hit the open tag
+                // note: this is what node.parent() does internally
+                while let Some(descendant) = cur.child_with_descendant(open_tag) {
+                    if descendant == open_tag {
+                        break;
+                    }
+                    ancestors.push(descendant);
+                    cur = descendant;
+                }
+
+                assert!(ancestors.len() > 0);
+
+                let mut tree_root_node = open_tag;
+
+                let mut parent_element_node_count = 0;
+                let mut doing_deep_search = false;
+
+                for &ancestor in ancestors.iter().rev() {
+                    tree_root_node = ancestor;
+                    let is_element = ancestor.kind() == config.jsx_element_node_name;
+                    let is_error = ancestor.is_error();
+                    if is_error || !is_element {
+                        break;
+                    }
+                    if is_element {
+                        let is_first = parent_element_node_count == 0;
+                        if !is_first {
+                            let has_open_tag_with_same_tag_name = ancestor
+                                .named_child(0)
+                                .filter(|n| n.kind() == config.open_tag_node_name)
+                                .map_or(false, |element_open_tag_node| {
+                                    tag_node_name_equals(
+                                        &element_open_tag_node,
+                                        &config.tag_name_node_name,
+                                        &tag_name,
+                                    )
+                                });
+                            if has_open_tag_with_same_tag_name {
+                                doing_deep_search = true;
+                            } else if doing_deep_search {
+                                break;
+                            }
+                        }
+                        parent_element_node_count += 1;
+                        if !doing_deep_search
+                            && parent_element_node_count
+                                >= ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT
+                        {
+                            break;
+                        }
+                    }
+                }
+                tree_root_node
+            };
+
+            let mut unclosed_open_tag_count: i32 = 0;
+
+            let mut cursor = layer_root_node.walk();
+
+            let mut stack = Vec::with_capacity(tree_root_node.descendant_count());
+            stack.extend(tree_root_node.children(&mut cursor));
+
+            let mut has_erroneous_close_tag = false;
+            let mut erroneous_close_tag_node_name = "";
+            let mut erroneous_close_tag_name_node_name = "";
+            if let Some(name) = config.erroneous_close_tag_node_name.as_deref() {
+                has_erroneous_close_tag = true;
+                erroneous_close_tag_node_name = name;
+                erroneous_close_tag_name_node_name = config
+                    .erroneous_close_tag_name_node_name
+                    .as_deref()
+                    .unwrap_or(&config.tag_name_node_name);
+            }
+
+            let is_after_open_tag = |node: &Node| {
+                return node.start_byte() < open_tag.start_byte()
+                    && node.end_byte() < open_tag.start_byte();
+            };
+
+            // perf: use cursor for more efficient traversal
+            // if child -> go to child
+            // else if next sibling -> go to next sibling
+            // else -> go to parent
+            // if parent == tree_root_node -> break
+            while let Some(node) = stack.pop() {
+                let kind = node.kind();
+                if kind == config.open_tag_node_name {
+                    if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
+                        unclosed_open_tag_count += 1;
+                    }
+                } else if kind == config.close_tag_node_name {
+                    if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
+                        unclosed_open_tag_count -= 1;
+                    }
+                } else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name {
+                    if tag_node_name_equals(&node, erroneous_close_tag_name_node_name, &tag_name) {
+                        if !is_after_open_tag(&node) {
+                            unclosed_open_tag_count -= 1;
+                        }
+                    }
+                } else if kind == config.jsx_element_node_name {
+                    // perf: filter only open,close,element,erroneous nodes
+                    stack.extend(node.children(&mut cursor));
+                }
+            }
+
+            if unclosed_open_tag_count <= 0 {
+                // skip if already closed
+                continue;
+            }
+        }
+        let edit_anchor = buffer.anchor_after(edited_range.end);
+        let edit_range = edit_anchor..edit_anchor;
+        edits.push((edit_range, format!("</{}>", tag_name)));
+    }
+    return Ok(edits);
+}
+
+pub(crate) fn refresh_enabled_in_any_buffer(
+    editor: &mut Editor,
+    multi_buffer: &Entity<MultiBuffer>,
+    cx: &Context<Editor>,
+) {
+    editor.jsx_tag_auto_close_enabled_in_any_buffer = {
+        let multi_buffer = multi_buffer.read(cx);
+        let mut found_enabled = false;
+        multi_buffer.for_each_buffer(|buffer| {
+            let buffer = buffer.read(cx);
+            let snapshot = buffer.snapshot();
+            for syntax_layer in snapshot.syntax_layers() {
+                let language = syntax_layer.language;
+                if language.config().jsx_tag_auto_close.is_none() {
+                    continue;
+                }
+                let language_settings = language::language_settings::language_settings(
+                    Some(language.name()),
+                    snapshot.file(),
+                    cx,
+                );
+                if language_settings.jsx_tag_auto_close.enabled {
+                    found_enabled = true;
+                }
+            }
+        });
+
+        found_enabled
+    };
+}
+
+pub(crate) type InitialBufferVersionsMap = HashMap<language::BufferId, clock::Global>;
+
+pub(crate) fn construct_initial_buffer_versions_map<
+    D: ToOffset + Copy,
+    _S: Into<std::sync::Arc<str>>,
+>(
+    editor: &Editor,
+    edits: &[(Range<D>, _S)],
+    cx: &Context<Editor>,
+) -> InitialBufferVersionsMap {
+    let mut initial_buffer_versions = InitialBufferVersionsMap::default();
+
+    if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
+        return initial_buffer_versions;
+    }
+
+    for (edit_range, _) in edits {
+        let edit_range_buffer = editor
+            .buffer()
+            .read(cx)
+            .excerpt_containing(edit_range.end, cx)
+            .map(|e| e.1);
+        if let Some(buffer) = edit_range_buffer {
+            let (buffer_id, buffer_version) =
+                buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version.clone()));
+            initial_buffer_versions.insert(buffer_id, buffer_version);
+        }
+    }
+    return initial_buffer_versions;
+}
+
+pub(crate) fn handle_from(
+    editor: &Editor,
+    initial_buffer_versions: InitialBufferVersionsMap,
+    window: &mut Window,
+    cx: &mut Context<Editor>,
+) {
+    if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
+        return;
+    }
+
+    struct JsxAutoCloseEditContext {
+        buffer: Entity<language::Buffer>,
+        config: language::JsxTagAutoCloseConfig,
+        edits: Vec<Range<usize>>,
+    }
+
+    let mut edit_contexts =
+        HashMap::<(language::BufferId, language::LanguageId), JsxAutoCloseEditContext>::default();
+
+    for (buffer_id, buffer_version_initial) in initial_buffer_versions {
+        let Some(buffer) = editor.buffer.read(cx).buffer(buffer_id) else {
+            continue;
+        };
+        let snapshot = buffer.read(cx).snapshot();
+        for edit in buffer.read(cx).edits_since(&buffer_version_initial) {
+            let Some(language) = snapshot.language_at(edit.new.end) else {
+                continue;
+            };
+
+            let Some(config) = language.config().jsx_tag_auto_close.as_ref() else {
+                continue;
+            };
+
+            let language_settings = snapshot.settings_at(edit.new.end, cx);
+            if !language_settings.jsx_tag_auto_close.enabled {
+                continue;
+            }
+
+            edit_contexts
+                .entry((snapshot.remote_id(), language.id()))
+                .or_insert_with(|| JsxAutoCloseEditContext {
+                    buffer: buffer.clone(),
+                    config: config.clone(),
+                    edits: vec![],
+                })
+                .edits
+                .push(edit.new);
+        }
+    }
+
+    for ((buffer_id, _), auto_close_context) in edit_contexts {
+        let JsxAutoCloseEditContext {
+            buffer,
+            config: jsx_tag_auto_close_config,
+            edits: edited_ranges,
+        } = auto_close_context;
+
+        let (buffer_version_initial, mut buffer_parse_status_rx) =
+            buffer.read_with(cx, |buffer, _| (buffer.version(), buffer.parse_status()));
+
+        cx.spawn_in(window, |this, mut cx| async move {
+            let Some(buffer_parse_status) = buffer_parse_status_rx.recv().await.ok() else {
+                return Some(());
+            };
+            if buffer_parse_status == language::ParseStatus::Parsing {
+                let Some(language::ParseStatus::Idle) = buffer_parse_status_rx.recv().await.ok()
+                else {
+                    return Some(());
+                };
+            }
+
+            let buffer_snapshot = buffer.read_with(&cx, |buf, _| buf.snapshot()).ok()?;
+
+            let Some(edit_behavior_state) =
+                should_auto_close(&buffer_snapshot, &edited_ranges, &jsx_tag_auto_close_config)
+            else {
+                return Some(());
+            };
+
+            let ensure_no_edits_since_start = || -> Option<()> {
+                // <div>wef,wefwef
+                let has_edits_since_start = this
+                    .read_with(&cx, |this, cx| {
+                        this.buffer.read_with(cx, |buffer, cx| {
+                            buffer.buffer(buffer_id).map_or(true, |buffer| {
+                                buffer.read_with(cx, |buffer, _| {
+                                    buffer.has_edits_since(&buffer_version_initial)
+                                })
+                            })
+                        })
+                    })
+                    .ok()?;
+
+                if has_edits_since_start {
+                    Err(anyhow!(
+                        "Auto-close Operation Failed - Buffer has edits since start"
+                    ))
+                    .log_err()?;
+                }
+
+                Some(())
+            };
+
+            ensure_no_edits_since_start()?;
+
+            let edits = cx
+                .background_executor()
+                .spawn({
+                    let buffer_snapshot = buffer_snapshot.clone();
+                    async move {
+                        generate_auto_close_edits(
+                            &buffer_snapshot,
+                            &edited_ranges,
+                            &jsx_tag_auto_close_config,
+                            edit_behavior_state,
+                        )
+                    }
+                })
+                .await;
+
+            let edits = edits
+                .context("Auto-close Operation Failed - Failed to compute edits")
+                .log_err()?;
+
+            if edits.is_empty() {
+                return Some(());
+            }
+
+            // check again after awaiting background task before applying edits
+            ensure_no_edits_since_start()?;
+
+            let multi_buffer_snapshot = this
+                .read_with(&cx, |this, cx| {
+                    this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
+                })
+                .ok()?;
+
+            let mut base_selections = Vec::new();
+            let mut buffer_selection_map = HashMap::default();
+
+            {
+                let selections = this
+                    .read_with(&cx, |this, _| this.selections.disjoint_anchors().clone())
+                    .ok()?;
+                for selection in selections.iter() {
+                    let Some(selection_buffer_offset_head) =
+                        multi_buffer_snapshot.point_to_buffer_offset(selection.head())
+                    else {
+                        base_selections.push(selection.clone());
+                        continue;
+                    };
+                    let Some(selection_buffer_offset_tail) =
+                        multi_buffer_snapshot.point_to_buffer_offset(selection.tail())
+                    else {
+                        base_selections.push(selection.clone());
+                        continue;
+                    };
+
+                    let is_entirely_in_buffer = selection_buffer_offset_head.0.remote_id()
+                        == buffer_id
+                        && selection_buffer_offset_tail.0.remote_id() == buffer_id;
+                    if !is_entirely_in_buffer {
+                        base_selections.push(selection.clone());
+                        continue;
+                    }
+
+                    let selection_buffer_offset_head = selection_buffer_offset_head.1;
+                    let selection_buffer_offset_tail = selection_buffer_offset_tail.1;
+                    buffer_selection_map.insert(
+                        (selection_buffer_offset_head, selection_buffer_offset_tail),
+                        (selection.clone(), None),
+                    );
+                }
+            }
+
+            let mut any_selections_need_update = false;
+            for edit in &edits {
+                let edit_range_offset = edit.0.to_offset(&buffer_snapshot);
+                if edit_range_offset.start != edit_range_offset.end {
+                    continue;
+                }
+                if let Some(selection) =
+                    buffer_selection_map.get_mut(&(edit_range_offset.start, edit_range_offset.end))
+                {
+                    if selection.0.head().bias() != text::Bias::Right
+                        || selection.0.tail().bias() != text::Bias::Right
+                    {
+                        continue;
+                    }
+                    if selection.1.is_none() {
+                        any_selections_need_update = true;
+                        selection.1 = Some(
+                            selection
+                                .0
+                                .clone()
+                                .map(|anchor| multi_buffer_snapshot.anchor_before(anchor)),
+                        );
+                    }
+                }
+            }
+
+            buffer
+                .update(&mut cx, |buffer, cx| {
+                    buffer.edit(edits, None, cx);
+                })
+                .ok()?;
+
+            if any_selections_need_update {
+                let multi_buffer_snapshot = this
+                    .read_with(&cx, |this, cx| {
+                        this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
+                    })
+                    .ok()?;
+
+                base_selections.extend(buffer_selection_map.values().map(|selection| {
+                    match &selection.1 {
+                        Some(left_biased_selection) => left_biased_selection.clone(),
+                        None => selection.0.clone(),
+                    }
+                }));
+
+                let base_selections = base_selections
+                    .into_iter()
+                    .map(|selection| {
+                        selection.map(|anchor| anchor.to_offset(&multi_buffer_snapshot))
+                    })
+                    .collect::<Vec<_>>();
+                this.update_in(&mut cx, |this, window, cx| {
+                    this.change_selections_inner(None, false, window, cx, |s| {
+                        s.select(base_selections);
+                    });
+                })
+                .ok()?;
+            }
+
+            Some(())
+        })
+        .detach();
+    }
+}

crates/language/src/buffer.rs 🔗

@@ -3080,6 +3080,25 @@ impl BufferSnapshot {
             .last()
     }
 
+    pub fn smallest_syntax_layer_containing<D: ToOffset>(
+        &self,
+        range: Range<D>,
+    ) -> Option<SyntaxLayer> {
+        let range = range.to_offset(self);
+        return self
+            .syntax
+            .layers_for_range(range, &self.text, false)
+            .max_by(|a, b| {
+                if a.depth != b.depth {
+                    a.depth.cmp(&b.depth)
+                } else if a.offset.0 != b.offset.0 {
+                    a.offset.0.cmp(&b.offset.0)
+                } else {
+                    a.node().end_byte().cmp(&b.node().end_byte()).reverse()
+                }
+            });
+    }
+
     /// Returns the main [`Language`].
     pub fn language(&self) -> Option<&Arc<Language>> {
         self.language.as_ref()

crates/language/src/language.rs 🔗

@@ -680,6 +680,9 @@ pub struct LanguageConfig {
     /// languages, but should not appear to the user as a distinct language.
     #[serde(default)]
     pub hidden: bool,
+    /// If configured, this language contains JSX style tags, and should support auto-closing of those tags.
+    #[serde(default)]
+    pub jsx_tag_auto_close: Option<JsxTagAutoCloseConfig>,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@@ -697,6 +700,34 @@ pub struct LanguageMatcher {
     pub first_line_pattern: Option<Regex>,
 }
 
+/// The configuration for JSX tag auto-closing.
+#[derive(Clone, Deserialize, JsonSchema)]
+pub struct JsxTagAutoCloseConfig {
+    /// The name of the node for a opening tag
+    pub open_tag_node_name: String,
+    /// The name of the node for an closing tag
+    pub close_tag_node_name: String,
+    /// The name of the node for a complete element with children for open and close tags
+    pub jsx_element_node_name: String,
+    /// The name of the node found within both opening and closing
+    /// tags that describes the tag name
+    pub tag_name_node_name: String,
+    /// Some grammars are smart enough to detect a closing tag
+    /// that is not valid i.e. doesn't match it's corresponding
+    /// opening tag or does not have a corresponding opening tag
+    /// This should be set to the name of the node for invalid
+    /// closing tags if the grammar contains such a node, otherwise
+    /// detecting already closed tags will not work properly
+    #[serde(default)]
+    pub erroneous_close_tag_node_name: Option<String>,
+    /// See above for erroneous_close_tag_node_name for details
+    /// This should be set if the node used for the tag name
+    /// within erroneous closing tags is different from the
+    /// normal tag name node name
+    #[serde(default)]
+    pub erroneous_close_tag_name_node_name: Option<String>,
+}
+
 /// Represents a language for the given range. Some languages (e.g. HTML)
 /// interleave several languages together, thus a single buffer might actually contain
 /// several nested scopes.
@@ -767,6 +798,7 @@ impl Default for LanguageConfig {
             soft_wrap: None,
             prettier_parser_name: None,
             hidden: false,
+            jsx_tag_auto_close: None,
         }
     }
 }
@@ -888,7 +920,7 @@ pub struct BracketPair {
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub(crate) struct LanguageId(usize);
+pub struct LanguageId(usize);
 
 impl LanguageId {
     pub(crate) fn new() -> Self {
@@ -1056,6 +1088,10 @@ impl Language {
         Self::new_with_id(LanguageId::new(), config, ts_language)
     }
 
+    pub fn id(&self) -> LanguageId {
+        self.id
+    }
+
     fn new_with_id(
         id: LanguageId,
         config: LanguageConfig,

crates/language/src/language_settings.rs 🔗

@@ -100,6 +100,8 @@ pub struct LanguageSettings {
     pub formatter: SelectedFormatter,
     /// Zed's Prettier integration settings.
     pub prettier: PrettierSettings,
+    /// Whether to automatically close JSX tags.
+    pub jsx_tag_auto_close: JsxTagAutoCloseSettings,
     /// Whether to use language servers to provide code intelligence.
     pub enable_language_server: bool,
     /// The list of language servers to use (or disable) for this language.
@@ -374,6 +376,9 @@ pub struct LanguageSettingsContent {
     /// Default: off
     #[serde(default)]
     pub prettier: Option<PrettierSettings>,
+    /// Whether to automatically close JSX tags.
+    #[serde(default)]
+    pub jsx_tag_auto_close: Option<JsxTagAutoCloseSettings>,
     /// Whether to use language servers to provide code intelligence.
     ///
     /// Default: true
@@ -1335,6 +1340,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
     );
     merge(&mut settings.formatter, src.formatter.clone());
     merge(&mut settings.prettier, src.prettier.clone());
+    merge(
+        &mut settings.jsx_tag_auto_close,
+        src.jsx_tag_auto_close.clone(),
+    );
     merge(&mut settings.format_on_save, src.format_on_save.clone());
     merge(
         &mut settings.remove_trailing_whitespace_on_save,
@@ -1398,6 +1407,13 @@ pub struct PrettierSettings {
     pub options: HashMap<String, serde_json::Value>,
 }
 
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct JsxTagAutoCloseSettings {
+    /// Enables or disables auto-closing of JSX tags.
+    #[serde(default)]
+    pub enabled: bool,
+}
+
 #[cfg(test)]
 mod tests {
     use gpui::TestAppContext;

crates/language/src/syntax_map.rs 🔗

@@ -121,9 +121,9 @@ impl SyntaxLayerContent {
 pub struct SyntaxLayer<'a> {
     /// The language for this layer.
     pub language: &'a Arc<Language>,
-    depth: usize,
+    pub(crate) depth: usize,
     tree: &'a Tree,
-    offset: (usize, tree_sitter::Point),
+    pub(crate) offset: (usize, tree_sitter::Point),
 }
 
 /// A layer of syntax highlighting. Like [SyntaxLayer], but holding
@@ -133,7 +133,7 @@ pub struct OwnedSyntaxLayer {
     /// The language for this layer.
     pub language: Arc<Language>,
     tree: tree_sitter::Tree,
-    offset: (usize, tree_sitter::Point),
+    pub offset: (usize, tree_sitter::Point),
 }
 
 #[derive(Debug, Clone)]

crates/languages/src/javascript/config.toml 🔗

@@ -20,6 +20,12 @@ tab_size = 2
 scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
 prettier_parser_name = "babel"
 
+[jsx_tag_auto_close]
+open_tag_node_name = "jsx_opening_element"
+close_tag_node_name = "jsx_closing_element"
+jsx_element_node_name = "jsx_element"
+tag_name_node_name = "identifier"
+
 [overrides.element]
 line_comments = { remove = true }
 block_comment = ["{/* ", " */}"]

crates/languages/src/lib.rs 🔗

@@ -11,7 +11,7 @@ use std::{str, sync::Arc};
 use typescript::typescript_task_context;
 use util::{asset_str, ResultExt};
 
-use crate::{bash::bash_task_context, go::GoContextProvider, rust::RustContextProvider};
+use crate::{bash::bash_task_context, rust::RustContextProvider};
 
 mod bash;
 mod c;
@@ -74,73 +74,49 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
         ("gitcommit", tree_sitter_gitcommit::LANGUAGE),
     ]);
 
-    macro_rules! language {
-        ($name:literal) => {
-            let config = load_config($name);
-            languages.register_language(
-                config.name.clone(),
-                config.grammar.clone(),
-                config.matcher.clone(),
-                config.hidden,
-                Arc::new(move || {
-                    Ok(LoadedLanguage {
-                        config: config.clone(),
-                        queries: load_queries($name),
-                        context_provider: None,
-                        toolchain_provider: None,
-                    })
-                }),
-            );
+    // Following are a series of helper macros for registering languages.
+    // Macros are used instead of a function or for loop in order to avoid
+    // code duplication and improve readability as the types get quite verbose
+    // to type out in some cases.
+    // Additionally, the `provider` fields in LoadedLanguage
+    // would have be `Copy` if we were to use a function or for-loop to register the languages
+    // due to the fact that we pass an `Arc<Fn>` to `languages.register_language`
+    // that loads and initializes the language lazily.
+    // We avoid this entirely by using a Macro
+
+    macro_rules! context_provider {
+        ($name:expr) => {
+            Some(Arc::new($name) as Arc<dyn ContextProvider>)
         };
-        ($name:literal, $adapters:expr) => {
-            let config = load_config($name);
-            // typeck helper
-            let adapters: Vec<Arc<dyn LspAdapter>> = $adapters;
-            for adapter in adapters {
-                languages.register_lsp_adapter(config.name.clone(), adapter);
-            }
-            languages.register_language(
-                config.name.clone(),
-                config.grammar.clone(),
-                config.matcher.clone(),
-                config.hidden,
-                Arc::new(move || {
-                    Ok(LoadedLanguage {
-                        config: config.clone(),
-                        queries: load_queries($name),
-                        context_provider: None,
-                        toolchain_provider: None,
-                    })
-                }),
-            );
+        () => {
+            None
         };
-        ($name:literal, $adapters:expr, $context_provider:expr) => {
-            let config = load_config($name);
-            // typeck helper
-            let adapters: Vec<Arc<dyn LspAdapter>> = $adapters;
-            for adapter in adapters {
-                languages.register_lsp_adapter(config.name.clone(), adapter);
-            }
-            languages.register_language(
-                config.name.clone(),
-                config.grammar.clone(),
-                config.matcher.clone(),
-                config.hidden,
-                Arc::new(move || {
-                    Ok(LoadedLanguage {
-                        config: config.clone(),
-                        queries: load_queries($name),
-                        context_provider: Some(Arc::new($context_provider)),
-                        toolchain_provider: None,
-                    })
-                }),
-            );
+    }
+
+    macro_rules! toolchain_provider {
+        ($name:expr) => {
+            Some(Arc::new($name) as Arc<dyn ToolchainLister>)
         };
-        ($name:literal, $adapters:expr, $context_provider:expr, $toolchain_provider:expr) => {
+        () => {
+            None
+        };
+    }
+
+    macro_rules! adapters {
+        ($($item:expr),+ $(,)?) => {
+            vec![
+                $(Arc::new($item) as Arc<dyn LspAdapter>,)*
+            ]
+        };
+        () => {
+            vec![]
+        };
+    }
+
+    macro_rules! register_language {
+        ($name:expr, adapters => $adapters:expr, context => $context:expr, toolchain => $toolchain:expr) => {
             let config = load_config($name);
-            // typeck helper
-            let adapters: Vec<Arc<dyn LspAdapter>> = $adapters;
-            for adapter in adapters {
+            for adapter in $adapters {
                 languages.register_lsp_adapter(config.name.clone(), adapter);
             }
             languages.register_language(
@@ -152,99 +128,137 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
                     Ok(LoadedLanguage {
                         config: config.clone(),
                         queries: load_queries($name),
-                        context_provider: Some(Arc::new($context_provider)),
-                        toolchain_provider: Some($toolchain_provider),
+                        context_provider: $context,
+                        toolchain_provider: $toolchain,
                     })
                 }),
             );
         };
+        ($name:expr) => {
+            register_language!($name, adapters => adapters![], context => context_provider!(), toolchain => toolchain_provider!())
+        };
+        ($name:expr, adapters => $adapters:expr, context => $context:expr, toolchain => $toolchain:expr) => {
+            register_language!($name, adapters => $adapters, context => $context, toolchain => $toolchain)
+        };
+        ($name:expr, adapters => $adapters:expr, context => $context:expr) => {
+            register_language!($name, adapters => $adapters, context => $context, toolchain => toolchain_provider!())
+        };
+        ($name:expr, adapters => $adapters:expr) => {
+            register_language!($name, adapters => $adapters, context => context_provider!(), toolchain => toolchain_provider!())
+        };
     }
-    language!("bash", Vec::new(), bash_task_context());
-    language!("c", vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>]);
-    language!("cpp", vec![Arc::new(c::CLspAdapter)]);
-    language!(
+
+    register_language!(
+        "bash",
+        adapters => adapters![],
+        context => context_provider!(bash_task_context()),
+        toolchain => toolchain_provider!()
+    );
+
+    register_language!(
+        "c",
+        adapters => adapters![c::CLspAdapter]
+    );
+    register_language!(
+        "cpp",
+        adapters => adapters![c::CLspAdapter]
+    );
+
+    register_language!(
         "css",
-        vec![Arc::new(css::CssLspAdapter::new(node_runtime.clone())),]
+        adapters => adapters![css::CssLspAdapter::new(node_runtime.clone())]
+    );
+
+    register_language!("diff");
+
+    register_language!(
+        "go",
+        adapters => adapters![go::GoLspAdapter],
+        context => context_provider!(go::GoContextProvider)
+    );
+    register_language!(
+        "gomod",
+        adapters => adapters![go::GoLspAdapter],
+        context => context_provider!(go::GoContextProvider)
     );
-    language!("diff");
-    language!("go", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
-    language!("gomod", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
-    language!(
+    register_language!(
         "gowork",
-        vec![Arc::new(go::GoLspAdapter)],
-        GoContextProvider
+        adapters => adapters![go::GoLspAdapter],
+        context => context_provider!(go::GoContextProvider)
     );
 
-    language!(
+    register_language!(
         "json",
-        vec![
-            Arc::new(json::JsonLspAdapter::new(
-                node_runtime.clone(),
-                languages.clone(),
-            )),
-            Arc::new(json::NodeVersionAdapter)
+        adapters => adapters![
+            json::JsonLspAdapter::new(node_runtime.clone(), languages.clone(),),
+            json::NodeVersionAdapter,
         ],
-        json_task_context()
+        context => context_provider!(json_task_context())
     );
-    language!(
+    register_language!(
         "jsonc",
-        vec![Arc::new(json::JsonLspAdapter::new(
-            node_runtime.clone(),
-            languages.clone(),
-        ))],
-        json_task_context()
+        adapters => adapters![
+            json::JsonLspAdapter::new(node_runtime.clone(), languages.clone(),),
+        ],
+        context => context_provider!(json_task_context())
     );
-    language!("markdown");
-    language!("markdown-inline");
-    language!(
+
+    register_language!("markdown");
+    register_language!("markdown-inline");
+
+    register_language!(
         "python",
-        vec![
-            Arc::new(python::PythonLspAdapter::new(node_runtime.clone(),)),
-            Arc::new(python::PyLspAdapter::new())
+        adapters => adapters![
+            python::PythonLspAdapter::new(node_runtime.clone()),
+            python::PyLspAdapter::new()
         ],
-        PythonContextProvider,
-        Arc::new(PythonToolchainProvider::default()) as Arc<dyn ToolchainLister>
+        context => context_provider!(PythonContextProvider),
+        toolchain => toolchain_provider!(PythonToolchainProvider::default())
     );
-    language!(
+    register_language!(
         "rust",
-        vec![Arc::new(rust::RustLspAdapter)],
-        RustContextProvider
+        adapters => adapters![rust::RustLspAdapter],
+        context => context_provider!(RustContextProvider)
     );
-    language!(
+    register_language!(
         "tsx",
-        vec![
-            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
-            Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
+        adapters => adapters![
+            typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
+            vtsls::VtslsLspAdapter::new(node_runtime.clone()),
         ],
-        typescript_task_context()
+        context => context_provider!(typescript_task_context()),
+        toolchain => toolchain_provider!()
     );
-    language!(
+    register_language!(
         "typescript",
-        vec![
-            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
-            Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
+        adapters => adapters![
+            typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
+            vtsls::VtslsLspAdapter::new(node_runtime.clone()),
         ],
-        typescript_task_context()
+        context => context_provider!(typescript_task_context())
     );
-    language!(
+    register_language!(
         "javascript",
-        vec![
-            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
-            Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
+        adapters => adapters![
+            typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
+            vtsls::VtslsLspAdapter::new(node_runtime.clone()),
         ],
-        typescript_task_context()
+        context => context_provider!(typescript_task_context())
     );
-    language!(
+    register_language!(
         "jsdoc",
-        vec![
-            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone(),)),
-            Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
+        adapters => adapters![
+            typescript::TypeScriptLspAdapter::new(node_runtime.clone()),
+            vtsls::VtslsLspAdapter::new(node_runtime.clone()),
         ]
     );
-    language!("regex");
-    language!(
-        "yaml",
-        vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]
+
+    register_language!("regex");
+
+    register_language!("yaml",
+        adapters => adapters![
+            yaml::YamlLspAdapter::new(node_runtime.clone()),
+        ]
     );
 
     // Register globally available language servers.
@@ -366,6 +380,7 @@ fn load_config(name: &str) -> LanguageConfig {
         config = LanguageConfig {
             name: config.name,
             matcher: config.matcher,
+            jsx_tag_auto_close: config.jsx_tag_auto_close,
             ..Default::default()
         }
     }

crates/languages/src/tsx/config.toml 🔗

@@ -18,6 +18,12 @@ scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-
 prettier_parser_name = "typescript"
 tab_size = 2
 
+[jsx_tag_auto_close]
+open_tag_node_name = "jsx_opening_element"
+close_tag_node_name = "jsx_closing_element"
+jsx_element_node_name = "jsx_element"
+tag_name_node_name = "identifier"
+
 [overrides.element]
 line_comments = { remove = true }
 block_comment = ["{/* ", " */}"]

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -1071,6 +1071,11 @@ impl MultiBuffer {
         self.history.start_transaction(now)
     }
 
+    pub fn last_transaction_id(&self) -> Option<TransactionId> {
+        let last_transaction = self.history.undo_stack.last()?;
+        return Some(last_transaction.id);
+    }
+
     pub fn end_transaction(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
         self.end_transaction_at(Instant::now(), cx)
     }

crates/rope/src/rope.rs 🔗

@@ -591,6 +591,7 @@ impl<'a> Cursor<'a> {
     }
 }
 
+#[derive(Clone)]
 pub struct Chunks<'a> {
     chunks: sum_tree::Cursor<'a, Chunk, usize>,
     range: Range<usize>,
@@ -780,6 +781,40 @@ impl<'a> Chunks<'a> {
             reversed,
         }
     }
+
+    pub fn equals_str(&self, other: &str) -> bool {
+        let chunk = self.clone();
+        if chunk.reversed {
+            let mut offset = other.len();
+            for chunk in chunk {
+                if other[0..offset].ends_with(chunk) {
+                    offset -= chunk.len();
+                } else {
+                    return false;
+                }
+            }
+            if offset != 0 {
+                return false;
+            }
+        } else {
+            let mut offset = 0;
+            for chunk in chunk {
+                if offset >= other.len() {
+                    return false;
+                }
+                if other[offset..].starts_with(chunk) {
+                    offset += chunk.len();
+                } else {
+                    return false;
+                }
+            }
+            if offset != other.len() {
+                return false;
+            }
+        }
+
+        return true;
+    }
 }
 
 impl<'a> Iterator for Chunks<'a> {
@@ -1855,6 +1890,53 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_chunks_equals_str() {
+        let text = "This is a multi-chunk\n& multi-line test string!";
+        let rope = Rope::from(text);
+        for start in 0..text.len() {
+            for end in start..text.len() {
+                let range = start..end;
+                let correct_substring = &text[start..end];
+
+                // Test that correct range returns true
+                assert!(rope
+                    .chunks_in_range(range.clone())
+                    .equals_str(correct_substring));
+                assert!(rope
+                    .reversed_chunks_in_range(range.clone())
+                    .equals_str(correct_substring));
+
+                // Test that all other ranges return false (unless they happen to match)
+                for other_start in 0..text.len() {
+                    for other_end in other_start..text.len() {
+                        if other_start == start && other_end == end {
+                            continue;
+                        }
+                        let other_substring = &text[other_start..other_end];
+
+                        // Only assert false if the substrings are actually different
+                        if other_substring == correct_substring {
+                            continue;
+                        }
+                        assert!(!rope
+                            .chunks_in_range(range.clone())
+                            .equals_str(other_substring));
+                        assert!(!rope
+                            .reversed_chunks_in_range(range.clone())
+                            .equals_str(other_substring));
+                    }
+                }
+            }
+        }
+
+        let rope = Rope::from("");
+        assert!(rope.chunks_in_range(0..0).equals_str(""));
+        assert!(rope.reversed_chunks_in_range(0..0).equals_str(""));
+        assert!(!rope.chunks_in_range(0..0).equals_str("foo"));
+        assert!(!rope.reversed_chunks_in_range(0..0).equals_str("foo"));
+    }
+
     fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize {
         while !text.is_char_boundary(offset) {
             match bias {