jsx_tag_auto_close: Fix <Foo.Bar> component auto-close (#27374)

- **support alternate tag name node names to fix autoclosing of
`<Foo.Bar>` style tags in TSX**
- **remove checks against close tag name while checking if tag is
closed**
- **move jsx tag auto close tests into jsx_tag_auto_close.rs**

Closes #27335

Release Notes:

- Fixed an issue with JSX tag auto-close where components containing a
`.` access like `<Foo.Bar>` would be auto-closed as `</>` instead of
`</Foo.Bar>`
This commit is contained in:
Ben Kunkle 2025-03-24 10:43:06 -05:00 committed by GitHub
parent 43712285bf
commit f2be201495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 271 additions and 257 deletions

View file

@ -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,
"<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