diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 913ab20c4c1..d2eb44ecc87 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1079,6 +1079,7 @@ pub struct Editor { show_selection_menu: Option, blame: Option>, blame_subscription: Option, + pending_blame_hover_observation: Option, custom_context_menu: Option< Box< dyn 'static @@ -2399,6 +2400,7 @@ impl Editor { }), blame: None, blame_subscription: None, + pending_blame_hover_observation: None, bookmark_store, breakpoint_store, diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 7224fa15d4c..6bb94a892dc 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -638,6 +638,29 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { + let just_started = self.blame.is_none(); + if just_started { + self.start_git_blame(true, window, cx); + } + let Some(blame) = self.blame.as_ref() else { + return; + }; + + if just_started && !blame.read(cx).has_generated_entries() { + let subscription = cx.observe_in(blame, window, |editor, blame, window, cx| { + if blame.read(cx).has_generated_entries() { + editor.pending_blame_hover_observation.take(); + editor.show_blame_hover_popover(window, cx); + } + }); + self.pending_blame_hover_observation = Some(subscription); + return; + } + + self.show_blame_hover_popover(window, cx); + } + + fn show_blame_hover_popover(&mut self, window: &mut Window, cx: &mut Context) { let snapshot = self.snapshot(window, cx); let cursor = self .selections @@ -647,9 +670,6 @@ impl Editor { return; }; - if self.blame.is_none() { - self.start_git_blame(true, window, cx); - } let Some(blame) = self.blame.as_ref() else { return; }; diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 0e6ad1cb0ea..e96a1087bd5 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -1295,4 +1295,101 @@ mod tests { filename: String::new(), } } + + #[gpui::test] + async fn test_blame_hover_shows_popover_on_first_trigger(cx: &mut gpui::TestAppContext) { + init_test(cx); + + cx.update(|cx| { + use gpui::UpdateGlobal; + settings::SettingsStore::update_global( + cx, + |store: &mut settings::SettingsStore, cx| { + store + .set_user_settings(r#"{"git": {"inline_blame": {"enabled": false}}}"#, cx) + .expect("failed to set user settings"); + }, + ); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/my-repo", + json!({ + ".git": {}, + "file.txt": "line 1\nline 2\nline 3\n" + }), + ) + .await; + + fs.set_blame_for_repo( + Path::new("/my-repo/.git"), + vec![( + repo_path("file.txt"), + Blame { + entries: vec![ + blame_entry("1b1b1b", 0..1), + blame_entry("2c2c2c", 1..2), + blame_entry("3d3d3d", 2..3), + ], + ..Default::default() + }, + )], + ); + + let project = project::Project::test(fs, ["/my-repo".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/my-repo/file.txt", cx) + }) + .await + .unwrap(); + let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + + let (editor, cx) = cx.add_window_view(|window, cx| { + crate::test::build_editor_with_project(project, multi_buffer, window, cx) + }); + + // Verify blame is not loaded yet + editor.update(cx, |editor, _cx| { + assert!( + editor.blame().is_none(), + "blame should not be loaded initially" + ); + }); + + // Focus the editor so that blame generation proceeds + editor.update_in(cx, |editor, window, cx| { + editor.focus_handle.focus(window, cx); + }); + + // Trigger BlameHover — this should start blame loading and defer showing the popover + editor.update_in(cx, |editor, window, cx| { + assert!(editor.blame().is_none()); + editor.blame_hover(&crate::BlameHover, window, cx); + assert!( + editor.blame().is_some(), + "blame entity should be created after blame_hover" + ); + assert!( + editor.pending_blame_hover_observation.is_some(), + "should have registered an observation to wait for blame data" + ); + }); + + // Let the async blame generation complete + cx.run_until_parked(); + + // The observation should have fired and cleaned itself up + editor.update(cx, |editor, cx| { + assert!( + editor.pending_blame_hover_observation.is_none(), + "observation should be consumed after blame data is generated" + ); + assert!( + editor.blame().unwrap().read(cx).has_generated_entries(), + "blame should have generated entries" + ); + }); + } }