Fix block-comment prefix stripping when joining lines (#63916)

Closes
https://github.com/zed-industries/zed/issues/63273#issuecomment-5580302020



https://github.com/user-attachments/assets/0945d43d-9a19-4af6-bc85-c3369d8babb3


Release Notes:

- Fixed block-comment prefix stripping when joining lines
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-09-08 15:16:55 +00:00
1 parent e2534d2357
commit 20d3cd1d79
3 files changed
+185 -26

No files matched your search

+10 -9
View File
@@ -5790,20 +5790,21 @@ impl Editor {
.collect::<String>();
if !line_text_after_indent.is_empty() {
let block_prefix = language_scope
let block_prefixes = language_scope
.block_comment()
.map(|c| c.prefix.as_ref())
.filter(|p| !p.is_empty());
let doc_prefix = language_scope
.documentation_comment()
.map(|c| c.prefix.as_ref())
.filter(|p| !p.is_empty());
.into_iter()
.chain(language_scope.documentation_comment())
.filter(|comment| {
language_scope.override_name() == Some("comment")
&& !comment.prefix.is_empty()
&& !line_text_after_indent.starts_with(comment.end.as_ref())
})
.map(|comment| comment.prefix.as_ref());
let comment_prefixes = language_scope
.line_comment_prefixes()
.iter()
.map(|p| p.as_ref())
.chain(block_prefix)
.chain(doc_prefix)
.chain(block_prefixes)
.map(|prefix| (prefix, false));
let all_prefixes = comment_prefixes.chain(
language_scope
+96 -16
View File
@@ -23,7 +23,7 @@ use gpui::{
BackgroundExecutor, DismissEvent, Task, TaskExt, TestAppContext, UpdateGlobal,
VisualTestContext, WindowBounds, WindowOptions, div,
};
use indoc::indoc;
use indoc::{formatdoc, indoc};
use language::{
BracketPair, BracketPairConfig,
Capability::{Read, ReadOnly, ReadWrite},
@@ -7038,22 +7038,8 @@ async fn test_join_lines_strips_comment_prefix(cx: &mut TestAppContext) {
init_test(cx, |_| {});
{
let language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["// ".into(), "/// ".into()],
documentation_comment: Some(BlockCommentConfig {
start: "/*".into(),
end: "*/".into(),
prefix: "* ".into(),
tab_size: 1,
}),
..LanguageConfig::default()
},
None,
));
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
// Strips the comment prefix (with trailing space) from the joined-in line.
cx.set_state(indoc! {"
@@ -7117,22 +7103,30 @@ async fn test_join_lines_strips_comment_prefix(cx: &mut TestAppContext) {
// Strips block comment body prefix (`* `) from the joined-in line.
cx.set_state(indoc! {"
/*
* ˇfoo
* bar
*/
"});
cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
cx.assert_editor_state(indoc! {"
/*
* fooˇ bar
*/
"});
// Strips bare block comment body prefix (`*` without trailing space).
cx.set_state(indoc! {"
/*
* ˇfoo
*
*/
"});
cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx));
cx.assert_editor_state(indoc! {"
/*
* fooˇ
*/
"});
}
@@ -7208,6 +7202,92 @@ async fn test_join_lines_strips_comment_prefix(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_join_lines_preserves_rust_operators(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
for insert_whitespace in [true, false] {
let separator = if insert_whitespace { " " } else { "" };
for indent in ["", " ", "\t"] {
for (first_line, next_line) in [
("let value =", "*pointer;"),
("let value =", "* pointer;"),
("let value =", "**pointer;"),
("let value =", "*指针;"),
("let value = 2", "* 3;"),
("let value = \"text", "*text\";"),
("let value = r#\"text", "*text\"#;"),
] {
cx.set_state(&formatdoc! {"
fn main() {{
{first_line}ˇ
{indent}{next_line}
}}"});
cx.update_editor(|editor, window, cx| {
editor.join_lines_impl(insert_whitespace, window, cx)
});
cx.assert_editor_state(&formatdoc! {"
fn main() {{
{first_line}ˇ{separator}{next_line}
}}"});
}
}
}
}
#[gpui::test]
async fn test_join_lines_rust_block_comments(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
for insert_whitespace in [true, false] {
let separator = if insert_whitespace { " " } else { "" };
for start in ["/*", "/**", "/*!"] {
for next_line in ["* bar", "*bar"] {
cx.set_state(&formatdoc! {"
{start}
* fooˇ
{next_line}
*/"});
cx.update_editor(|editor, window, cx| {
editor.join_lines_impl(insert_whitespace, window, cx)
});
cx.assert_editor_state(&formatdoc! {"
{start}
* fooˇ{separator}bar
*/"});
}
cx.set_state(&formatdoc! {"
{start}
* fooˇ
*
*/"});
cx.update_editor(|editor, window, cx| {
editor.join_lines_impl(insert_whitespace, window, cx)
});
cx.assert_editor_state(&formatdoc! {"
{start}
* fooˇ
*/"});
cx.set_state(&formatdoc! {"
{start}
* fooˇ
*/"});
cx.update_editor(|editor, window, cx| {
editor.join_lines_impl(insert_whitespace, window, cx)
});
cx.assert_editor_state(&formatdoc! {"
{start}
* fooˇ{separator}*/"});
}
}
}
#[gpui::test]
async fn test_custom_newlines_cause_no_false_positive_diffs(
executor: BackgroundExecutor,
+79 -1
View File
@@ -26,7 +26,7 @@ use util::{path, test::marked_text_ranges};
pub use vim_test_context::*;
use gpui::VisualTestContext;
use indoc::indoc;
use indoc::{formatdoc, indoc};
use project::FakeFs;
use search::BufferSearchBar;
use search::{ProjectSearchView, project_search};
@@ -621,6 +621,84 @@ async fn test_join_lines(cx: &mut gpui::TestAppContext) {
"});
}
#[perf]
#[gpui::test]
async fn test_join_lines_rust_dereference(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
for dereference in ["*value.get()", "* value.get()"] {
let initial = formatdoc! {"
ˇlet another_value = unsafe {{
{dereference}
}};"};
let joined = formatdoc! {"
let another_value = unsafe {{ˇ {dereference}
}};"};
let fully_joined = format!("let another_value = unsafe {{ {dereference}ˇ }};");
for keystrokes in [
"shift-j",
"1 shift-j",
"2 shift-j",
"v j shift-j",
"shift-v j shift-j",
"j v k shift-j",
"j shift-v k shift-j",
] {
cx.assert_binding_normal(keystrokes, &initial, &joined);
}
for keystrokes in [
"3 shift-j",
"v 2 j shift-j",
"shift-v 2 j shift-j",
"2 j v 2 k shift-j",
"2 j shift-v 2 k shift-j",
] {
cx.assert_binding_normal(keystrokes, &initial, &fully_joined);
}
}
}
#[perf]
#[gpui::test]
async fn test_join_lines_rust_dereference_without_whitespace(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
for dereference in ["*value.get()", "* value.get()"] {
let initial = formatdoc! {"
ˇlet another_value = unsafe {{
{dereference}
}};"};
let joined = formatdoc! {"
let another_value = unsafe {{ˇ{dereference}
}};"};
let fully_joined = format!("let another_value = unsafe {{{dereference}ˇ}};");
for keystrokes in [
"g shift-j",
"1 g shift-j",
"2 g shift-j",
"v j g shift-j",
"shift-v j g shift-j",
"j v k g shift-j",
"j shift-v k g shift-j",
] {
cx.assert_binding_normal(keystrokes, &initial, &joined);
}
for keystrokes in [
"3 g shift-j",
"v 2 j g shift-j",
"shift-v 2 j g shift-j",
"2 j v 2 k g shift-j",
"2 j shift-v 2 k g shift-j",
] {
cx.assert_binding_normal(keystrokes, &initial, &fully_joined);
}
}
}
#[cfg(target_os = "macos")]
#[perf]
#[gpui::test]