git: Side-by-side diff UI (#47349)

This PR implements a UI for the side-by-side diff, using blocks to align
the two sides and adding a coherent `SplitEditorElement`.

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
This commit is contained in:
Cole Miller 2026-01-22 02:31:14 -05:00 committed by GitHub
parent 7adb97acb9
commit 618f848c1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 7882 additions and 888 deletions

View file

@ -38,3 +38,7 @@ slow-timeout = { period = "300s", terminate-after = 1 }
[[profile.default.overrides]]
filter = 'package(vim) and (test(test_command_read) or test(test_capital_f_and_capital_t) or test(test_f_and_t) or test(test_change_paragraph_object) or test(test_change_surrounding_character_objects) or test(test_change_word_object) or test(test_delete_paragraph_object) or test(test_delete_surrounding_character_objects) or test(test_delete_word_object))'
slow-timeout = { period = "300s", terminate-after = 1 }
[[profile.default.overrides]]
filter = 'package(editor) and test(test_random_split_editor)'
slow-timeout = { period = "300s", terminate-after = 1 }

1
Cargo.lock generated
View file

@ -5516,6 +5516,7 @@ dependencies = [
"ui",
"unicode-script",
"unicode-segmentation",
"unicode-width",
"unindent",
"url",
"util",

View file

@ -707,6 +707,7 @@ tracing = "0.1.40"
unicase = "2.6"
unicode-script = "0.5.7"
unicode-segmentation = "1.10"
unicode-width = "0.2"
unindent = "0.2.0"
url = "2.2"
urlencoding = "2.1.2"

View file

@ -227,7 +227,7 @@ impl PendingDiff {
diff.update_diff(
text_snapshot.clone(),
Some(base_text.clone()),
false,
None,
language,
cx,
)
@ -399,7 +399,7 @@ async fn build_buffer_diff(
secondary_diff.update_diff(
text_snapshot.clone(),
Some(old_text),
true,
Some(false),
language.clone(),
cx,
)

View file

@ -408,7 +408,7 @@ impl ActionLog {
diff.update_diff(
buffer_snapshot.clone(),
Some(new_base_text),
true,
Some(true),
language,
cx,
)

View file

@ -302,7 +302,7 @@ impl CodegenAlternative {
let snapshot = buffer.read(cx).snapshot(cx);
let (old_buffer, _, _) = snapshot
.range_to_buffer_ranges(range.clone())
.range_to_buffer_ranges(range.start..=range.end)
.pop()
.unwrap();
let old_buffer = cx.new(|cx| {
@ -679,7 +679,7 @@ impl CodegenAlternative {
let language_name = {
let multibuffer = self.buffer.read(cx);
let snapshot = multibuffer.snapshot(cx);
let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
let ranges = snapshot.range_to_buffer_ranges(self.range.start..=self.range.end);
ranges
.first()
.and_then(|(buffer, _, _)| buffer.language())

View file

@ -1074,7 +1074,8 @@ impl InlineAssistant {
let language_name = assist.editor.upgrade().and_then(|editor| {
let multibuffer = editor.read(cx).buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
let ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
let ranges =
snapshot.range_to_buffer_ranges(assist.range.start..=assist.range.end);
ranges
.first()
.and_then(|(buffer, _, _)| buffer.language())

File diff suppressed because it is too large Load diff

View file

@ -329,7 +329,7 @@ impl RatePredictionsModal {
let update = diff.update_diff(
new_buffer_snapshot.text.clone(),
Some(old_buffer_snapshot.text().into()),
true,
Some(true),
language,
cx,
);

View file

@ -122,6 +122,7 @@ tree-sitter-typescript.workspace = true
tree-sitter-yaml.workspace = true
tree-sitter-bash.workspace = true
tree-sitter-md.workspace = true
unicode-width.workspace = true
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -37,6 +37,7 @@ mod rust_analyzer_ext;
pub mod scroll;
mod selections_collection;
mod split;
pub mod split_editor_view;
pub mod tasks;
#[cfg(test)]
@ -57,8 +58,8 @@ pub use editor_settings::{
HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap,
};
pub use element::{
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
render_breadcrumb_text,
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, OverlayPainter,
OverlayPainterData, PointForPosition, render_breadcrumb_text,
};
pub use git::blame::BlameRenderer;
pub use hover_popover::hover_markdown_style;
@ -71,7 +72,8 @@ pub use multi_buffer::{
MultiBufferOffset, MultiBufferOffsetUtf16, MultiBufferSnapshot, PathKey, RowInfo, ToOffset,
ToPoint,
};
pub use split::SplittableEditor;
pub use split::{SplittableEditor, ToggleLockedCursors, ToggleSplitDiff};
pub use split_editor_view::SplitEditorView;
pub use text::Bias;
use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
@ -1310,6 +1312,10 @@ pub struct Editor {
folding_newlines: Task<()>,
select_next_is_case_sensitive: Option<bool>,
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
scroll_companion: Option<WeakEntity<Editor>>,
on_local_selections_changed:
Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
suppress_selection_callback: bool,
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
accent_data: Option<AccentData>,
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
@ -1808,7 +1814,7 @@ pub(crate) struct FocusedBlock {
}
#[derive(Clone, Debug)]
enum JumpData {
pub enum JumpData {
MultiBufferRow {
row: MultiBufferRow,
line_offset_from_top: u32,
@ -2515,6 +2521,9 @@ impl Editor {
folding_newlines: Task::ready(()),
lookup_key: None,
select_next_is_case_sensitive: None,
scroll_companion: None,
on_local_selections_changed: None,
suppress_selection_callback: false,
applicable_language_settings: HashMap::default(),
accent_data: None,
fetched_tree_sitter_chunks: HashMap::default(),
@ -3522,6 +3531,14 @@ impl Editor {
}
self.blink_manager.update(cx, BlinkManager::pause_blinking);
if local && !self.suppress_selection_callback {
if let Some(callback) = self.on_local_selections_changed.as_ref() {
let cursor_position = self.selections.newest::<Point>(&display_map).head();
callback(cursor_position, window, cx);
}
}
cx.emit(EditorEvent::SelectionsChanged { local });
let selections = &self.selections.disjoint_anchors_arc();
@ -5544,7 +5561,7 @@ impl Editor {
Bias::Left,
);
multi_buffer_snapshot
.range_to_buffer_ranges(multi_buffer_visible_start..multi_buffer_visible_end)
.range_to_buffer_ranges(multi_buffer_visible_start..=multi_buffer_visible_end)
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
@ -7346,7 +7363,9 @@ impl Editor {
}
let match_task = cx.background_spawn(async move {
let buffer_ranges = multi_buffer_snapshot
.range_to_buffer_ranges(multi_buffer_range_to_query)
.range_to_buffer_ranges(
multi_buffer_range_to_query.start..=multi_buffer_range_to_query.end,
)
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty());
let mut match_ranges = Vec::new();
@ -8522,7 +8541,7 @@ impl Editor {
..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
for (buffer_snapshot, range, excerpt_id) in
multi_buffer_snapshot.range_to_buffer_ranges(range)
multi_buffer_snapshot.range_to_buffer_ranges(range.start..=range.end)
{
let Some(buffer) = project
.read(cx)
@ -18743,7 +18762,7 @@ impl Editor {
BTreeMap::new();
for selection_range in selection_ranges {
for (buffer, buffer_range, _) in
snapshot.range_to_buffer_ranges(selection_range)
snapshot.range_to_buffer_ranges(selection_range.start..=selection_range.end)
{
let buffer_id = buffer.remote_id();
let start = buffer.anchor_before(buffer_range.start);
@ -20779,7 +20798,6 @@ impl Editor {
pub fn set_soft_wrap_mode(
&mut self,
mode: language_settings::SoftWrap,
cx: &mut Context<Self>,
) {
self.soft_wrap_mode_override = Some(mode);
@ -21021,6 +21039,25 @@ impl Editor {
self.delegate_expand_excerpts = delegate;
}
pub fn set_scroll_companion(&mut self, companion: Option<WeakEntity<Editor>>) {
self.scroll_companion = companion;
}
pub fn scroll_companion(&self) -> Option<&WeakEntity<Editor>> {
self.scroll_companion.as_ref()
}
pub fn set_on_local_selections_changed(
&mut self,
callback: Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
) {
self.on_local_selections_changed = callback;
}
pub fn set_suppress_selection_callback(&mut self, suppress: bool) {
self.suppress_selection_callback = suppress;
}
pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
self.show_git_diff_gutter = Some(show_git_diff_gutter);
cx.notify();
@ -22524,7 +22561,8 @@ impl Editor {
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range);
let buffer_ranges = multi_buffer_snapshot
.range_to_buffer_ranges(selection_range.start..=selection_range.end);
let (buffer, range, _) = if selection.reversed {
buffer_ranges.first()
@ -22542,15 +22580,19 @@ impl Editor {
};
let buffer_diff_snapshot = buffer_diff.read(cx).snapshot(cx);
let (mut translated, _, _) = buffer_diff_snapshot.points_to_base_text_points(
[
Point::new(start_row_in_buffer, 0),
Point::new(end_row_in_buffer, 0),
],
buffer,
);
let start_row = translated.next().unwrap().start.row;
let end_row = translated.next().unwrap().end.row;
Some((
multi_buffer.buffer(buffer.remote_id()).unwrap(),
buffer_diff_snapshot.row_to_base_text_row(start_row_in_buffer, Bias::Left, buffer)
..buffer_diff_snapshot.row_to_base_text_row(
end_row_in_buffer,
Bias::Left,
buffer,
),
start_row..end_row,
))
});
@ -23839,7 +23881,7 @@ impl Editor {
self.open_excerpts_common(None, false, window, cx)
}
fn open_excerpts_common(
pub(crate) fn open_excerpts_common(
&mut self,
jump_data: Option<JumpData>,
split: bool,
@ -24331,6 +24373,7 @@ impl Editor {
modifiers: window.modifiers(),
},
&position_map,
None,
window,
cx,
);
@ -24794,7 +24837,7 @@ impl Editor {
self.active_diagnostics == ActiveDiagnostic::All || !self.mode().is_full()
}
fn create_style(&self, cx: &App) -> EditorStyle {
pub(crate) fn create_style(&self, cx: &App) -> EditorStyle {
let settings = ThemeSettings::get_global(cx);
let mut text_style = match self.mode {
@ -25480,7 +25523,10 @@ impl NewlineConfig {
buffer: &MultiBufferSnapshot,
range: Range<MultiBufferOffset>,
) -> bool {
let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
let (buffer, range) = match buffer
.range_to_buffer_ranges(range.start..=range.end)
.as_slice()
{
[(buffer, range, _)] => (*buffer, range.clone()),
_ => return false,
};

View file

@ -49,7 +49,8 @@ use gpui::{
Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
Size, StatefulInteractiveElement, Style, Styled, StyledText, TextAlign, TextRun,
TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop,
linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black,
linear_gradient, outline, pattern_slash, point, px, quad, relative, rgba, size,
solid_background, transparent_black,
};
use itertools::Itertools;
use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting};
@ -191,9 +192,29 @@ struct RenderBlocksOutput {
resized_blocks: Option<HashMap<CustomBlockId, u32>>,
}
/// Data passed to overlay painters during the paint phase.
pub struct OverlayPainterData<'a> {
pub editor: &'a Entity<Editor>,
pub snapshot: &'a EditorSnapshot,
pub scroll_position: gpui::Point<ScrollOffset>,
pub line_height: Pixels,
pub visible_row_range: Range<DisplayRow>,
pub hitbox: &'a Hitbox,
}
pub type OverlayPainter = Box<dyn FnOnce(OverlayPainterData<'_>, &mut Window, &mut App)>;
pub struct EditorElement {
editor: Entity<Editor>,
style: EditorStyle,
split_side: Option<SplitSide>,
overlay_painter: Option<OverlayPainter>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplitSide {
Left,
Right,
}
impl EditorElement {
@ -203,9 +224,23 @@ impl EditorElement {
Self {
editor: editor.clone(),
style,
split_side: None,
overlay_painter: None,
}
}
pub fn set_split_side(&mut self, side: SplitSide) {
self.split_side = Some(side);
}
pub fn set_overlay_painter(&mut self, painter: OverlayPainter) {
self.overlay_painter = Some(painter);
}
fn should_show_buffer_headers(&self) -> bool {
self.split_side.is_none()
}
fn register_actions(&self, window: &mut Window, cx: &mut App) {
let editor = &self.editor;
editor.update(cx, |editor, cx| {
@ -1201,6 +1236,7 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseMoveEvent,
position_map: &PositionMap,
split_side: Option<SplitSide>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@ -1328,7 +1364,10 @@ impl EditorElement {
indicator.is_active && indicator.display_row == valid_point.row()
});
let breakpoint_indicator = if gutter_hovered && !is_on_diff_review_button_row {
let breakpoint_indicator = if gutter_hovered
&& !is_on_diff_review_button_row
&& split_side != Some(SplitSide::Left)
{
let buffer_anchor = position_map
.snapshot
.display_point_to_anchor(valid_point, Bias::Left);
@ -1908,6 +1947,10 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Option<EditorScrollbars> {
if self.split_side == Some(SplitSide::Left) {
return None;
}
let show_scrollbars = self.editor.read(cx).show_scrollbars;
if (!show_scrollbars.horizontal && !show_scrollbars.vertical)
|| self.style.scrollbar_width.is_zero()
@ -2466,6 +2509,11 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
// Don't show code actions in split diff view
if self.split_side.is_some() {
return None;
}
if !snapshot
.show_code_actions
.unwrap_or(EditorSettings::get_global(cx).inline_code_actions)
@ -3045,6 +3093,10 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
if self.split_side == Some(SplitSide::Left) {
return Vec::new();
}
self.editor.update(cx, |editor, cx| {
breakpoints
.into_iter()
@ -3145,6 +3197,10 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
if self.split_side == Some(SplitSide::Left) {
return Vec::new();
}
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
// TODO: add edit button on the right side of each row in the context menu
@ -3848,18 +3904,18 @@ impl EditorElement {
height,
..
} => {
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
let result = v_flex().id(block_id).w_full().pr(editor_margins.right);
let mut result = v_flex().id(block_id).w_full().pr(editor_margins.right);
let jump_data = header_jump_data(
snapshot,
block_row_start,
*height,
first_excerpt,
latest_selection_anchors,
);
result
.child(self.render_buffer_header(
if self.should_show_buffer_headers() {
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
let jump_data = header_jump_data(
snapshot,
block_row_start,
*height,
first_excerpt,
latest_selection_anchors,
);
result = result.child(self.render_buffer_header(
first_excerpt,
true,
selected,
@ -3867,8 +3923,13 @@ impl EditorElement {
jump_data,
window,
cx,
))
.into_any_element()
));
} else {
result =
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
}
result.into_any_element()
}
Block::ExcerptBoundary { .. } => {
@ -3892,22 +3953,27 @@ impl EditorElement {
Block::BufferHeader { excerpt, height } => {
let mut result = v_flex().id(block_id).w_full();
let jump_data = header_jump_data(
snapshot,
block_row_start,
*height,
excerpt,
latest_selection_anchors,
);
if self.should_show_buffer_headers() {
let jump_data = header_jump_data(
snapshot,
block_row_start,
*height,
excerpt,
latest_selection_anchors,
);
if sticky_header_excerpt_id != Some(excerpt.id) {
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
if sticky_header_excerpt_id != Some(excerpt.id) {
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
result = result.child(div().pr(editor_margins.right).child(
self.render_buffer_header(
excerpt, false, selected, false, jump_data, window, cx,
),
));
result = result.child(div().pr(editor_margins.right).child(
self.render_buffer_header(
excerpt, false, selected, false, jump_data, window, cx,
),
));
} else {
result =
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
}
} else {
result =
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
@ -3915,6 +3981,13 @@ impl EditorElement {
result.into_any()
}
Block::Spacer { height, .. } => div()
.id(block_id)
.w_full()
.h((*height as f32) * line_height)
.bg(pattern_slash(rgba(0xFFFFFF10), 8.0, 8.0))
.into_any(),
};
// Discover the element's content height, then round up to the nearest multiple of line height.
@ -6613,7 +6686,7 @@ impl EditorElement {
GitGutterSetting::TrackedFiles
)
});
if show_git_gutter {
if show_git_gutter && self.split_side.is_none() {
Self::paint_gutter_diff_hunks(layout, window, cx)
}
@ -7949,6 +8022,7 @@ impl EditorElement {
window.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
let split_side = self.split_side;
move |event: &MouseMoveEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble {
@ -7962,7 +8036,7 @@ impl EditorElement {
Self::mouse_dragged(editor, event, &position_map, window, cx)
}
Self::mouse_moved(editor, event, &position_map, window, cx)
Self::mouse_moved(editor, event, &position_map, split_side, window, cx)
});
}
}
@ -8013,7 +8087,7 @@ impl EditorElement {
}
let buffer_snapshot = &display_snapshot.buffer_snapshot();
for (buffer, buffer_range, excerpt_id) in
buffer_snapshot.range_to_buffer_ranges(anchor_range)
buffer_snapshot.range_to_buffer_ranges(anchor_range.start..=anchor_range.end)
{
let buffer_range =
buffer.anchor_after(buffer_range.start)..buffer.anchor_before(buffer_range.end);
@ -8264,7 +8338,7 @@ fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
})
}
fn header_jump_data(
pub(crate) fn header_jump_data(
editor_snapshot: &EditorSnapshot,
block_row_start: DisplayRow,
height: u32,
@ -9528,6 +9602,38 @@ impl Element for EditorElement {
}
};
// When jumping from one side of a side-by-side diff to the
// other, we autoscroll autoscroll to keep the target range in view.
//
// If our scroll companion has a pending autoscroll request, process it
// first so that both editors render with synchronized scroll positions.
// This is important for split diff views where one editor may prepaint
// before the other.
if let Some(companion) = self
.editor
.read(cx)
.scroll_companion()
.and_then(|c| c.upgrade())
{
if companion.read(cx).scroll_manager.has_autoscroll_request() {
companion.update(cx, |companion_editor, cx| {
let companion_autoscroll_request =
companion_editor.scroll_manager.take_autoscroll_request();
companion_editor.autoscroll_vertically(
bounds,
line_height,
max_scroll_top,
companion_autoscroll_request,
window,
cx,
);
});
snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
}
}
let (
autoscroll_request,
autoscroll_containing_element,
@ -10070,23 +10176,27 @@ impl Element for EditorElement {
}
}
let sticky_buffer_header = sticky_header_excerpt.map(|sticky_header_excerpt| {
window.with_element_namespace("blocks", |window| {
self.layout_sticky_buffer_header(
sticky_header_excerpt,
scroll_position,
line_height,
right_margin,
&snapshot,
&hitbox,
&selected_buffer_ids,
&blocks,
&latest_selection_anchors,
window,
cx,
)
let sticky_buffer_header = if self.should_show_buffer_headers() {
sticky_header_excerpt.map(|sticky_header_excerpt| {
window.with_element_namespace("blocks", |window| {
self.layout_sticky_buffer_header(
sticky_header_excerpt,
scroll_position,
line_height,
right_margin,
&snapshot,
&hitbox,
&selected_buffer_ids,
&blocks,
&latest_selection_anchors,
window,
cx,
)
})
})
});
} else {
None
};
let start_buffer_row =
MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot()).row);
@ -10753,6 +10863,18 @@ impl Element for EditorElement {
self.paint_scrollbars(layout, window, cx);
self.paint_edit_prediction_popover(layout, window, cx);
self.paint_mouse_context_menu(layout, window, cx);
if let Some(overlay_painter) = self.overlay_painter.take() {
let data = OverlayPainterData {
editor: &self.editor,
snapshot: &layout.position_map.snapshot,
scroll_position: layout.position_map.snapshot.scroll_position(),
line_height: layout.position_map.line_height,
visible_row_range: layout.visible_display_row_range.clone(),
hitbox: &layout.hitbox,
};
overlay_painter(data, window, cx);
}
});
})
})
@ -11563,15 +11685,15 @@ impl PositionMap {
}
}
struct BlockLayout {
id: BlockId,
x_offset: Pixels,
row: Option<DisplayRow>,
element: AnyElement,
available_space: Size<AvailableSpace>,
style: BlockStyle,
overlaps_gutter: bool,
is_buffer_header: bool,
pub(crate) struct BlockLayout {
pub(crate) id: BlockId,
pub(crate) x_offset: Pixels,
pub(crate) row: Option<DisplayRow>,
pub(crate) element: AnyElement,
pub(crate) available_space: Size<AvailableSpace>,
pub(crate) style: BlockStyle,
pub(crate) overlaps_gutter: bool,
pub(crate) is_buffer_header: bool,
}
pub fn layout_line(

View file

@ -365,6 +365,10 @@ impl ScrollManager {
self.show_scrollbars
}
pub fn has_autoscroll_request(&self) -> bool {
self.autoscroll_request.is_some()
}
pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> {
self.autoscroll_request.take()
}
@ -588,14 +592,30 @@ impl Editor {
cx: &mut Context<Self>,
) -> WasScrolled {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.set_scroll_position_taking_display_map(
let was_scrolled = self.set_scroll_position_taking_display_map(
scroll_position,
local,
autoscroll,
map,
window,
cx,
)
);
if local && was_scrolled.0 {
if let Some(companion) = self.scroll_companion.as_ref().and_then(|c| c.upgrade()) {
companion.update(cx, |companion_editor, cx| {
companion_editor.set_scroll_position_internal(
scroll_position,
false,
false,
window,
cx,
);
});
}
}
was_scrolled
}
fn set_scroll_position_taking_display_map(

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -176,7 +176,15 @@ pub fn block_content_for_tests(
}
pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> String {
let draw_size = size(px(3000.0), px(3000.0));
editor_content_with_blocks_and_width(editor, px(3000.), cx)
}
pub fn editor_content_with_blocks_and_width(
editor: &Entity<Editor>,
width: Pixels,
cx: &mut VisualTestContext,
) -> String {
let draw_size = size(width, px(3000.0));
cx.simulate_resize(draw_size);
cx.draw(gpui::Point::default(), draw_size, |_, _| editor.clone());
let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| {
@ -224,7 +232,14 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
height,
} => {
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
format!("§ {}", first_excerpt.buffer.file().unwrap().file_name(cx))
format!(
"§ {}",
first_excerpt
.buffer
.file()
.map(|file| file.file_name(cx))
.unwrap_or("<no file>")
)
}));
for row in row.0 + 1..row.0 + height {
lines[row as usize].push_str("§ -----");
@ -236,15 +251,28 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
}
}
Block::BufferHeader { excerpt, height } => {
lines[row.0 as usize].push_str(
&cx.update(|_, cx| {
format!("§ {}", excerpt.buffer.file().unwrap().file_name(cx))
}),
);
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
format!(
"§ {}",
excerpt
.buffer
.file()
.map(|file| file.file_name(cx))
.unwrap_or("<no file>")
)
}));
for row in row.0 + 1..row.0 + height {
lines[row as usize].push_str("§ -----");
}
}
Block::Spacer { height, .. } => {
for row in row.0..row.0 + height {
while lines.len() <= row as usize {
lines.push(String::new());
}
lines[row as usize].push_str("§ spacer");
}
}
}
}
lines.join("\n")

View file

@ -884,7 +884,7 @@ async fn build_buffer_diff(
diff.update_diff(
buffer.text.clone(),
old_text.map(|old_text| Arc::from(old_text.as_str())),
true,
Some(true),
language.clone(),
cx,
)

View file

@ -178,7 +178,7 @@ async fn build_buffer_diff(
diff.update_diff(
new_buffer_snapshot.text.clone(),
Some(old_buffer_snapshot.text().into()),
true,
Some(true),
new_buffer_snapshot.language().cloned(),
cx,
)

View file

@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, anyhow};
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use collections::{HashMap, HashSet};
use editor::{
Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor,
Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor, ToggleSplitDiff,
actions::{GoToHunk, GoToPreviousHunk, SendReviewToAgent},
multibuffer_context_lines,
scroll::Autoscroll,
@ -477,6 +477,7 @@ impl ProjectDiff {
}
fn button_states(&self, cx: &App) -> ButtonStates {
let is_split = self.editor.read(cx).is_split();
let editor = self.editor.read(cx).primary_editor().read(cx);
let snapshot = self.multibuffer.read(cx).snapshot(cx);
let prev_next = snapshot.diff_hunks().nth(1).is_some();
@ -537,6 +538,7 @@ impl ProjectDiff {
selection,
stage_all,
unstage_all,
is_split,
}
}
@ -1293,6 +1295,7 @@ struct ButtonStates {
selection: bool,
stage_all: bool,
unstage_all: bool,
is_split: bool,
}
impl Render for ProjectDiffToolbar {
@ -1432,6 +1435,24 @@ impl Render for ProjectDiffToolbar {
)
},
)
.child(
Button::new(
"toggle-split",
if button_states.is_split {
"Stacked View"
} else {
"Split View"
},
)
.tooltip(Tooltip::for_action_title_in(
"Toggle Split View",
&ToggleSplitDiff,
&focus_handle,
))
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(&ToggleSplitDiff, window, cx);
})),
)
.child(
Button::new("commit", "Commit")
.tooltip(Tooltip::for_action_title_in(

View file

@ -268,7 +268,7 @@ async fn update_diff_buffer(
diff.update_diff(
source_buffer_snapshot.text.clone(),
Some(Arc::from(base_text.as_str())),
true,
Some(true),
language.clone(),
cx,
)

View file

@ -734,14 +734,14 @@ impl Default for Background {
}
/// Creates a hash pattern background
pub fn pattern_slash(color: Hsla, width: f32, interval: f32) -> Background {
pub fn pattern_slash(color: impl Into<Hsla>, width: f32, interval: f32) -> Background {
let width_scaled = (width * 255.0) as u32;
let interval_scaled = (interval * 255.0) as u32;
let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
Background {
tag: BackgroundTag::PatternSlash,
solid: color,
solid: color.into(),
gradient_angle_or_pattern_height: height,
..Default::default()
}

View file

@ -586,7 +586,7 @@ pub struct Chunk<'a> {
}
/// A set of edits to a given version of a buffer, computed asynchronously.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Diff {
pub base_version: clock::Global,
pub line_ending: LineEnding,
@ -2148,11 +2148,15 @@ impl Buffer {
/// Spawns a background task that asynchronously computes a `Diff` between the buffer's text
/// and the given new text.
pub fn diff(&self, mut new_text: String, cx: &App) -> Task<Diff> {
pub fn diff<T>(&self, new_text: T, cx: &App) -> Task<Diff>
where
T: AsRef<str> + Send + 'static,
{
let old_text = self.as_rope().clone();
let base_version = self.version();
cx.background_spawn(async move {
let old_text = old_text.to_string();
let mut new_text = new_text.as_ref().to_owned();
let line_ending = LineEnding::detect(&new_text);
LineEnding::normalize(&mut new_text);
let edits = text_diff(&old_text, &new_text);

View file

@ -259,7 +259,7 @@ impl SyntaxTreeView {
let multi_buffer = editor.buffer().read(cx);
let (buffer, range, excerpt_id) = snapshot
.buffer_snapshot()
.range_to_buffer_ranges(selection_range)
.range_to_buffer_ranges(selection_range.start..=selection_range.end)
.pop()?;
let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap();
Some((buffer, range, excerpt_id))

View file

@ -10,8 +10,8 @@ pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::{Result, anyhow};
use buffer_diff::{
BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunk, DiffHunkSecondaryStatus,
DiffHunkStatus, DiffHunkStatusKind,
BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffChanged, DiffHunk,
DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
@ -537,12 +537,19 @@ impl DiffState {
fn new(diff: Entity<BufferDiff>, cx: &mut Context<MultiBuffer>) -> Self {
DiffState {
_subscription: cx.subscribe(&diff, |this, diff, event, cx| match event {
BufferDiffEvent::DiffChanged {
BufferDiffEvent::DiffChanged(DiffChanged {
changed_range,
base_text_changed_range: _,
} => {
if let Some(changed_range) = changed_range.clone() {
this.buffer_diff_changed(diff, changed_range, cx)
extended_range,
}) => {
let use_extended = this.snapshot.borrow().use_extended_diff_range;
let range = if use_extended {
extended_range.clone()
} else {
changed_range.clone()
};
if let Some(range) = range {
this.buffer_diff_changed(diff, range, cx)
}
cx.emit(Event::BufferDiffChanged);
}
@ -564,10 +571,11 @@ impl DiffState {
_subscription: cx.subscribe(&diff, {
let main_buffer = main_buffer.clone();
move |this, diff, event, cx| match event {
BufferDiffEvent::DiffChanged {
BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: _,
base_text_changed_range,
} => {
extended_range: _,
}) => {
if let Some(base_text_changed_range) = base_text_changed_range.clone() {
this.inverted_buffer_diff_changed(
diff,
@ -609,6 +617,7 @@ pub struct MultiBufferSnapshot {
trailing_excerpt_update_count: usize,
all_diff_hunks_expanded: bool,
show_deleted_hunks: bool,
use_extended_diff_range: bool,
show_headers: bool,
}
@ -1903,6 +1912,7 @@ impl MultiBuffer {
trailing_excerpt_update_count,
all_diff_hunks_expanded: _,
show_deleted_hunks: _,
use_extended_diff_range: _,
show_headers: _,
} = self.snapshot.get_mut();
let start = ExcerptDimension(MultiBufferOffset::ZERO);
@ -2367,7 +2377,6 @@ impl MultiBuffer {
.get(&buffer_id)
.is_none_or(|old_diff| !new_diff.base_texts_definitely_eq(old_diff));
snapshot.diffs.insert_or_replace(buffer_id, new_diff);
self.buffer_changed_since_sync.replace(true);
let buffer = buffer_state.buffer.read(cx);
let diff_change_range = range.to_offset(buffer);
@ -2648,6 +2657,10 @@ impl MultiBuffer {
self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx);
}
pub fn set_use_extended_diff_range(&mut self, use_extended: bool, _cx: &mut Context<Self>) {
self.snapshot.get_mut().use_extended_diff_range = use_extended;
}
pub fn has_multiple_hunks(&self, cx: &App) -> bool {
self.read(cx)
.diff_hunks_in_range(Anchor::min()..Anchor::max())
@ -2988,6 +3001,7 @@ impl MultiBuffer {
trailing_excerpt_update_count: _,
all_diff_hunks_expanded: _,
show_deleted_hunks: _,
use_extended_diff_range: _,
show_headers: _,
} = snapshot;
*is_dirty = false;
@ -3117,6 +3131,9 @@ impl MultiBuffer {
&& let Some((old_diff, old_main_buffer, new_main_buffer)) =
inverted_diff_touch_info.get(locator)
{
// TODO(split-diff) this iterates over all excerpts for all edited buffers;
// it would be nice to be able to skip excerpts that weren't edited using
// new_main_buffer.has_edits_since_in_range.
let excerpt_buffer_start = old_excerpt
.range
.context
@ -3124,13 +3141,27 @@ impl MultiBuffer {
.to_offset(&old_excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + old_excerpt.text_summary.len;
for hunk in old_diff.hunks_intersecting_base_text_range(
excerpt_buffer_start..excerpt_buffer_end,
old_main_buffer,
) {
if hunk.buffer_range.start.is_valid(new_main_buffer) {
continue;
}
let mut hunks = old_diff
.hunks_intersecting_base_text_range(
excerpt_buffer_start..excerpt_buffer_end,
old_main_buffer,
)
.chain(old_diff.hunks_intersecting_base_text_range(
excerpt_buffer_start..excerpt_buffer_end,
new_main_buffer,
))
.filter(|hunk| {
hunk.buffer_range.start.is_valid(&old_main_buffer)
!= hunk.buffer_range.start.is_valid(&new_main_buffer)
})
.collect::<Vec<_>>();
hunks.sort_by(|l, r| {
l.diff_base_byte_range
.start
.cmp(&r.diff_base_byte_range.start)
});
for hunk in hunks {
let hunk_buffer_start = hunk.diff_base_byte_range.start;
if hunk_buffer_start >= excerpt_buffer_start
&& hunk_buffer_start <= excerpt_buffer_end
@ -3406,6 +3437,7 @@ impl MultiBuffer {
edit_buffer_start..edit_buffer_end,
main_buffer,
) {
did_expand_hunks = true;
let hunk_buffer_range = hunk.diff_base_byte_range.clone();
if hunk_buffer_range.start < excerpt_buffer_start {
log::trace!("skipping hunk that starts before excerpt");
@ -4081,27 +4113,52 @@ impl MultiBufferSnapshot {
&self,
ranges: impl Iterator<Item = Range<T>>,
) -> impl Iterator<Item = (&BufferSnapshot, Range<BufferOffset>, ExcerptId)> {
ranges.flat_map(|range| self.range_to_buffer_ranges(range).into_iter())
ranges.flat_map(|range| {
self.range_to_buffer_ranges((Bound::Included(range.start), Bound::Included(range.end)))
.into_iter()
})
}
pub fn range_to_buffer_ranges<T: ToOffset>(
pub fn range_to_buffer_ranges<R, T>(
&self,
range: Range<T>,
) -> Vec<(&BufferSnapshot, Range<BufferOffset>, ExcerptId)> {
let start = range.start.to_offset(self);
let end = range.end.to_offset(self);
range: R,
) -> Vec<(&BufferSnapshot, Range<BufferOffset>, ExcerptId)>
where
R: RangeBounds<T>,
T: ToOffset,
{
let start = match range.start_bound() {
Bound::Included(start) => start.to_offset(self),
Bound::Excluded(_) => panic!("excluded start bound not supported"),
Bound::Unbounded => MultiBufferOffset::ZERO,
};
let end_bound = match range.end_bound() {
Bound::Included(end) => Bound::Included(end.to_offset(self)),
Bound::Excluded(end) => Bound::Excluded(end.to_offset(self)),
Bound::Unbounded => Bound::Unbounded,
};
let bounds = (Bound::Included(start), end_bound);
let mut cursor = self.cursor::<MultiBufferOffset, BufferOffset>();
cursor.seek(&start);
let mut result: Vec<(&BufferSnapshot, Range<BufferOffset>, ExcerptId)> = Vec::new();
while let Some(region) = cursor.region() {
if region.range.start > end {
let dominated_by_end_bound = match end_bound {
Bound::Included(end) => region.range.start > end,
Bound::Excluded(end) => region.range.start >= end,
Bound::Unbounded => false,
};
if dominated_by_end_bound {
break;
}
if region.is_main_buffer {
let start_overshoot = start.saturating_sub(region.range.start);
let end_overshoot = end.saturating_sub(region.range.start);
let end_offset = match end_bound {
Bound::Included(end) | Bound::Excluded(end) => end,
Bound::Unbounded => region.range.end,
};
let end_overshoot = end_offset.saturating_sub(region.range.start);
let start = region
.buffer_range
.end
@ -4120,6 +4177,20 @@ impl MultiBufferSnapshot {
}
cursor.next();
}
if let Some(excerpt) = cursor.excerpt() {
let dominated_by_prev_excerpt =
result.last().is_some_and(|(_, _, id)| *id == excerpt.id);
if !dominated_by_prev_excerpt && excerpt.text_summary.len == 0 {
let excerpt_position = self.len();
if bounds.contains(&excerpt_position) {
let buffer_offset =
BufferOffset(excerpt.range.context.start.to_offset(&excerpt.buffer));
result.push((&excerpt.buffer, buffer_offset..buffer_offset, excerpt.id));
}
}
}
result
}
@ -6740,6 +6811,15 @@ impl MultiBufferSnapshot {
self.diffs.get(&buffer_id).map(|diff| &diff.diff)
}
/// For inverted diffs (used in side-by-side diff view), returns the main buffer
/// snapshot that the diff's anchors refer to. Returns `None` if the diff is not
/// inverted or if there's no diff for the given buffer ID.
pub fn inverted_diff_main_buffer(&self, buffer_id: BufferId) -> Option<&text::BufferSnapshot> {
self.diffs
.get(&buffer_id)
.and_then(|diff| diff.main_buffer.as_ref())
}
/// Visually annotates a position or range with the `Debug` representation of a value. The
/// callsite of this function is used as a key - previous annotations will be removed.
#[cfg(debug_assertions)]
@ -6767,11 +6847,11 @@ impl MultiBufferSnapshot {
.to_multi_buffer_debug_ranges(self)
.into_iter()
.flat_map(|range| {
self.range_to_buffer_ranges(range).into_iter().map(
|(buffer, range, _excerpt_id)| {
self.range_to_buffer_ranges(range.start..=range.end)
.into_iter()
.map(|(buffer, range, _excerpt_id)| {
buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
},
)
})
})
.collect();
text::debug::GlobalDebugRanges::with_locked(|debug_ranges| {

View file

@ -4012,7 +4012,7 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
diff.update_diff(
buffer.read(cx).text_snapshot(),
Some(base_text.into()),
false,
None,
None,
cx,
)
@ -4050,7 +4050,7 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
diff.update_diff(
buffer.read(cx).text_snapshot(),
Some(base_text.into()),
false,
None,
None,
cx,
)
@ -4890,3 +4890,122 @@ fn test_excerpts_containment_functions(cx: &mut App) {
"excerpt_containing should return None for ranges spanning multiple excerpts"
);
}
#[gpui::test]
fn test_range_to_buffer_ranges_with_range_bounds(cx: &mut App) {
use std::ops::Bound;
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb", cx));
let buffer_2 = cx.new(|cx| Buffer::local("ccc", cx));
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let (excerpt_1_id, excerpt_2_id) = multibuffer.update(cx, |multibuffer, cx| {
let excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))],
cx,
)[0];
let excerpt_2_id = multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(0, 3))],
cx,
)[0];
(excerpt_1_id, excerpt_2_id)
});
let snapshot = multibuffer.read(cx).snapshot(cx);
assert_eq!(snapshot.text(), "aaa\nbbb\nccc");
let excerpt_2_start = Point::new(2, 0);
let ranges_half_open = snapshot.range_to_buffer_ranges(Point::zero()..excerpt_2_start);
assert_eq!(
ranges_half_open.len(),
1,
"Half-open range ending at excerpt start should EXCLUDE that excerpt"
);
assert_eq!(ranges_half_open[0].2, excerpt_1_id);
let ranges_inclusive = snapshot.range_to_buffer_ranges(Point::zero()..=excerpt_2_start);
assert_eq!(
ranges_inclusive.len(),
2,
"Inclusive range ending at excerpt start should INCLUDE that excerpt"
);
assert_eq!(ranges_inclusive[0].2, excerpt_1_id);
assert_eq!(ranges_inclusive[1].2, excerpt_2_id);
let ranges_unbounded =
snapshot.range_to_buffer_ranges((Bound::Included(Point::zero()), Bound::Unbounded));
assert_eq!(
ranges_unbounded.len(),
2,
"Unbounded end should include all excerpts"
);
assert_eq!(ranges_unbounded[0].2, excerpt_1_id);
assert_eq!(ranges_unbounded[1].2, excerpt_2_id);
let ranges_excluded_end = snapshot.range_to_buffer_ranges((
Bound::Included(Point::zero()),
Bound::Excluded(excerpt_2_start),
));
assert_eq!(
ranges_excluded_end.len(),
1,
"Excluded end bound should exclude excerpt starting at that point"
);
assert_eq!(ranges_excluded_end[0].2, excerpt_1_id);
let buffer_empty = cx.new(|cx| Buffer::local("", cx));
let multibuffer_trailing_empty = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let (te_excerpt_1_id, te_excerpt_2_id) =
multibuffer_trailing_empty.update(cx, |multibuffer, cx| {
let excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))],
cx,
)[0];
let excerpt_2_id = multibuffer.push_excerpts(
buffer_empty.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(0, 0))],
cx,
)[0];
(excerpt_1_id, excerpt_2_id)
});
let snapshot_trailing = multibuffer_trailing_empty.read(cx).snapshot(cx);
assert_eq!(snapshot_trailing.text(), "aaa\nbbb\n");
let max_point = snapshot_trailing.max_point();
let ranges_half_open_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..max_point);
assert_eq!(
ranges_half_open_max.len(),
1,
"Half-open range to max_point should EXCLUDE trailing empty excerpt at max_point"
);
assert_eq!(ranges_half_open_max[0].2, te_excerpt_1_id);
let ranges_inclusive_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..=max_point);
assert_eq!(
ranges_inclusive_max.len(),
2,
"Inclusive range to max_point should INCLUDE trailing empty excerpt"
);
assert_eq!(ranges_inclusive_max[0].2, te_excerpt_1_id);
assert_eq!(ranges_inclusive_max[1].2, te_excerpt_2_id);
let ranges_unbounded_trailing = snapshot_trailing
.range_to_buffer_ranges((Bound::Included(Point::zero()), Bound::Unbounded));
assert_eq!(
ranges_unbounded_trailing.len(),
2,
"Unbounded end should include trailing empty excerpt"
);
assert_eq!(ranges_unbounded_trailing[0].2, te_excerpt_1_id);
assert_eq!(ranges_unbounded_trailing[1].2, te_excerpt_2_id);
}

View file

@ -3115,7 +3115,7 @@ impl BufferGitState {
unstaged_diff.read(cx).update_diff(
buffer.clone(),
index,
index_changed,
index_changed.then_some(false),
language.clone(),
cx,
)
@ -3138,7 +3138,7 @@ impl BufferGitState {
uncommitted_diff.read(cx).update_diff(
buffer.clone(),
head,
head_changed,
head_changed.then_some(true),
language.clone(),
cx,
)

View file

@ -9,7 +9,8 @@ use crate::{
};
use async_trait::async_trait;
use buffer_diff::{
BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks,
BufferDiffEvent, DiffChanged, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
assert_hunks,
};
use fs::FakeFs;
use futures::{StreamExt, future};
@ -8159,10 +8160,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
BufferDiffEvent::HunksStagedOrUnstaged(_)
));
let event = diff_events.next().await.unwrap();
if let BufferDiffEvent::DiffChanged {
if let BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: Some(changed_range),
base_text_changed_range: _,
} = event
extended_range: _,
}) = event
{
let changed_range = changed_range.to_point(&snapshot);
assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
@ -8202,10 +8204,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
// The diff emits a change event for the changed index text.
let event = diff_events.next().await.unwrap();
if let BufferDiffEvent::DiffChanged {
if let BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: Some(changed_range),
base_text_changed_range: _,
} = event
extended_range: _,
}) = event
{
let changed_range = changed_range.to_point(&snapshot);
assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
@ -8260,10 +8263,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
BufferDiffEvent::HunksStagedOrUnstaged(_)
));
let event = diff_events.next().await.unwrap();
if let BufferDiffEvent::DiffChanged {
if let BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: Some(changed_range),
base_text_changed_range: _,
} = event
extended_range: _,
}) = event
{
let changed_range = changed_range.to_point(&snapshot);
assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
@ -8302,10 +8306,11 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
});
let event = diff_events.next().await.unwrap();
if let BufferDiffEvent::DiffChanged {
if let BufferDiffEvent::DiffChanged(DiffChanged {
changed_range: Some(changed_range),
base_text_changed_range: _,
} = event
extended_range: _,
}) = event
{
let changed_range = changed_range.to_point(&snapshot);
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));

View file

@ -72,7 +72,7 @@ where
}
}
fn reset(&mut self) {
pub fn reset(&mut self) {
self.did_seek = false;
self.at_end = self.tree.is_empty();
self.stack.truncate(0);