diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e3e1c14562..be01987333 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -64,7 +64,7 @@ use multi_buffer::{ use project::{ debugger::breakpoint_store::{Breakpoint, BreakpointSessionState}, - project_settings::ProjectSettings, + project_settings::{InlineBlameLocation, ProjectSettings}, }; use settings::{ GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring, @@ -8599,6 +8599,14 @@ impl Element for EditorElement { if !editor.show_git_blame_inline { return None; } + // Blame is only painted inline for the Inline location, so + // reserving scroll room for it in other locations would let + // the editor scroll into blank space. + if ProjectSettings::get_global(cx).git.inline_blame.location + != InlineBlameLocation::Inline + { + return None; + } let blame = editor.blame.as_ref()?; let (_, blame_entry) = blame .update(cx, |blame, cx| { @@ -10999,6 +11007,195 @@ mod tests { } } + #[gpui::test] + async fn test_status_bar_blame_location_reserves_no_scroll_width(cx: &mut TestAppContext) { + struct FixedWidthBlameRenderer; + + impl BlameRenderer for FixedWidthBlameRenderer { + fn max_author_length(&self) -> usize { + 20 + } + + fn render_blame_entry( + &self, + _: &gpui::TextStyle, + _: BlameEntry, + _: Option, + _: Vec, + _: Entity, + _: WeakEntity, + _: Entity, + _: usize, + _: Hsla, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn render_inline_blame_entry( + &self, + _: &gpui::TextStyle, + _: BlameEntry, + _: &mut App, + ) -> Option { + Some(div().w(px(160.)).into_any_element()) + } + + fn render_blame_entry_popover( + &self, + _: BlameEntry, + _: ScrollHandle, + _: Option, + _: Vec, + _: Entity, + _: Entity, + _: WeakEntity, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn open_blame_commit( + &self, + _: BlameEntry, + _: Entity, + _: WeakEntity, + _: &mut Window, + _: &mut App, + ) { + } + } + + init_test(cx, |_| {}); + cx.update(|cx| crate::git::set_blame_renderer(FixedWidthBlameRenderer, cx)); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + util::path!("/my-repo"), + serde_json::json!({ + ".git": {}, + "file.txt": "a ".repeat(100), + }), + ) + .await; + fs.set_blame_for_repo( + std::path::Path::new(util::path!("/my-repo/.git")), + vec![( + git::repository::repo_path("file.txt"), + git::blame::Blame { + entries: vec![BlameEntry { + sha: "1b1b1b".parse().unwrap(), + range: 0..1, + original_line_number: 0, + author: None, + author_mail: None, + author_time: None, + author_tz: None, + committer_name: None, + committer_email: None, + committer_time: None, + committer_tz: None, + summary: None, + previous: None, + filename: String::new(), + }], + ..Default::default() + }, + )], + ); + + let project = project::Project::test(fs, [util::path!("/my-repo").as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(util::path!("/my-repo/file.txt"), cx) + }) + .await + .unwrap(); + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + + let window = cx.add_window(|window, cx| { + // No soft wrap: a long line legitimately scrolls horizontally, so the + // inline blame width reservation is meaningful and observable here. + let mut editor = Editor::new(EditorMode::full(), buffer, Some(project), window, cx); + editor.set_soft_wrap_mode(language_settings::SoftWrap::None, cx); + editor + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + let editor = window.root(cx).unwrap(); + cx.update(|window, cx| window.focus(&editor.read(cx).focus_handle(cx), cx)); + editor.update(cx, |editor, cx| { + editor + .blame() + .expect("inline blame should be running") + .clone() + .update(cx, |blame, cx| blame.focus(cx)) + }); + cx.executor().run_until_parked(); + + // Ensure the blame entry actually loaded, so a broken setup can't let this + // test pass vacuously with a zero-width blame reservation on both draws. + editor.update(cx, |editor, cx| { + assert!(editor.show_git_blame_inline); + let blame = editor + .blame() + .expect("inline blame should be running") + .clone(); + let entry = blame.update(cx, |blame, cx| { + blame + .blame_for_rows( + &[RowInfo { + buffer_row: Some(0), + buffer_id: Some(buffer_id), + ..Default::default() + }], + cx, + ) + .next() + .flatten() + }); + assert!(entry.is_some(), "blame entry should be available"); + }); + + let style = cx.update(|_, cx| editor.update(cx, |editor, cx| editor.style(cx).clone())); + + // Default `Inline` location: the long line plus the reserved inline blame + // width push the horizontal scroll range past the viewport. + let (_, state) = cx.draw(Default::default(), size(px(226.), px(500.)), |_, _| { + EditorElement::new(&editor, style.clone()) + }); + let scroll_max_with_inline_blame = state.position_map.scroll_max.x; + assert!( + scroll_max_with_inline_blame > 0., + "inline blame on a long line should reserve horizontal scroll room" + ); + + // Moving blame to the status bar paints nothing inline, so the reservation + // must be dropped and the scroll range shrink accordingly. + cx.update(|_, cx| { + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings + .git + .get_or_insert_default() + .inline_blame + .get_or_insert_default() + .location = Some(settings::InlineBlameLocation::StatusBar); + }); + }); + }); + + let (_, state) = cx.draw(Default::default(), size(px(226.), px(500.)), |_, _| { + EditorElement::new(&editor, style.clone()) + }); + assert!( + state.position_map.scroll_max.x < scroll_max_with_inline_blame, + "Blame in the status bar should not reserve horizontal scroll room" + ); + } + #[gpui::test] async fn test_point_for_position_clipped_rows(cx: &mut TestAppContext) { init_test(cx, |_| {});