Add `editor: fold at level <level>` commands (#19750)

Joseph T. Lyons and Piotr Osiewicz created

Closes https://github.com/zed-industries/zed/issues/5142

Note that I only moved the cursor to the top of the file so it wouldn't
jump - the commands work no matter where you are in the file.


https://github.com/user-attachments/assets/78c74ca6-5c17-477c-b5d1-97c5665e44b0

Also, is VS Code doing this right thing here? or is it busted?


https://github.com/user-attachments/assets/8c503b50-9671-4221-b9f8-1e692fe8cd9a

Release Notes:

- Added `editor: fold at level <level>` commands. macOS: `cmd-k,
cmd-<number>`, Linux: `ctrl-k, ctrl-<number>`.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>

Change summary

assets/keymaps/default-linux.json |  9 +++++++
assets/keymaps/default-macos.json | 10 +++++++
crates/editor/src/actions.rs      |  5 ++++
crates/editor/src/editor.rs       | 41 ++++++++++++++++++++++++++++----
crates/editor/src/element.rs      |  1 
5 files changed, 59 insertions(+), 7 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -313,6 +313,15 @@
       "ctrl-k ctrl-l": "editor::ToggleFold",
       "ctrl-k ctrl-[": "editor::FoldRecursive",
       "ctrl-k ctrl-]": "editor::UnfoldRecursive",
+      "ctrl-k ctrl-1": ["editor::FoldAtLevel", { "level": 1 }],
+      "ctrl-k ctrl-2": ["editor::FoldAtLevel", { "level": 2 }],
+      "ctrl-k ctrl-3": ["editor::FoldAtLevel", { "level": 3 }],
+      "ctrl-k ctrl-4": ["editor::FoldAtLevel", { "level": 4 }],
+      "ctrl-k ctrl-5": ["editor::FoldAtLevel", { "level": 5 }],
+      "ctrl-k ctrl-6": ["editor::FoldAtLevel", { "level": 6 }],
+      "ctrl-k ctrl-7": ["editor::FoldAtLevel", { "level": 7 }],
+      "ctrl-k ctrl-8": ["editor::FoldAtLevel", { "level": 8 }],
+      "ctrl-k ctrl-9": ["editor::FoldAtLevel", { "level": 9 }],
       "ctrl-k ctrl-0": "editor::FoldAll",
       "ctrl-k ctrl-j": "editor::UnfoldAll",
       "ctrl-space": "editor::ShowCompletions",

assets/keymaps/default-macos.json 🔗

@@ -349,7 +349,15 @@
       "alt-cmd-]": "editor::UnfoldLines",
       "cmd-k cmd-l": "editor::ToggleFold",
       "cmd-k cmd-[": "editor::FoldRecursive",
-      "cmd-k cmd-]": "editor::UnfoldRecursive",
+      "cmd-k cmd-1": ["editor::FoldAtLevel", { "level": 1 }],
+      "cmd-k cmd-2": ["editor::FoldAtLevel", { "level": 2 }],
+      "cmd-k cmd-3": ["editor::FoldAtLevel", { "level": 3 }],
+      "cmd-k cmd-4": ["editor::FoldAtLevel", { "level": 4 }],
+      "cmd-k cmd-5": ["editor::FoldAtLevel", { "level": 5 }],
+      "cmd-k cmd-6": ["editor::FoldAtLevel", { "level": 6 }],
+      "cmd-k cmd-7": ["editor::FoldAtLevel", { "level": 7 }],
+      "cmd-k cmd-8": ["editor::FoldAtLevel", { "level": 8 }],
+      "cmd-k cmd-9": ["editor::FoldAtLevel", { "level": 9 }],
       "cmd-k cmd-0": "editor::FoldAll",
       "cmd-k cmd-j": "editor::UnfoldAll",
       "ctrl-space": "editor::ShowCompletions",

crates/editor/src/actions.rs 🔗

@@ -153,6 +153,10 @@ pub struct DeleteToPreviousWordStart {
     pub ignore_newlines: bool,
 }
 
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct FoldAtLevel {
+    pub level: u32,
+}
 impl_actions!(
     editor,
     [
@@ -182,6 +186,7 @@ impl_actions!(
         ToggleCodeActions,
         ToggleComments,
         UnfoldAt,
+        FoldAtLevel
     ]
 );
 

crates/editor/src/editor.rs 🔗

@@ -10728,15 +10728,44 @@ impl Editor {
         self.fold_ranges(fold_ranges, true, cx);
     }
 
+    fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) {
+        let fold_at_level = fold_at.level;
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let mut fold_ranges = Vec::new();
+        let mut stack = vec![(0, snapshot.max_buffer_row().0, 1)];
+
+        while let Some((mut start_row, end_row, current_level)) = stack.pop() {
+            while start_row < end_row {
+                match self.snapshot(cx).foldable_range(MultiBufferRow(start_row)) {
+                    Some(foldable_range) => {
+                        let nested_start_row = foldable_range.0.start.row + 1;
+                        let nested_end_row = foldable_range.0.end.row;
+
+                        if current_level == fold_at_level {
+                            fold_ranges.push(foldable_range);
+                        }
+
+                        if current_level <= fold_at_level {
+                            stack.push((nested_start_row, nested_end_row, current_level + 1));
+                        }
+
+                        start_row = nested_end_row + 1;
+                    }
+                    None => start_row += 1,
+                }
+            }
+        }
+
+        self.fold_ranges(fold_ranges, true, cx);
+    }
+
     pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
         let mut fold_ranges = Vec::new();
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let snapshot = self.buffer.read(cx).snapshot(cx);
 
-        for row in 0..display_map.max_buffer_row().0 {
-            if let Some((foldable_range, fold_text)) =
-                display_map.foldable_range(MultiBufferRow(row))
-            {
-                fold_ranges.push((foldable_range, fold_text));
+        for row in 0..snapshot.max_buffer_row().0 {
+            if let Some(foldable_range) = self.snapshot(cx).foldable_range(MultiBufferRow(row)) {
+                fold_ranges.push(foldable_range);
             }
         }
 

crates/editor/src/element.rs 🔗

@@ -336,6 +336,7 @@ impl EditorElement {
         register_action(view, cx, Editor::open_url);
         register_action(view, cx, Editor::open_file);
         register_action(view, cx, Editor::fold);
+        register_action(view, cx, Editor::fold_at_level);
         register_action(view, cx, Editor::fold_all);
         register_action(view, cx, Editor::fold_at);
         register_action(view, cx, Editor::fold_recursive);