This commit is contained in:
Jordan Pittman 2025-08-26 16:02:01 -04:00 committed by GitHub
commit 4407db38b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 235 additions and 1 deletions

View file

@ -753,6 +753,8 @@ actions!(
UniqueLinesCaseInsensitive,
/// Removes duplicate lines (case-sensitive).
UniqueLinesCaseSensitive,
UnwrapSyntaxNode
UnwrapSyntaxNode,
/// Wraps in an HTML tag.
WrapInTag
]
);

View file

@ -10447,6 +10447,132 @@ impl Editor {
})
}
pub fn supports_wrap_in_tag(&self, cx: &App) -> bool {
let Some((_, buffer, _)) = self.active_excerpt(cx) else {
return false;
};
let Some(language) = buffer.read(cx).language() else {
return false;
};
let tag_languages = vec![
"astro",
"html",
"javascript",
"typescript",
"svelte",
"tsx",
"vue.js",
];
let lsp_id = language.lsp_id();
return tag_languages.into_iter().any(|s| s == lsp_id);
}
pub fn wrap_in_tag(&mut self, _: &WrapInTag, window: &mut Window, cx: &mut Context<Self>) {
if !self.supports_wrap_in_tag(cx) {
return;
}
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
let buffer = self.buffer.read(cx).snapshot(cx);
let mut edits = Vec::new();
let selections = self.selections.all::<Point>(cx);
let mut selections = selections.iter();
let mut new_selections = Vec::new();
let mut selection_id = 0;
let mut last_row = 0;
let mut col_offset = 0;
while let Some(selection) = selections.next() {
let mut start_point = selection.start;
let mut end_point = selection.end;
let mut text = buffer
.text_for_range(start_point..end_point)
.collect::<String>();
text.insert_str(0, "<>");
text.push_str("</>");
edits.push((start_point..end_point, text));
// When this selection is on a different row than the previous one
// there's no need to offset subsequent selections
if end_point.row != last_row {
col_offset = 0;
}
start_point.column += col_offset;
end_point.column += col_offset;
// Put cursor into the opening tag
start_point.column += 1;
col_offset += 1;
new_selections.push(Selection {
id: selection_id,
start: start_point,
end: start_point,
goal: SelectionGoal::None,
reversed: false,
});
selection_id += 1;
// When start and end are on the same row then the column
// needs to be offset by two additional chars.
if start_point.row == end_point.row {
end_point.column += 2;
col_offset += 2;
}
// Put another cursor into the closing tag
end_point.column += 2;
col_offset += 2;
new_selections.push(Selection {
id: selection_id,
start: end_point,
end: end_point,
goal: SelectionGoal::None,
reversed: false,
});
selection_id += 1;
last_row = end_point.row;
}
self.transact(window, cx, |this, window, cx| {
let buffer = this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
buffer.snapshot(cx)
});
// Recalculate offsets on newly edited buffer
let new_selections = new_selections
.iter()
.map(|s| Selection {
id: s.id,
start: buffer.point_to_offset(s.start),
end: buffer.point_to_offset(s.end),
goal: s.goal,
reversed: s.reversed,
})
.collect();
this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
});
this.request_autoscroll(Autoscroll::fit(), cx);
});
}
pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context<Self>) {
let Some(project) = self.project.clone() else {
return;

View file

@ -4403,6 +4403,109 @@ async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let js_language = Arc::new(Language::new(
LanguageConfig {
name: "JavaScript".into(),
..LanguageConfig::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
cx.set_state(indoc! {"
«testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_in_tag(&WrapInTag, window, cx));
cx.assert_editor_state(indoc! {"
<«ˇ»>test</«ˇ»>
"});
cx.set_state(indoc! {"
«test
testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_in_tag(&WrapInTag, window, cx));
cx.assert_editor_state(indoc! {"
<«ˇ»>test
test</«ˇ»>
"});
}
#[gpui::test]
async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let js_language = Arc::new(Language::new(
LanguageConfig {
name: "JavaScript".into(),
..LanguageConfig::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
cx.set_state(indoc! {"
«testˇ»
«testˇ» «testˇ»
«testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_in_tag(&WrapInTag, window, cx));
cx.assert_editor_state(indoc! {"
<«ˇ»>test</«ˇ»>
<«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
<«ˇ»>test</«ˇ»>
"});
cx.set_state(indoc! {"
«test
testˇ»
«test
testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_in_tag(&WrapInTag, window, cx));
cx.assert_editor_state(indoc! {"
<«ˇ»>test
test</«ˇ»>
<«ˇ»>test
test</«ˇ»>
"});
}
#[gpui::test]
async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let plaintext_language = Arc::new(Language::new(
LanguageConfig {
name: "Plain Text".into(),
..LanguageConfig::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
cx.set_state(indoc! {"
«testˇ»
"});
cx.update_editor(|e, window, cx| e.wrap_in_tag(&WrapInTag, window, cx));
cx.assert_editor_state(indoc! {"
«testˇ»
"});
}
#[gpui::test]
async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View file

@ -585,6 +585,9 @@ impl EditorElement {
register_action(editor, window, Editor::edit_log_breakpoint);
register_action(editor, window, Editor::enable_breakpoint);
register_action(editor, window, Editor::disable_breakpoint);
if editor.read(cx).supports_wrap_in_tag(cx) {
register_action(editor, window, Editor::wrap_in_tag);
}
}
fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {