editor: Do not reserve inline blame scroll width when blame is in the status bar (#61422)

## Problem

When `git.inline_blame.location` is `status_bar` (anything other than
`inline`), nothing is painted inline at the end of the line — yet the
horizontal scroll range still reserves the longest row's inline blame
width (introduced in #23374), letting the editor scroll into blank space
when a line is long enough to scroll. Reported in #24752 (e.g.
@Nemesis19's comment: still reproduces with `location: status_bar`).

## Fix

Skip the blame width reservation in the scroll range when the blame
location isn't `Inline` — one guard in the `longest_line_blame_width`
closure in `element.rs`. This is independent of the soft-wrap fix for
#24752 (#61319) and can land on its own.

## Test

`test_status_bar_blame_location_reserves_no_scroll_width` asserts that,
with a long line and no soft wrap, moving blame from inline to the
status bar shrinks the horizontal scroll range (the reservation is
dropped). It uses a fixed-width test `BlameRenderer`, since the default
test renderer returns `None` and never exercised the reservation.

Release Notes:

- Fixed inline git blame reserving horizontal scroll space even when
blame is displayed in the status bar
This commit is contained in:
Vlad Gaevsky authored and GitHub committed 2026-07-22 14:32:56 +00:00
1 parent f555af0236
commit 9cbae806f2
1 file changed
+198 -1
+198 -1
View File
@@ -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<ParsedCommitMessage>,
_: Vec<SharedString>,
_: Entity<project::git_store::Repository>,
_: WeakEntity<Workspace>,
_: Entity<Editor>,
_: usize,
_: Hsla,
_: &mut Window,
_: &mut App,
) -> Option<AnyElement> {
None
}
fn render_inline_blame_entry(
&self,
_: &gpui::TextStyle,
_: BlameEntry,
_: &mut App,
) -> Option<AnyElement> {
Some(div().w(px(160.)).into_any_element())
}
fn render_blame_entry_popover(
&self,
_: BlameEntry,
_: ScrollHandle,
_: Option<ParsedCommitMessage>,
_: Vec<SharedString>,
_: Entity<Markdown>,
_: Entity<project::git_store::Repository>,
_: WeakEntity<Workspace>,
_: &mut Window,
_: &mut App,
) -> Option<AnyElement> {
None
}
fn open_blame_commit(
&self,
_: BlameEntry,
_: Entity<project::git_store::Repository>,
_: WeakEntity<Workspace>,
_: &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::<settings::SettingsStore, _>(|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, |_| {});