From ccd82319b262d773ccad1f4b64d846eb141792e5 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:29:57 +0530 Subject: [PATCH 1/2] git_graph: wire up Vim mode navigation --- Cargo.lock | 1 + assets/keymaps/vim.json | 9 ++ crates/git_graph/Cargo.toml | 1 + crates/git_graph/src/git_graph.rs | 153 ++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 67495074258f02..ae0928700d849b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7181,6 +7181,7 @@ dependencies = [ "git_ui", "gpui", "language", + "log", "menu", "project", "rand 0.9.2", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 464270274af775..c831a5bdbca31a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -1024,6 +1024,15 @@ "enter": "menu::Cancel", }, }, + { + "context": "GitGraph", + "bindings": { + "j": "vim::MenuSelectNext", + "k": "vim::MenuSelectPrevious", + "shift-g": "menu::SelectLast", + "g g": "menu::SelectFirst" + } + }, { "context": "GitPanel && ChangesList && !GitBranchSelector", "use_key_equivalents": true, diff --git a/crates/git_graph/Cargo.toml b/crates/git_graph/Cargo.toml index e9e31a8361e367..57c94cb260a11d 100644 --- a/crates/git_graph/Cargo.toml +++ b/crates/git_graph/Cargo.toml @@ -28,6 +28,7 @@ git.workspace = true git_ui.workspace = true gpui.workspace = true language.workspace = true +log.workspace = true menu.workspace = true project.workspace = true search.workspace = true diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index 370da2a8394f39..4a7d465adfe90e 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -18,6 +18,7 @@ use gpui::{ px, uniform_list, }; use language::line_diff; +use log; use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::git_store::{ CommitDataState, GitGraphEvent, GitStore, GitStoreEvent, GraphDataResponse, Repository, @@ -1338,10 +1339,12 @@ impl GitGraph { } fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context) { + log::debug!("GitGraph::select_first"); self.select_entry(0, ScrollStrategy::Nearest, cx); } fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { + log::debug!("GitGraph::select_prev"); if let Some(selected_entry_idx) = &self.selected_entry_idx { self.select_entry( selected_entry_idx.saturating_sub(1), @@ -1354,6 +1357,7 @@ impl GitGraph { } fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { + log::debug!("GitGraph::select_next"); if let Some(selected_entry_idx) = &self.selected_entry_idx { self.select_entry( selected_entry_idx @@ -1368,6 +1372,7 @@ impl GitGraph { } fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context) { + log::debug!("GitGraph::select_last"); self.select_entry( self.graph_data.commits.len().saturating_sub(1), ScrollStrategy::Nearest, @@ -4141,4 +4146,152 @@ mod tests { ); }); } + + #[gpui::test] + async fn test_git_graph_navigation(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + Path::new("/project"), + serde_json::json!({ + ".git": {}, + "file.txt": "content", + }), + ) + .await; + + let mut rng = StdRng::seed_from_u64(42); + let commits = generate_random_commit_dag(&mut rng, 10, false); + fs.set_graph_commits(Path::new("/project/.git"), commits.clone()); + + let project = Project::test(fs.clone(), [Path::new("/project")], cx).await; + cx.run_until_parked(); + + let repository = project.read_with(cx, |project, cx| { + project + .active_repository(cx) + .expect("should have a repository") + }); + + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + workspace::MultiWorkspace::test_new(project.clone(), window, cx) + }); + + let workspace_weak = + multi_workspace.read_with(&*cx, |multi, _| multi.workspace().downgrade()); + + let git_graph = cx.new_window_entity(|window, cx| { + GitGraph::new( + repository.read(cx).id, + project.read(cx).git_store().clone(), + workspace_weak, + window, + cx, + ) + }); + cx.run_until_parked(); + + // Focus the git graph + git_graph.update_in(cx, |graph, window, cx| { + graph.focus_handle(cx).focus(window, cx); + }); + cx.run_until_parked(); + + // Draw it to ensure action handlers are registered + cx.draw( + point(px(0.), px(0.)), + gpui::size(px(1200.), px(800.)), + |_, _| git_graph.clone().into_any_element(), + ); + cx.run_until_parked(); + + git_graph.read_with(&*cx, |graph, _| { assert_eq!(graph.graph_data.commits.len(), 10); }); + // Initial state: no selection + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, None); + }); + + // Select first + git_graph.update_in(cx, |graph, window, cx| { + graph.select_first(&menu::SelectFirst, window, cx); + }); + cx.run_until_parked(); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, Some(0)); + }); + + // Select next + git_graph.update_in(cx, |graph, window, cx| { + graph.select_next(&menu::SelectNext, window, cx); + }); + cx.run_until_parked(); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, Some(1)); + }); + + // Select previous + git_graph.update_in(cx, |graph, window, cx| { + graph.select_prev(&menu::SelectPrevious, window, cx); + }); + cx.run_until_parked(); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, Some(0)); + }); + + // Select last + git_graph.update_in(cx, |graph, window, cx| { + graph.select_last(&menu::SelectLast, window, cx); + }); + cx.run_until_parked(); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, Some(9)); + }); + + // Select next at the end should stay at the end + git_graph.update_in(cx, |graph, window, cx| { + graph.select_next(&menu::SelectNext, window, cx); + }); + cx.run_until_parked(); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, Some(9)); + }); + + // Select previous from last + git_graph.update_in(cx, |graph, window, cx| { + graph.select_prev(&menu::SelectPrevious, window, cx); + }); + cx.run_until_parked(); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, Some(8)); + }); + + // Select previous from no selection should select first + git_graph.update(cx, |graph, cx| { + graph.selected_entry_idx = None; + cx.notify(); + }); + cx.run_until_parked(); + git_graph.update_in(cx, |graph, window, cx| { + graph.select_prev(&menu::SelectPrevious, window, cx); + }); + cx.run_until_parked(); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, Some(0)); + }); + + // Select next from no selection should select first + git_graph.update(cx, |graph, cx| { + graph.selected_entry_idx = None; + cx.notify(); + }); + cx.run_until_parked(); + git_graph.update_in(cx, |graph, window, cx| { + graph.select_next(&menu::SelectNext, window, cx); + }); + cx.run_until_parked(); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.selected_entry_idx, Some(0)); + }); + } } From 92e1d8bc43230d827d1164f1a3f3e8b87fe5943e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 27 Apr 2026 10:58:33 +0200 Subject: [PATCH 2/2] Remove log statements --- Cargo.lock | 1 - crates/git_graph/Cargo.toml | 1 - crates/git_graph/src/git_graph.rs | 9 +++------ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae0928700d849b..67495074258f02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7181,7 +7181,6 @@ dependencies = [ "git_ui", "gpui", "language", - "log", "menu", "project", "rand 0.9.2", diff --git a/crates/git_graph/Cargo.toml b/crates/git_graph/Cargo.toml index 57c94cb260a11d..e9e31a8361e367 100644 --- a/crates/git_graph/Cargo.toml +++ b/crates/git_graph/Cargo.toml @@ -28,7 +28,6 @@ git.workspace = true git_ui.workspace = true gpui.workspace = true language.workspace = true -log.workspace = true menu.workspace = true project.workspace = true search.workspace = true diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index 4a7d465adfe90e..53834886bea321 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -18,7 +18,6 @@ use gpui::{ px, uniform_list, }; use language::line_diff; -use log; use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::git_store::{ CommitDataState, GitGraphEvent, GitStore, GitStoreEvent, GraphDataResponse, Repository, @@ -1339,12 +1338,10 @@ impl GitGraph { } fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context) { - log::debug!("GitGraph::select_first"); self.select_entry(0, ScrollStrategy::Nearest, cx); } fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { - log::debug!("GitGraph::select_prev"); if let Some(selected_entry_idx) = &self.selected_entry_idx { self.select_entry( selected_entry_idx.saturating_sub(1), @@ -1357,7 +1354,6 @@ impl GitGraph { } fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - log::debug!("GitGraph::select_next"); if let Some(selected_entry_idx) = &self.selected_entry_idx { self.select_entry( selected_entry_idx @@ -1372,7 +1368,6 @@ impl GitGraph { } fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context) { - log::debug!("GitGraph::select_last"); self.select_entry( self.graph_data.commits.len().saturating_sub(1), ScrollStrategy::Nearest, @@ -4206,7 +4201,9 @@ mod tests { ); cx.run_until_parked(); - git_graph.read_with(&*cx, |graph, _| { assert_eq!(graph.graph_data.commits.len(), 10); }); + git_graph.read_with(&*cx, |graph, _| { + assert_eq!(graph.graph_data.commits.len(), 10); + }); // Initial state: no selection git_graph.read_with(&*cx, |graph, _| { assert_eq!(graph.selected_entry_idx, None);