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:
1 parent
e9b18cd2d5
commit
918e494e61
3 files changed
+268
-5
No files matched your search
@@ -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, |_| {});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user