From f4addb6a24a9aac5a94be15cdf3fd66184dd8ca0 Mon Sep 17 00:00:00 2001 From: Finn Eitreim <48069764+feitreim@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:53:08 -0400 Subject: [PATCH] editor: Make the multiline comment folding more robust (#54102) Basically just applied the suggested fix as described in #53606 , I also experimented with a hackier soln. that used the same logic as the code this is replacing, but it seemed solidly worse than this version. The original issue extremely clear with what is going on / what needed to be fixed. heres a video: https://github.com/user-attachments/assets/e7fc60f7-0a8c-403c-877f-93f58f370b00 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 is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #53606 Release Notes: - editor: Fixed incorrect inclusion of comments into folds --- crates/editor/src/display_map.rs | 23 +++- crates/editor/src/editor_tests.rs | 176 ++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index dae77579eb6a887accfe3a7510f21a3d9970dc6e..d00376fc02039e422b116f361f99656c211716b1 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -2268,23 +2268,38 @@ impl DisplaySnapshot { && !self.is_line_folded(MultiBufferRow(start.row)) { let start_line_indent = self.line_indent_for_buffer_row(buffer_row); - let max_point = self.buffer_snapshot().max_point(); + let snapshot = self.buffer_snapshot(); + let max_point = snapshot.max_point(); let mut closing_row = None; + // End byte of the smallest syntactic node enclosing `buffer_row`. + // Used to tell standalone top-level comments (which terminate the + // fold) apart from unindented content inside a multi-line string + // or block comment belonging to the folded node (which does not). + let foldable_node_end = { + let row_start = Point::new(buffer_row.0, 0); + let row_end = Point::new(buffer_row.0, snapshot.line_len(buffer_row)); + snapshot + .syntax_ancestor(row_start..row_end) + .map(|(_, range)| range.end) + }; + for row in (buffer_row.0 + 1)..=max_point.row { let line_indent = self.line_indent_for_buffer_row(MultiBufferRow(row)); if !line_indent.is_line_blank() && line_indent.raw_len() <= start_line_indent.raw_len() { - if self - .buffer_snapshot() + let in_string_or_comment_scope = snapshot .language_scope_at(Point::new(row, 0)) .is_some_and(|scope| { matches!( scope.override_name(), Some("string") | Some("comment") | Some("comment.inclusive") ) - }) + }); + if in_string_or_comment_scope + && let Some(end) = foldable_node_end + && Point::new(row, 0).to_offset(snapshot) < end { continue; } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 647f40a95c3fca01dd0de74cbe9837feb461f11b..6dfbf3340244d1036f9764c5f7a7e50e9d84ab86 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1636,6 +1636,182 @@ async fn test_fold_with_unindented_multiline_block_comment_includes_closing_brac }); } +#[gpui::test] +async fn test_fold_preserves_top_level_comments_between_python_classes(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_python::LANGUAGE.into()), + ) + .with_queries(LanguageQueries { + overrides: Some(Cow::from(indoc! {" + (comment) @comment.inclusive + (string) @string + "})), + ..Default::default() + }) + .expect("Could not parse queries"), + ); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + class Foo: + def bar(self): + pass + + + # SECTION SEPARATOR + + class Baz: + def qux(self): + passˇ + "}); + + cx.update_editor(|editor, window, cx| { + editor.fold_at_level(&FoldAtLevel(1), window, cx); + assert_eq!( + editor.display_text(cx), + indoc! {" + class Foo:⋯ + + + # SECTION SEPARATOR + + class Baz: + def qux(self): + pass + "}, + ); + }); +} + +#[gpui::test] +async fn test_fold_preserves_top_level_comments_between_rust_functions(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_queries(LanguageQueries { + overrides: Some(Cow::from(indoc! {" + [ + (string_literal) + (raw_string_literal) + ] @string + [ + (line_comment) + (block_comment) + ] @comment.inclusive + "})), + ..Default::default() + }) + .expect("Could not parse queries"), + ); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + fn foo() { + bar(); + } + + + // SECTION SEPARATOR + + + fn baz() { + qux();ˇ + } + "}); + + cx.update_editor(|editor, window, cx| { + editor.fold_at_level(&FoldAtLevel(1), window, cx); + assert_eq!( + editor.display_text(cx), + indoc! {" + fn foo() {⋯ + } + + + // SECTION SEPARATOR + + + fn baz() { + qux(); + } + "}, + ); + }); +} + +#[gpui::test] +async fn test_fold_terminates_at_top_level_multiline_string_between_python_classes( + cx: &mut TestAppContext, +) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_python::LANGUAGE.into()), + ) + .with_queries(LanguageQueries { + overrides: Some(Cow::from(indoc! {" + (comment) @comment.inclusive + (string) @string + "})), + ..Default::default() + }) + .expect("Could not parse queries"), + ); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {r#" + class Foo: + def bar(self): + pass + + + """ + top-level docstring at zero indent + """ + + + class Baz: + def qux(self): + passˇ + "#}); + + cx.update_editor(|editor, window, cx| { + editor.fold_at_level(&FoldAtLevel(1), window, cx); + assert_eq!( + editor.display_text(cx), + indoc! {r#" + class Foo:⋯ + + + """ + top-level docstring at zero indent + """ + + + class Baz: + def qux(self): + pass + "#}, + ); + }); +} + #[gpui::test] fn test_fold_at_level(cx: &mut TestAppContext) { init_test(cx, |_| {});