Improve code folding to exclude folding line breaks in whitespace-sensitive languages (#13108)

Nigel Jose created

<img width="1219" alt="Screenshot 2024-06-16 at 15 43 31"
src="https://github.com/zed-industries/zed/assets/87859239/dd05de16-7f20-4c88-9e95-021555b8b78b">
<img width="1219" alt="Screenshot 2024-06-16 at 15 45 10"
src="https://github.com/zed-industries/zed/assets/87859239/b1b78cdd-f34d-4ea3-9728-4741727a9643">

Updated the foldable_range method to exclude folding line breaks during
code folding in whitespace-sensitive languages like Python and YAML.
This adjustment ensures that folding behaves as expected, similar to
other code editors.

Ref #11614

Release Notes:

- Improved code folds to ignore trailing newlines

Change summary

crates/editor/src/display_map.rs  |  19 +++
crates/editor/src/editor_tests.rs | 169 +++++++++++++++++++++++++++++++++
2 files changed, 186 insertions(+), 2 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -983,8 +983,23 @@ impl DisplaySnapshot {
                     break;
                 }
             }
-            let end = end.unwrap_or(max_point);
-            Some((start..end, self.fold_placeholder.clone()))
+
+            let mut row_before_line_breaks = end.unwrap_or(max_point);
+            while row_before_line_breaks.row > start.row
+                && self
+                    .buffer_snapshot
+                    .is_line_blank(MultiBufferRow(row_before_line_breaks.row))
+            {
+                row_before_line_breaks.row -= 1;
+            }
+
+            row_before_line_breaks = Point::new(
+                row_before_line_breaks.row,
+                self.buffer_snapshot
+                    .line_len(MultiBufferRow(row_before_line_breaks.row)),
+            );
+
+            Some((start..row_before_line_breaks, self.fold_placeholder.clone()))
         } else {
             None
         }

crates/editor/src/editor_tests.rs 🔗

@@ -858,6 +858,175 @@ fn test_fold_action(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple(
+            &"
+                class Foo:
+                    # Hello!
+
+                    def a():
+                        print(1)
+
+                    def b():
+                        print(2)
+
+                    def c():
+                        print(3)
+            "
+            .unindent(),
+            cx,
+        );
+        build_editor(buffer.clone(), cx)
+    });
+
+    _ = view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(10), 0)
+            ]);
+        });
+        view.fold(&Fold, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                class Foo:
+                    # Hello!
+
+                    def a():
+                        print(1)
+
+                    def b():⋯
+
+                    def c():⋯
+            "
+            .unindent(),
+        );
+
+        view.fold(&Fold, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                class Foo:⋯
+            "
+            .unindent(),
+        );
+
+        view.unfold_lines(&UnfoldLines, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                class Foo:
+                    # Hello!
+
+                    def a():
+                        print(1)
+
+                    def b():⋯
+
+                    def c():⋯
+            "
+            .unindent(),
+        );
+
+        view.unfold_lines(&UnfoldLines, cx);
+        assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
+    });
+}
+
+#[gpui::test]
+fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple(
+            &"
+                class Foo:
+                    # Hello!
+
+                    def a():
+                        print(1)
+
+                    def b():
+                        print(2)
+
+
+                    def c():
+                        print(3)
+
+
+            "
+            .unindent(),
+            cx,
+        );
+        build_editor(buffer.clone(), cx)
+    });
+
+    _ = view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(11), 0)
+            ]);
+        });
+        view.fold(&Fold, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                class Foo:
+                    # Hello!
+
+                    def a():
+                        print(1)
+
+                    def b():⋯
+
+
+                    def c():⋯
+
+
+            "
+            .unindent(),
+        );
+
+        view.fold(&Fold, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                class Foo:⋯
+
+
+            "
+            .unindent(),
+        );
+
+        view.unfold_lines(&UnfoldLines, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                class Foo:
+                    # Hello!
+
+                    def a():
+                        print(1)
+
+                    def b():⋯
+
+
+                    def c():⋯
+
+
+            "
+            .unindent(),
+        );
+
+        view.unfold_lines(&UnfoldLines, cx);
+        assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
+    });
+}
+
 #[gpui::test]
 fn test_move_cursor(cx: &mut TestAppContext) {
     init_test(cx, |_| {});