diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 58ae55044d..ca7921b87a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -18034,245 +18034,6 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex }); } -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, - "" => "
ˇ
" - ); - - check!( - test_basic_nested, - "
" + ">" => "
ˇ
" - ); - - check!( - test_basic_ignore_already_closed, - "
" + ">" => "
ˇ
" - ); - - check!( - test_doesnt_autoclose_closing_tag, - "" => "
ˇ" - ); - - check!( - test_jsx_attr, - "
}ˇ" + ">" => "
}>ˇ
" - ); - - check!( - test_ignores_closing_tags_in_expr_block, - "
}
" + ">" => "
ˇ
{
}
" - ); - - check!( - test_doesnt_autoclose_on_gt_in_expr, - "
" => "
ˇ" - ); - - check!( - test_ignores_closing_tags_with_different_tag_names, - "
" + ">" => "
ˇ
" - ); - - check!( - test_autocloses_in_jsx_expression, - "
{" + ">" => "
{
ˇ
}
" - ); - - check!( - test_doesnt_autoclose_already_closed_in_jsx_expression, - "
{}
" + ">" => "
{
ˇ
}
" - ); - - check!( - test_autocloses_fragment, - "<ˇ" + ">" => "<>ˇ" - ); - - check!( - test_does_not_include_type_argument_in_autoclose_tag_name, - " attr={boolean_value}ˇ" + ">" => " attr={boolean_value}>ˇ" - ); - - check!( - test_does_not_autoclose_doctype, - "" => "ˇ" - ); - - check!( - test_does_not_autoclose_comment, - "ˇ" - ); - - check!( - test_multi_cursor_autoclose_same_tag, - r#" - " => - r#" -
ˇ
-
ˇ
- "# - ); - - check!( - test_multi_cursor_autoclose_different_tags, - r#" - " => - r#" -
ˇ
- ˇ - "# - ); - - check!( - test_multi_cursor_autoclose_some_dont_autoclose_others, - r#" - - ˇ - ˇ - "# - + ">" => - r#" -
ˇ
-
ˇ - ˇ - ˇ - ˇ - >ˇ - >ˇ - "# - ); - - check!( - test_doesnt_mess_up_trailing_text, - "" => "
ˇ
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("", window, cx); - }); - cx.run_until_parked(); - - cx.assert_editor_state("
ˇ
\n
ˇ
\nˇ"); - } -} - fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index d41f70f18e..9e932b5b89 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -114,7 +114,13 @@ pub(crate) fn generate_auto_close_edits( 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) + .filter(|node| { + node.kind() == config.tag_name_node_name + || config + .tag_name_node_name_alternates + .iter() + .any(|alternate| alternate == node.kind()) + }) .map_or("".to_string(), |node| { buffer.text_for_range(node.byte_range()).collect::() }); @@ -174,12 +180,9 @@ pub(crate) fn generate_auto_close_edits( * 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 tag_node_name_equals = |node: &Node, 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); } @@ -225,11 +228,7 @@ pub(crate) fn generate_auto_close_edits( .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, - ) + tag_node_name_equals(&element_open_tag_node, &tag_name) }); if has_open_tag_with_same_tag_name { doing_deep_search = true; @@ -258,14 +257,9 @@ pub(crate) fn generate_auto_close_edits( 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| { @@ -281,15 +275,15 @@ pub(crate) fn generate_auto_close_edits( 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) { + if tag_node_name_equals(&node, &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) { + if tag_node_name_equals(&node, &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 tag_node_name_equals(&node, &tag_name) { if !is_after_open_tag(&node) { unclosed_open_tag_count -= 1; } @@ -614,3 +608,256 @@ pub(crate) fn handle_from( .detach(); } } + +#[cfg(test)] +mod jsx_tag_autoclose_tests { + use crate::{ + editor_tests::init_test, + test::{build_editor, editor_test_context::EditorTestContext}, + }; + + use super::*; + use gpui::{AppContext as _, TestAppContext}; + use language::language_settings::JsxTagAutoCloseSettings; + use languages::language; + use multi_buffer::ExcerptRange; + use text::Selection; + + 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, + "" => "
ˇ
" + ); + + check!( + test_basic_nested, + "
" + ">" => "
ˇ
" + ); + + check!( + test_basic_ignore_already_closed, + "
" + ">" => "
ˇ
" + ); + + check!( + test_doesnt_autoclose_closing_tag, + "" => "
ˇ" + ); + + check!( + test_jsx_attr, + "
}ˇ" + ">" => "
}>ˇ
" + ); + + check!( + test_ignores_closing_tags_in_expr_block, + "
}
" + ">" => "
ˇ
{
}
" + ); + + check!( + test_doesnt_autoclose_on_gt_in_expr, + "
" => "
ˇ" + ); + + check!( + test_ignores_closing_tags_with_different_tag_names, + "
" + ">" => "
ˇ
" + ); + + check!( + test_autocloses_in_jsx_expression, + "
{" + ">" => "
{
ˇ
}
" + ); + + check!( + test_doesnt_autoclose_already_closed_in_jsx_expression, + "
{}
" + ">" => "
{
ˇ
}
" + ); + + check!( + test_autocloses_fragment, + "<ˇ" + ">" => "<>ˇ" + ); + + check!( + test_does_not_include_type_argument_in_autoclose_tag_name, + " attr={boolean_value}ˇ" + ">" => " attr={boolean_value}>ˇ" + ); + + check!( + test_does_not_autoclose_doctype, + "" => "ˇ" + ); + + check!( + test_does_not_autoclose_comment, + "ˇ" + ); + + check!( + test_autocloses_dot_separated_component, + "" => "ˇ" + ); + + check!( + test_multi_cursor_autoclose_same_tag, + r#" + " => + r#" +
ˇ
+
ˇ
+ "# + ); + + check!( + test_multi_cursor_autoclose_different_tags, + r#" + " => + r#" +
ˇ
+ ˇ + "# + ); + + check!( + test_multi_cursor_autoclose_some_dont_autoclose_others, + r#" + + ˇ + ˇ + "# + + ">" => + r#" +
ˇ
+
ˇ + ˇ + ˇ + ˇ + >ˇ + >ˇ + "# + ); + + check!( + test_doesnt_mess_up_trailing_text, + "" => "
ˇ
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("", window, cx); + }); + cx.run_until_parked(); + + cx.assert_editor_state("
ˇ
\n
ˇ
\nˇ"); + } +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 3af382edea..bda5db426e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -727,6 +727,11 @@ pub struct JsxTagAutoCloseConfig { /// The name of the node found within both opening and closing /// tags that describes the tag name pub tag_name_node_name: String, + /// Alternate Node names for tag names. + /// Specifically needed as TSX represents the name in `` + /// as `member_expression` rather than `identifier` as usual + #[serde(default)] + pub tag_name_node_name_alternates: Vec, /// 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 diff --git a/crates/languages/src/tsx/config.toml b/crates/languages/src/tsx/config.toml index 1fbe56b055..6c0fdbbd5a 100644 --- a/crates/languages/src/tsx/config.toml +++ b/crates/languages/src/tsx/config.toml @@ -23,6 +23,7 @@ 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" +tag_name_node_name_alternates = ["member_expression"] [overrides.element] line_comments = { remove = true }