Improve auto indentation in situations with multiline comments present (#61669)

## Objective

Fix incorrect indentation after multiline comments and when pasting
multiline code immediately after a comment.

Fixes #61668 
Fixes #55597 

## Solution

- Calculate logical indentation from the opening line of block comments,
documentation comments, and multiline strings.
- Reuse the logical indentation for both newline handling and multiline
paste auto-indent.
- Preserve the relative indentation of pasted multiline content while
using the logical indentation of the surrounding code as its basis.

## Testing

- `cargo test -p editor --lib test_newline -- --nocapture`
- `cargo test -p editor --lib test_paste -- --nocapture`
- `cargo test -p language --lib test_autoindent -- --nocapture`
- `./script/clippy -p language`
- `./script/clippy -p editor`
- Regression coverage includes Go, TypeScript, and Python comment/string
indentation, plus Go multiline paste after a documentation comment.
- No platform-specific behavior is involved; the fix is implemented in
shared editor and language auto-indent logic.

The demonstration video is as follows:


https://github.com/user-attachments/assets/2a2ee494-d4ea-4b16-8644-f868b9f31272


https://github.com/user-attachments/assets/9487f893-e584-448d-a1c7-a94c76c03f5b


## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed incorrect indentation after multiline comments and multiline
paste.
This commit is contained in:
Yifeng Xu authored and GitHub committed 2026-08-26 14:33:25 +00:00
1 parent e9b18cd2d5
commit 918e494e61
3 files changed
+268 -5

No files matched your search

+164 -2
View File
@@ -5712,9 +5712,13 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
/**
*
*/
ˇ
ˇ
"});
cx.set_state("fn test() {\n /**\n *\n */ˇ\n}");
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state("fn test() {\n /**\n *\n */\n ˇ\n}");
// Ensure that inline comment followed by code
// doesn't add comment prefix on newline
cx.set_state(indoc! {"
@@ -5738,7 +5742,7 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
/**
*
*/
ˇtext
ˇtext
"});
// Ensure if not comment block it doesn't
@@ -5767,6 +5771,71 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_newline_closing_comment_indent_across_languages(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let go_language = languages::language("go", tree_sitter_go::LANGUAGE.into());
cx.update_buffer(|buffer, cx| buffer.set_language(Some(go_language), cx));
cx.set_state("func test() {\n\t/**\n\t * doc\n\t */ˇ\n}");
cx.update_editor(|editor, window, cx| editor.newline(&Newline, window, cx));
cx.assert_editor_state("func test() {\n\t/**\n\t * doc\n\t */\n\tˇ\n}");
let typescript_language = languages::language(
"typescript",
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
);
cx.update_buffer(|buffer, cx| buffer.set_language(Some(typescript_language), cx));
cx.set_state("function test() {\n /**\n * doc\n */ˇ\n}");
cx.update_editor(|editor, window, cx| editor.newline(&Newline, window, cx));
cx.assert_editor_state("function test() {\n /**\n * doc\n */\n ˇ\n}");
let python_language = languages::language("python", tree_sitter_python::LANGUAGE.into());
cx.update_buffer(|buffer, cx| buffer.set_language(Some(python_language), cx));
cx.set_state("def test():\n \"\"\"doc\n \"\"\"ˇ\n");
cx.update_editor(|editor, window, cx| editor.newline(&Newline, window, cx));
cx.assert_editor_state("def test():\n \"\"\"doc\n \"\"\"\n ˇ\n");
}
#[gpui::test]
async fn test_newline_after_closing_delimiter_lookalike_in_string(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language(
Some(languages::language("go", tree_sitter_go::LANGUAGE.into())),
cx,
)
});
// A line inside a string literal that happens to look like a block comment's
// closing delimiter does not close anything, so it keeps its own indentation.
cx.set_state("func test() {\n\tvar s = `\n*/ˇ\n`\n}");
cx.update_editor(|editor, window, cx| editor.newline(&Newline, window, cx));
cx.assert_editor_state("func test() {\n\tvar s = `\n*/\nˇ\n`\n}");
}
#[gpui::test]
async fn test_newline_twice_inside_block(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language(
Some(languages::language("go", tree_sitter_go::LANGUAGE.into())),
cx,
)
});
cx.set_state("func test() {ˇ}");
cx.update_editor(|editor, window, cx| editor.newline(&Newline, window, cx));
cx.assert_editor_state("func test() {\n ˇ\n}");
// The blank line the cursor sits on holds nothing but auto-indent
// whitespace, but the line that replaces it still belongs inside the block.
cx.update_editor(|editor, window, cx| editor.newline(&Newline, window, cx));
cx.assert_editor_state("func test() {\n\n ˇ\n}");
}
#[gpui::test]
async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
init_test(cx, |settings| {
@@ -6092,6 +6161,24 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
"});
}
#[gpui::test]
async fn test_tab_after_closing_comment_delimiter(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language(
Some(languages::language("go", tree_sitter_go::LANGUAGE.into())),
cx,
)
});
// The suggested indent is taken from the line the comment opened on, so that
// it agrees with what pressing enter after the closing delimiter produces.
cx.set_state("func test() {\n\t/**\n\t * doc\n\t */\nˇ\n}");
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
cx.assert_editor_state("func test() {\n\t/**\n\t * doc\n\t */\n\tˇ\n}");
}
#[gpui::test]
async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
init_test(cx, |settings| {
@@ -10821,6 +10908,81 @@ async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_paste_after_closing_documentation_comment(cx: &mut TestAppContext) {
init_test(cx, |_| {});
cx.write_to_clipboard(ClipboardItem::new_string(
"func find() {\n\treturn 1\n}\n".into(),
));
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language(
Some(languages::language("go", tree_sitter_go::LANGUAGE.into())),
cx,
)
});
cx.set_state("package test\n\n/**\n * test\n */ˇ");
cx.update_editor(|editor, window, cx| editor.newline(&Newline, window, cx));
cx.update_editor(|editor, window, cx| editor.paste(&Paste, window, cx));
cx.assert_editor_state("package test\n\n/**\n * test\n */\nfunc find() {\n\treturn 1\n}\nˇ");
cx.write_to_clipboard(ClipboardItem::new_string(
" func find() {\n \treturn 1\n }".into(),
));
cx.set_state("package test\n\n/**\n * test\n */ˇ");
cx.update_editor(|editor, window, cx| editor.newline(&Newline, window, cx));
cx.update_editor(|editor, window, cx| editor.paste(&Paste, window, cx));
cx.assert_editor_state("package test\n\n/**\n * test\n */\nfunc find() {\n\treturn 1\n}ˇ");
}
#[gpui::test]
async fn test_paste_shifts_block_by_first_line_delta(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language(
Some(languages::language("go", tree_sitter_go::LANGUAGE.into())),
cx,
)
});
cx.write_to_clipboard(ClipboardItem::new_string(
" func find() {\n \treturn 1\n }".into(),
));
cx.set_state("package test\n\nˇ");
cx.update_editor(|editor, window, cx| editor.paste(&Paste, window, cx));
cx.assert_editor_state("package test\n\nfunc find() {\n\treturn 1\n}ˇ");
// The block moves by however far its first line moved, so a first line that
// is indented deeper than the lines below it stays deeper.
cx.write_to_clipboard(ClipboardItem::new_string(" foo()\n bar()".into()));
cx.set_state("func test() {\nˇ\n}");
cx.update_editor(|editor, window, cx| editor.paste(&Paste, window, cx));
cx.assert_editor_state("func test() {\n foo()\nbar()ˇ\n}");
}
#[gpui::test]
async fn test_paste_after_closing_comment_delimiter(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language(
Some(languages::language("go", tree_sitter_go::LANGUAGE.into())),
cx,
)
});
// The closing delimiter's line is not part of the paste, so it keeps its own
// indentation even though that indentation differs from the opening line's.
cx.write_to_clipboard(ClipboardItem::new_string("x := 1\ny := 2".into()));
cx.set_state("package test\n\n/*\n * test\n */ˇ");
cx.update_editor(|editor, window, cx| editor.paste(&Paste, window, cx));
cx.assert_editor_state("package test\n\n/*\n * test\n */x := 1\n y := 2ˇ");
}
#[gpui::test]
async fn test_paste_multiline_from_other_app_into_matching_cursors(cx: &mut TestAppContext) {
init_test(cx, |_| {});
+24
View File
@@ -558,6 +558,8 @@ impl Editor {
let end = selection.end;
let selection_is_empty = start == end;
let language_scope = buffer.language_scope_at(start);
existing_indent =
logical_indent_for_newline(&start_point, &buffer, existing_indent);
let (delimiter, newline_config) = if let Some(language) = &language_scope {
let needs_extra_newline = NewlineConfig::insert_extra_newline_brackets(
&buffer,
@@ -2619,6 +2621,28 @@ fn documentation_delimiter_for_newline(
}
}
/// The indentation a line inserted at `start_point` should start at, which is
/// `existing_indent` unless the cursor sits after the closing delimiter of a
/// multi-line block comment. See [`language::BufferSnapshot::block_comment_closing_indent`].
fn logical_indent_for_newline(
start_point: &Point,
buffer: &MultiBufferSnapshot,
existing_indent: IndentSize,
) -> IndentSize {
let Some((snapshot, line_range)) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))
else {
return existing_indent;
};
// Columns agree between the multi-buffer and the underlying buffer for a line
// an excerpt shows in full, which is the case we care about. For a partial
// first line the column comes out too small and the lookup below declines,
// leaving the indent alone.
let position = Point::new(line_range.start.row, start_point.column);
snapshot
.block_comment_closing_indent(position)
.unwrap_or(existing_indent)
}
fn list_delimiter_for_newline(
start_point: &Point,
buffer: &MultiBufferSnapshot,