Fix gutter highlights not matching diff hunks near excerpt boundaries (#25600)

Release Notes:

- Fixed gutter highlights not matching diff hunks in multibuffers in
some cases

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Cole Miller 2025-02-25 18:33:16 -05:00 committed by GitHub
parent d68d858a10
commit 198f56c763
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 173 additions and 66 deletions

View file

@ -52,7 +52,7 @@ pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit};
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager;
use buffer_diff::DiffHunkSecondaryStatus;
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus};
use client::{Collaborator, ParticipantIndex};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
@ -253,6 +253,19 @@ impl Navigated {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum DisplayDiffHunk {
Folded {
display_row: DisplayRow,
},
Unfolded {
diff_base_byte_range: Range<usize>,
display_row_range: Range<DisplayRow>,
multi_buffer_range: Range<Anchor>,
status: DiffHunkStatus,
},
}
pub fn init_settings(cx: &mut App) {
EditorSettings::register(cx);
}
@ -16915,6 +16928,52 @@ impl EditorSnapshot {
hunks
}
fn display_diff_hunks_for_rows<'a>(
&'a self,
display_rows: Range<DisplayRow>,
folded_buffers: &'a HashSet<BufferId>,
) -> impl 'a + Iterator<Item = DisplayDiffHunk> {
let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(self);
let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(self);
self.buffer_snapshot
.diff_hunks_in_range(buffer_start..buffer_end)
.filter_map(|hunk| {
if folded_buffers.contains(&hunk.buffer_id) {
return None;
}
let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
let hunk_end_point = Point::new(hunk.row_range.end.0, 0);
let hunk_display_start = self.point_to_display_point(hunk_start_point, Bias::Left);
let hunk_display_end = self.point_to_display_point(hunk_end_point, Bias::Right);
let display_hunk = if hunk_display_start.column() != 0 {
DisplayDiffHunk::Folded {
display_row: hunk_display_start.row(),
}
} else {
let mut end_row = hunk_display_end.row();
if hunk_display_end.column() > 0 {
end_row.0 += 1;
}
DisplayDiffHunk::Unfolded {
status: hunk.status(),
diff_base_byte_range: hunk.diff_base_byte_range,
display_row_range: hunk_display_start.row()..end_row,
multi_buffer_range: Anchor::range_in_buffer(
hunk.excerpt_id,
hunk.buffer_id,
hunk.buffer_range,
),
}
};
Some(display_hunk)
})
}
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
self.display_snapshot.buffer_snapshot.language_at(position)
}

View file

@ -24,7 +24,7 @@ use language::{
Override, Point,
};
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::IndentGuide;
use multi_buffer::{IndentGuide, PathKey};
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::project_settings::{LspSettings, ProjectSettings};
@ -36,6 +36,7 @@ use std::{
sync::atomic::{self, AtomicUsize},
};
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
use text::ToPoint as _;
use unindent::Unindent;
use util::{
assert_set_eq, path,
@ -14983,6 +14984,94 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp
);
}
#[gpui::test]
async fn test_display_diff_hunks(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
".git": {},
"file-1": "ONE\n",
"file-2": "TWO\n",
"file-3": "THREE\n",
}),
)
.await;
fs.set_head_for_repo(
path!("/test/.git").as_ref(),
&[
("file-1".into(), "one\n".into()),
("file-2".into(), "two\n".into()),
("file-3".into(), "three\n".into()),
],
);
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
let mut buffers = vec![];
for i in 1..=3 {
let buffer = project
.update(cx, |project, cx| {
let path = format!(path!("/test/file-{}"), i);
project.open_local_buffer(path, cx)
})
.await
.unwrap();
buffers.push(buffer);
}
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
multibuffer.set_all_diff_hunks_expanded(cx);
for buffer in &buffers {
let snapshot = buffer.read(cx).snapshot();
multibuffer.set_excerpts_for_path(
PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
buffer.clone(),
vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
}
multibuffer
});
let editor = cx.add_window(|window, cx| {
Editor::new(
EditorMode::Full,
multibuffer,
Some(project),
true,
window,
cx,
)
});
cx.run_until_parked();
let snapshot = editor
.update(cx, |editor, window, cx| editor.snapshot(window, cx))
.unwrap();
let hunks = snapshot
.display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
.map(|hunk| match hunk {
DisplayDiffHunk::Unfolded {
display_row_range, ..
} => display_row_range,
DisplayDiffHunk::Folded { .. } => unreachable!(),
})
.collect::<Vec<_>>();
assert_eq!(
hunks,
[
DisplayRow(3)..DisplayRow(5),
DisplayRow(10)..DisplayRow(12),
DisplayRow(17)..DisplayRow(19),
]
);
}
#[gpui::test]
async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View file

@ -15,8 +15,8 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint,
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt,
@ -79,19 +79,6 @@ use workspace::{item::Item, notifications::NotifyTaskExt};
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
const MIN_SCROLL_THUMB_SIZE: f32 = 25.;
#[derive(Debug, Clone, PartialEq, Eq)]
enum DisplayDiffHunk {
Folded {
display_row: DisplayRow,
},
Unfolded {
diff_base_byte_range: Range<usize>,
display_row_range: Range<DisplayRow>,
multi_buffer_range: Range<Anchor>,
status: DiffHunkStatus,
},
}
struct SelectionLayout {
head: DisplayPoint,
cursor_shape: CursorShape,
@ -1553,50 +1540,11 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(snapshot);
let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(snapshot);
let mut display_hunks = Vec::<(DisplayDiffHunk, Option<Hitbox>)>::new();
let folded_buffers = self.editor.read(cx).folded_buffers(cx);
for hunk in snapshot
.buffer_snapshot
.diff_hunks_in_range(buffer_start..buffer_end)
{
if folded_buffers.contains(&hunk.buffer_id) {
continue;
}
let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
let hunk_end_point = Point::new(hunk.row_range.end.0, 0);
let hunk_display_start = snapshot.point_to_display_point(hunk_start_point, Bias::Left);
let hunk_display_end = snapshot.point_to_display_point(hunk_end_point, Bias::Right);
let display_hunk = if hunk_display_start.column() != 0 {
DisplayDiffHunk::Folded {
display_row: hunk_display_start.row(),
}
} else {
let mut end_row = hunk_display_end.row();
if hunk_display_end.column() > 0 {
end_row.0 += 1;
}
DisplayDiffHunk::Unfolded {
status: hunk.status(),
diff_base_byte_range: hunk.diff_base_byte_range,
display_row_range: hunk_display_start.row()..end_row,
multi_buffer_range: Anchor::range_in_buffer(
hunk.excerpt_id,
hunk.buffer_id,
hunk.buffer_range,
),
}
};
display_hunks.push((display_hunk, None));
}
let mut display_hunks = snapshot
.display_diff_hunks_for_rows(display_rows, folded_buffers)
.map(|hunk| (hunk, None))
.collect::<Vec<_>>();
let git_gutter_setting = ProjectSettings::get_global(cx)
.git
.git_gutter

View file

@ -6146,6 +6146,16 @@ where
|| self.diff_transforms.item().is_none()
{
self.excerpts.next(&());
} else if let Some(DiffTransform::DeletedHunk { hunk_info, .. }) =
self.diff_transforms.item()
{
if self
.excerpts
.item()
.map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id)
{
self.excerpts.next(&());
}
}
}
}
@ -6183,12 +6193,13 @@ where
return true;
}
self.diff_transforms.next(&());
let next_transform = self.diff_transforms.item();
self.diff_transforms.prev(&());
next_transform.map_or(true, |next_transform| {
matches!(next_transform, DiffTransform::BufferContent { .. })
let next_transform = self.diff_transforms.next_item();
next_transform.map_or(true, |next_transform| match next_transform {
DiffTransform::BufferContent { .. } => true,
DiffTransform::DeletedHunk { hunk_info, .. } => self
.excerpts
.item()
.map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id),
})
}