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, |_| {});