git_graph: Add basic keyboard navigation (#49051)

Remco Smits created

This PR adds basic support for keyboard navigation for the git graph
panel.

**Back and forward**:


https://github.com/user-attachments/assets/5015b0d9-bf83-4944-8c9b-1c5b9badfdb4

**Scrolling**:


https://github.com/user-attachments/assets/451badb5-59df-48a2-aa73-be5188d28dae

- [x] Tests or screenshots needed?
- [x] Code Reviewed
- [x] Manual QA
- Do we need to add keybinds for it, or is falling back to the default
keybindings enough?
 
 **TODO**:
 - [x] Add auto scroll when you select the last visible item

Release Notes:

- N/A (no release notes since its behind a feature flag)

Change summary

Cargo.lock                        |  1 +
crates/git_graph/Cargo.toml       |  1 +
crates/git_graph/src/git_graph.rs | 29 +++++++++++++++++++++++++++--
3 files changed, 29 insertions(+), 2 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7221,6 +7221,7 @@ dependencies = [
  "git",
  "git_ui",
  "gpui",
+ "menu",
  "project",
  "rand 0.9.2",
  "recent_projects",

crates/git_graph/Cargo.toml 🔗

@@ -26,6 +26,7 @@ feature_flags.workspace = true
 git.workspace = true
 git_ui.workspace = true
 gpui.workspace = true
+menu.workspace = true
 project.workspace = true
 settings.workspace = true
 smallvec.workspace = true

crates/git_graph/src/git_graph.rs 🔗

@@ -9,9 +9,10 @@ use git_ui::commit_tooltip::CommitAvatar;
 use gpui::{
     AnyElement, App, Bounds, ClipboardItem, Context, Corner, DefiniteLength, ElementId, Entity,
     EventEmitter, FocusHandle, Focusable, FontWeight, Hsla, InteractiveElement, ParentElement,
-    PathBuilder, Pixels, Point, Render, ScrollWheelEvent, SharedString, Styled, Subscription, Task,
-    WeakEntity, Window, actions, anchored, deferred, point, px,
+    PathBuilder, Pixels, Point, Render, ScrollStrategy, ScrollWheelEvent, SharedString, Styled,
+    Subscription, Task, WeakEntity, Window, actions, anchored, deferred, point, px,
 };
+use menu::{SelectNext, SelectPrevious};
 use project::{
     Project,
     git_store::{CommitDataState, GitStoreEvent, Repository, RepositoryEvent},
@@ -844,6 +845,22 @@ impl GitGraph {
             .collect()
     }
 
+    fn select_prev(&mut self, _: &SelectPrevious, _window: &mut Window, cx: &mut Context<Self>) {
+        if let Some(selected_entry_idx) = &self.selected_entry_idx {
+            self.select_entry(selected_entry_idx.saturating_sub(1), cx);
+        } else {
+            self.select_entry(0, cx);
+        }
+    }
+
+    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+        if let Some(selected_entry_idx) = &self.selected_entry_idx {
+            self.select_entry(selected_entry_idx.saturating_add(1), cx);
+        } else {
+            self.select_prev(&SelectPrevious, window, cx);
+        }
+    }
+
     fn select_entry(&mut self, idx: usize, cx: &mut Context<Self>) {
         if self.selected_entry_idx == Some(idx) {
             return;
@@ -851,6 +868,12 @@ impl GitGraph {
 
         self.selected_entry_idx = Some(idx);
         self.selected_commit_diff = None;
+        self.table_interaction_state.update(cx, |state, cx| {
+            state
+                .scroll_handle
+                .scroll_to_item(idx, ScrollStrategy::Nearest);
+            cx.notify();
+        });
 
         let Some(commit) = self.graph_data.commits.get(idx) else {
             return;
@@ -1604,6 +1627,8 @@ impl Render for GitGraph {
             .bg(cx.theme().colors().editor_background)
             .key_context("GitGraph")
             .track_focus(&self.focus_handle)
+            .on_action(cx.listener(Self::select_prev))
+            .on_action(cx.listener(Self::select_next))
             .child(content)
             .children(self.context_menu.as_ref().map(|(menu, position, _)| {
                 deferred(