git_ui: Add support for collapsing/expanding entries with your keyboard (#45002)

Remco Smits created

This PR adds support for collapsing/expanding Git entries with your
keyboard like you can inside the project panel and variable list.

I noticed there is a bug that selecting the next entry when you are on
the directory level will select a non-visible entry. Will fix that in
another PR, as it is not related to this feature implementation.

**Result**:


https://github.com/user-attachments/assets/912cc146-1e1c-485f-9b60-5ddc0a124696

Release Notes:

- Git panel: Add support for collapsing/expanding entries with your
keyboard.

Change summary

assets/keymaps/default-linux.json   |  2 +
assets/keymaps/default-macos.json   |  2 +
assets/keymaps/default-windows.json |  2 +
crates/git_ui/src/git_panel.rs      | 48 +++++++++++++++++++++++++++++++
4 files changed, 54 insertions(+)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -900,6 +900,8 @@
   {
     "context": "GitPanel && ChangesList",
     "bindings": {
+      "left": "git_panel::CollapseSelectedEntry",
+      "right": "git_panel::ExpandSelectedEntry",
       "up": "menu::SelectPrevious",
       "down": "menu::SelectNext",
       "enter": "menu::Confirm",

assets/keymaps/default-macos.json 🔗

@@ -975,6 +975,8 @@
     "context": "GitPanel && ChangesList",
     "use_key_equivalents": true,
     "bindings": {
+      "left": "git_panel::CollapseSelectedEntry",
+      "right": "git_panel::ExpandSelectedEntry",
       "up": "menu::SelectPrevious",
       "down": "menu::SelectNext",
       "cmd-up": "menu::SelectFirst",

assets/keymaps/default-windows.json 🔗

@@ -904,6 +904,8 @@
     "context": "GitPanel && ChangesList",
     "use_key_equivalents": true,
     "bindings": {
+      "left": "git_panel::CollapseSelectedEntry",
+      "right": "git_panel::ExpandSelectedEntry",
       "up": "menu::SelectPrevious",
       "down": "menu::SelectNext",
       "enter": "menu::Confirm",

crates/git_ui/src/git_panel.rs 🔗

@@ -98,6 +98,10 @@ actions!(
         ToggleSortByPath,
         /// Toggles showing entries in tree vs flat view.
         ToggleTreeView,
+        /// Expands the selected entry to show its children.
+        ExpandSelectedEntry,
+        /// Collapses the selected entry to hide its children.
+        CollapseSelectedEntry,
     ]
 );
 
@@ -896,6 +900,48 @@ impl GitPanel {
             .position(|entry| entry.status_entry().is_some())
     }
 
+    fn expand_selected_entry(
+        &mut self,
+        _: &ExpandSelectedEntry,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(entry) = self.get_selected_entry().cloned() else {
+            return;
+        };
+
+        if let GitListEntry::Directory(dir_entry) = entry {
+            if dir_entry.expanded {
+                self.select_next(&SelectNext, window, cx);
+            } else {
+                self.toggle_directory(&dir_entry.key, window, cx);
+            }
+        } else {
+            self.select_next(&SelectNext, window, cx);
+        }
+    }
+
+    fn collapse_selected_entry(
+        &mut self,
+        _: &CollapseSelectedEntry,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(entry) = self.get_selected_entry().cloned() else {
+            return;
+        };
+
+        if let GitListEntry::Directory(dir_entry) = entry {
+            if dir_entry.expanded {
+                self.toggle_directory(&dir_entry.key, window, cx);
+            } else {
+                self.select_previous(&SelectPrevious, window, cx);
+            }
+        } else {
+            self.select_previous(&SelectPrevious, window, cx);
+        }
+    }
+
     fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
         if let Some(first_entry) = self.first_status_entry_index() {
             self.selected_entry = Some(first_entry);
@@ -5264,6 +5310,8 @@ impl Render for GitPanel {
                     .on_action(cx.listener(Self::stash_all))
                     .on_action(cx.listener(Self::stash_pop))
             })
+            .on_action(cx.listener(Self::collapse_selected_entry))
+            .on_action(cx.listener(Self::expand_selected_entry))
             .on_action(cx.listener(Self::select_first))
             .on_action(cx.listener(Self::select_next))
             .on_action(cx.listener(Self::select_previous))