diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a32910e78a..316d945ca4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -71,7 +71,6 @@ pub use element::{ use futures::{future, FutureExt}; use fuzzy::{StringMatch, StringMatchCandidate}; use git::blame::GitBlame; -use git::diff_hunk_to_display; use gpui::{ div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry, @@ -84,8 +83,8 @@ use gpui::{ }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; -use hunk_diff::ExpandedHunks; pub(crate) use hunk_diff::HoveredHunk; +use hunk_diff::{diff_hunk_to_display, ExpandedHunks}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion_provider::*; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 31e4efb83b..cf8edb67dc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7,14 +7,11 @@ use crate::{ CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine, ShowScrollbar, }, - git::{ - blame::{CommitDetails, GitBlame}, - diff_hunk_to_display, DisplayDiffHunk, - }, + git::blame::{CommitDetails, GitBlame}, hover_popover::{ self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, - hunk_diff::ExpandedHunk, + hunk_diff::{diff_hunk_to_display, DisplayDiffHunk, ExpandedHunk}, hunk_status, items::BufferSearchHighlights, mouse_context_menu::{self, MenuPosition, MouseContextMenu}, diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index fb18ca45a2..080babe4c6 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -1,309 +1 @@ pub mod blame; - -use std::ops::Range; - -use git::diff::DiffHunkStatus; -use language::Point; -use multi_buffer::{Anchor, MultiBufferDiffHunk}; - -use crate::{ - display_map::{DisplaySnapshot, ToDisplayPoint}, - hunk_status, AnchorRangeExt, DisplayRow, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DisplayDiffHunk { - Folded { - display_row: DisplayRow, - }, - - Unfolded { - diff_base_byte_range: Range, - display_row_range: Range, - multi_buffer_range: Range, - status: DiffHunkStatus, - }, -} - -impl DisplayDiffHunk { - pub fn start_display_row(&self) -> DisplayRow { - match self { - &DisplayDiffHunk::Folded { display_row } => display_row, - DisplayDiffHunk::Unfolded { - display_row_range, .. - } => display_row_range.start, - } - } - - pub fn contains_display_row(&self, display_row: DisplayRow) -> bool { - let range = match self { - &DisplayDiffHunk::Folded { display_row } => display_row..=display_row, - - DisplayDiffHunk::Unfolded { - display_row_range, .. - } => display_row_range.start..=display_row_range.end, - }; - - range.contains(&display_row) - } -} - -pub fn diff_hunk_to_display( - hunk: &MultiBufferDiffHunk, - snapshot: &DisplaySnapshot, -) -> DisplayDiffHunk { - let hunk_start_point = Point::new(hunk.row_range.start.0, 0); - let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0); - let hunk_end_point_sub = Point::new( - hunk.row_range - .end - .0 - .saturating_sub(1) - .max(hunk.row_range.start.0), - 0, - ); - - let status = hunk_status(hunk); - let is_removal = status == DiffHunkStatus::Removed; - - let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0); - let folds_end = Point::new(hunk.row_range.end.0 + 2, 0); - let folds_range = folds_start..folds_end; - - let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| { - let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot); - let fold_point_range = fold_point_range.start..=fold_point_range.end; - - let folded_start = fold_point_range.contains(&hunk_start_point); - let folded_end = fold_point_range.contains(&hunk_end_point_sub); - let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub); - - (folded_start && folded_end) || (is_removal && folded_start_sub) - }); - - if let Some(fold) = containing_fold { - let row = fold.range.start.to_display_point(snapshot).row(); - DisplayDiffHunk::Folded { display_row: row } - } else { - let start = hunk_start_point.to_display_point(snapshot).row(); - - let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start); - let hunk_end_point = Point::new(hunk_end_row.0, 0); - - let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point); - let multi_buffer_end = snapshot.buffer_snapshot.anchor_after(hunk_end_point); - let end = hunk_end_point.to_display_point(snapshot).row(); - - DisplayDiffHunk::Unfolded { - display_row_range: start..end, - multi_buffer_range: multi_buffer_start..multi_buffer_end, - status, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - } - } -} - -#[cfg(test)] -mod tests { - use crate::Point; - use crate::{editor_tests::init_test, hunk_status}; - use gpui::{Context, TestAppContext}; - use language::Capability::ReadWrite; - use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow}; - use project::{FakeFs, Project}; - use unindent::Unindent; - #[gpui::test] - async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { - use git::diff::DiffHunkStatus; - init_test(cx, |_| {}); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - - // buffer has two modified hunks with two rows each - let buffer_1 = project.update(cx, |project, cx| { - project.create_local_buffer( - " - 1.zero - 1.ONE - 1.TWO - 1.three - 1.FOUR - 1.FIVE - 1.six - " - .unindent() - .as_str(), - None, - cx, - ) - }); - buffer_1.update(cx, |buffer, cx| { - buffer.set_diff_base( - Some( - " - 1.zero - 1.one - 1.two - 1.three - 1.four - 1.five - 1.six - " - .unindent(), - ), - cx, - ); - }); - - // buffer has a deletion hunk and an insertion hunk - let buffer_2 = project.update(cx, |project, cx| { - project.create_local_buffer( - " - 2.zero - 2.one - 2.two - 2.three - 2.four - 2.five - 2.six - " - .unindent() - .as_str(), - None, - cx, - ) - }); - buffer_2.update(cx, |buffer, cx| { - buffer.set_diff_base( - Some( - " - 2.zero - 2.one - 2.one-and-a-half - 2.two - 2.three - 2.four - 2.six - " - .unindent(), - ), - cx, - ); - }); - - cx.background_executor.run_until_parked(); - - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(ReadWrite); - multibuffer.push_excerpts( - buffer_1.clone(), - [ - // excerpt ends in the middle of a modified hunk - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 5), - primary: Default::default(), - }, - // excerpt begins in the middle of a modified hunk - ExcerptRange { - context: Point::new(5, 0)..Point::new(6, 5), - primary: Default::default(), - }, - ], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ - // excerpt ends at a deletion - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 5), - primary: Default::default(), - }, - // excerpt starts at a deletion - ExcerptRange { - context: Point::new(2, 0)..Point::new(2, 5), - primary: Default::default(), - }, - // excerpt fully contains a deletion hunk - ExcerptRange { - context: Point::new(1, 0)..Point::new(2, 5), - primary: Default::default(), - }, - // excerpt fully contains an insertion hunk - ExcerptRange { - context: Point::new(4, 0)..Point::new(6, 5), - primary: Default::default(), - }, - ], - cx, - ); - multibuffer - }); - - let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); - - assert_eq!( - snapshot.text(), - " - 1.zero - 1.ONE - 1.FIVE - 1.six - 2.zero - 2.one - 2.two - 2.one - 2.two - 2.four - 2.five - 2.six" - .unindent() - ); - - let expected = [ - ( - DiffHunkStatus::Modified, - MultiBufferRow(1)..MultiBufferRow(2), - ), - ( - DiffHunkStatus::Modified, - MultiBufferRow(2)..MultiBufferRow(3), - ), - //TODO: Define better when and where removed hunks show up at range extremities - ( - DiffHunkStatus::Removed, - MultiBufferRow(6)..MultiBufferRow(6), - ), - ( - DiffHunkStatus::Removed, - MultiBufferRow(8)..MultiBufferRow(8), - ), - ( - DiffHunkStatus::Added, - MultiBufferRow(10)..MultiBufferRow(11), - ), - ]; - - assert_eq!( - snapshot - .git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12)) - .map(|hunk| (hunk_status(&hunk), hunk.row_range)) - .collect::>(), - &expected, - ); - - assert_eq!( - snapshot - .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12)) - .map(|hunk| (hunk_status(&hunk), hunk.row_range)) - .collect::>(), - expected - .iter() - .rev() - .cloned() - .collect::>() - .as_slice(), - ); - } -} diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 2f7bb49e85..67e8a25df5 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -1,18 +1,16 @@ -use std::{ - ops::{Range, RangeInclusive}, - sync::Arc, -}; - use collections::{hash_map, HashMap, HashSet}; use git::diff::DiffHunkStatus; use gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View}; -use language::Buffer; +use language::{Buffer, BufferId, Point}; use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, MultiBufferSnapshot, ToPoint, }; use settings::SettingsStore; -use text::{BufferId, Point}; +use std::{ + ops::{Range, RangeInclusive}, + sync::Arc, +}; use ui::{ prelude::*, ActiveTheme, ContextMenu, InteractiveElement, IntoElement, ParentElement, Pixels, Styled, ViewContext, VisualContext, @@ -20,13 +18,11 @@ use ui::{ use util::{debug_panic, RangeExt}; use crate::{ - editor_settings::CurrentLineHighlight, - git::{diff_hunk_to_display, DisplayDiffHunk}, - hunk_status, hunks_for_selections, - mouse_context_menu::MouseContextMenu, - BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, Editor, - EditorElement, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertFile, - RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, + editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, + mouse_context_menu::MouseContextMenu, BlockDisposition, BlockProperties, BlockStyle, + CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, Editor, EditorElement, + EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertFile, RevertSelectedHunks, + ToDisplayPoint, ToggleHunkDiff, }; #[derive(Debug, Clone)] @@ -43,12 +39,35 @@ pub(super) struct ExpandedHunks { hunk_update_tasks: HashMap, Task<()>>, } +#[derive(Debug, Clone)] +pub(super) struct ExpandedHunk { + pub block: Option, + pub hunk_range: Range, + pub diff_base_byte_range: Range, + pub status: DiffHunkStatus, + pub folded: bool, +} + #[derive(Debug)] struct DiffBaseBuffer { buffer: Model, diff_base_version: usize, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DisplayDiffHunk { + Folded { + display_row: DisplayRow, + }, + + Unfolded { + diff_base_byte_range: Range, + display_row_range: Range, + multi_buffer_range: Range, + status: DiffHunkStatus, + }, +} + impl ExpandedHunks { pub fn hunks(&self, include_folded: bool) -> impl Iterator { self.hunks @@ -57,15 +76,6 @@ impl ExpandedHunks { } } -#[derive(Debug, Clone)] -pub(super) struct ExpandedHunk { - pub block: Option, - pub hunk_range: Range, - pub diff_base_byte_range: Range, - pub status: DiffHunkStatus, - pub folded: bool, -} - impl Editor { pub(super) fn open_hunk_context_menu( &mut self, @@ -883,3 +893,287 @@ fn to_inclusive_row_range( let new_range = point_range.to_anchors(&snapshot.buffer_snapshot); new_range.start..=new_range.end } + +impl DisplayDiffHunk { + pub fn start_display_row(&self) -> DisplayRow { + match self { + &DisplayDiffHunk::Folded { display_row } => display_row, + DisplayDiffHunk::Unfolded { + display_row_range, .. + } => display_row_range.start, + } + } + + pub fn contains_display_row(&self, display_row: DisplayRow) -> bool { + let range = match self { + &DisplayDiffHunk::Folded { display_row } => display_row..=display_row, + + DisplayDiffHunk::Unfolded { + display_row_range, .. + } => display_row_range.start..=display_row_range.end, + }; + + range.contains(&display_row) + } +} + +pub fn diff_hunk_to_display( + hunk: &MultiBufferDiffHunk, + snapshot: &DisplaySnapshot, +) -> DisplayDiffHunk { + let hunk_start_point = Point::new(hunk.row_range.start.0, 0); + let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0); + let hunk_end_point_sub = Point::new( + hunk.row_range + .end + .0 + .saturating_sub(1) + .max(hunk.row_range.start.0), + 0, + ); + + let status = hunk_status(hunk); + let is_removal = status == DiffHunkStatus::Removed; + + let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0); + let folds_end = Point::new(hunk.row_range.end.0 + 2, 0); + let folds_range = folds_start..folds_end; + + let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| { + let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot); + let fold_point_range = fold_point_range.start..=fold_point_range.end; + + let folded_start = fold_point_range.contains(&hunk_start_point); + let folded_end = fold_point_range.contains(&hunk_end_point_sub); + let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub); + + (folded_start && folded_end) || (is_removal && folded_start_sub) + }); + + if let Some(fold) = containing_fold { + let row = fold.range.start.to_display_point(snapshot).row(); + DisplayDiffHunk::Folded { display_row: row } + } else { + let start = hunk_start_point.to_display_point(snapshot).row(); + + let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start); + let hunk_end_point = Point::new(hunk_end_row.0, 0); + + let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point); + let multi_buffer_end = snapshot.buffer_snapshot.anchor_after(hunk_end_point); + let end = hunk_end_point.to_display_point(snapshot).row(); + + DisplayDiffHunk::Unfolded { + display_row_range: start..end, + multi_buffer_range: multi_buffer_start..multi_buffer_end, + status, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{editor_tests::init_test, hunk_status}; + use gpui::{Context, TestAppContext}; + use language::Capability::ReadWrite; + use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow}; + use project::{FakeFs, Project}; + use unindent::Unindent as _; + + #[gpui::test] + async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { + use git::diff::DiffHunkStatus; + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.background_executor.clone()); + let project = Project::test(fs, [], cx).await; + + // buffer has two modified hunks with two rows each + let buffer_1 = project.update(cx, |project, cx| { + project.create_local_buffer( + " + 1.zero + 1.ONE + 1.TWO + 1.three + 1.FOUR + 1.FIVE + 1.six + " + .unindent() + .as_str(), + None, + cx, + ) + }); + buffer_1.update(cx, |buffer, cx| { + buffer.set_diff_base( + Some( + " + 1.zero + 1.one + 1.two + 1.three + 1.four + 1.five + 1.six + " + .unindent(), + ), + cx, + ); + }); + + // buffer has a deletion hunk and an insertion hunk + let buffer_2 = project.update(cx, |project, cx| { + project.create_local_buffer( + " + 2.zero + 2.one + 2.two + 2.three + 2.four + 2.five + 2.six + " + .unindent() + .as_str(), + None, + cx, + ) + }); + buffer_2.update(cx, |buffer, cx| { + buffer.set_diff_base( + Some( + " + 2.zero + 2.one + 2.one-and-a-half + 2.two + 2.three + 2.four + 2.six + " + .unindent(), + ), + cx, + ); + }); + + cx.background_executor.run_until_parked(); + + let multibuffer = cx.new_model(|cx| { + let mut multibuffer = MultiBuffer::new(ReadWrite); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + // excerpt ends in the middle of a modified hunk + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt begins in the middle of a modified hunk + ExcerptRange { + context: Point::new(5, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + // excerpt ends at a deletion + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt starts at a deletion + ExcerptRange { + context: Point::new(2, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains a deletion hunk + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains an insertion hunk + ExcerptRange { + context: Point::new(4, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer + }); + + let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); + + assert_eq!( + snapshot.text(), + " + 1.zero + 1.ONE + 1.FIVE + 1.six + 2.zero + 2.one + 2.two + 2.one + 2.two + 2.four + 2.five + 2.six" + .unindent() + ); + + let expected = [ + ( + DiffHunkStatus::Modified, + MultiBufferRow(1)..MultiBufferRow(2), + ), + ( + DiffHunkStatus::Modified, + MultiBufferRow(2)..MultiBufferRow(3), + ), + //TODO: Define better when and where removed hunks show up at range extremities + ( + DiffHunkStatus::Removed, + MultiBufferRow(6)..MultiBufferRow(6), + ), + ( + DiffHunkStatus::Removed, + MultiBufferRow(8)..MultiBufferRow(8), + ), + ( + DiffHunkStatus::Added, + MultiBufferRow(10)..MultiBufferRow(11), + ), + ]; + + assert_eq!( + snapshot + .git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12)) + .map(|hunk| (hunk_status(&hunk), hunk.row_range)) + .collect::>(), + &expected, + ); + + assert_eq!( + snapshot + .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12)) + .map(|hunk| (hunk_status(&hunk), hunk.row_range)) + .collect::>(), + expected + .iter() + .rev() + .cloned() + .collect::>() + .as_slice(), + ); + } +}