diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 6e9a2dc406..ca9ca0e867 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -116,7 +116,7 @@ // "alt-v": ["editor::MovePageUp", { "center_cursor": true }], "ctrl-alt-space": "editor::ShowCharacterPalette", "ctrl-;": "editor::ToggleLineNumbers", - "ctrl-k ctrl-r": "editor::RevertSelectedHunks", + "ctrl-k ctrl-r": "git::Restore", "ctrl-'": "editor::ToggleSelectedDiffHunks", "ctrl-\"": "editor::ExpandAllHunkDiffs", "ctrl-i": "editor::ShowSignatureHelp", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 5d52deea67..5b0995084b 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -126,7 +126,10 @@ "ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }], "ctrl-cmd-space": "editor::ShowCharacterPalette", "cmd-;": "editor::ToggleLineNumbers", - "cmd-alt-z": "editor::RevertSelectedHunks", + "cmd-alt-z": "git::Restore", + "cmd-alt-y": "git::ToggleStaged", + "cmd-y": "git::StageAndNext", + "cmd-shift-y": "git::UnstageAndNext", "cmd-'": "editor::ToggleSelectedDiffHunks", "cmd-\"": "editor::ExpandAllHunkDiffs", "cmd-alt-g b": "editor::ToggleGitBlame", diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index 618ca04488..cc0f8ad332 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -44,7 +44,7 @@ "shift-f2": "editor::GoToPrevDiagnostic", "ctrl-alt-shift-down": "editor::GoToHunk", "ctrl-alt-shift-up": "editor::GoToPrevHunk", - "ctrl-alt-z": "editor::RevertSelectedHunks", + "ctrl-alt-z": "git::Restore", "ctrl-home": "editor::MoveToBeginning", "ctrl-end": "editor::MoveToEnd", "ctrl-shift-home": "editor::SelectToBeginning", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1f2e6b26e3..211cbcf926 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -446,7 +446,7 @@ "d": "vim::CurrentLine", "s": "vim::PushDeleteSurrounds", "o": "editor::ToggleSelectedDiffHunks", // "d o" - "p": "editor::RevertSelectedHunks" // "d p" + "p": "git::Restore" // "d p" } }, { diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 3efa89d469..71cffc315b 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -357,7 +357,6 @@ gpui::actions!( ReverseLines, RevertFile, ReloadFile, - RevertSelectedHunks, Rewrap, ScrollCursorBottom, ScrollCursorCenter, @@ -400,7 +399,6 @@ gpui::actions!( ToggleInlayHints, ToggleEditPrediction, ToggleLineNumbers, - ToggleStagedSelectedDiffHunks, SwapSelectionEnds, SetMark, ToggleRelativeLineNumbers, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e30b369bd3..c0f30fb4b0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -73,6 +73,7 @@ use futures::{ }; use fuzzy::StringMatchCandidate; +use ::git::Restore; use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, CompletionsMenu, ContextMenuOrigin, @@ -7025,21 +7026,6 @@ impl Editor { }) } - pub fn revert_file(&mut self, _: &RevertFile, window: &mut Window, cx: &mut Context) { - let mut revert_changes = HashMap::default(); - let snapshot = self.snapshot(window, cx); - for hunk in snapshot - .hunks_for_ranges(Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter()) - { - self.prepare_revert_change(&mut revert_changes, &hunk, cx); - } - if !revert_changes.is_empty() { - self.transact(window, cx, |editor, window, cx| { - editor.revert(revert_changes, window, cx); - }); - } - } - pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context) { let Some(project) = self.project.clone() else { return; @@ -7048,27 +7034,62 @@ impl Editor { .detach_and_notify_err(window, cx); } - pub fn revert_selected_hunks( + pub fn restore_file( &mut self, - _: &RevertSelectedHunks, + _: &::git::RestoreFile, window: &mut Window, cx: &mut Context, ) { - let selections = self.selections.all(cx).into_iter().map(|s| s.range()); - self.revert_hunks_in_ranges(selections, window, cx); + let mut buffer_ids = HashSet::default(); + let snapshot = self.buffer().read(cx).snapshot(cx); + for selection in self.selections.all::(cx) { + buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range())) + } + + let buffer = self.buffer().read(cx); + let ranges = buffer_ids + .into_iter() + .flat_map(|buffer_id| buffer.excerpt_ranges_for_buffer(buffer_id, cx)) + .collect::>(); + + self.restore_hunks_in_ranges(ranges, window, cx); } - fn revert_hunks_in_ranges( + pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context) { + let selections = self + .selections + .all(cx) + .into_iter() + .map(|s| s.range()) + .collect(); + self.restore_hunks_in_ranges(selections, window, cx); + } + + fn restore_hunks_in_ranges( &mut self, - ranges: impl Iterator>, + ranges: Vec>, window: &mut Window, cx: &mut Context, ) { let mut revert_changes = HashMap::default(); - let snapshot = self.snapshot(window, cx); - for hunk in &snapshot.hunks_for_ranges(ranges) { - self.prepare_revert_change(&mut revert_changes, &hunk, cx); + let snapshot = self.buffer.read(cx).snapshot(cx); + let Some(project) = &self.project else { + return; + }; + + let chunk_by = self + .snapshot(window, cx) + .hunks_for_ranges(ranges.into_iter()) + .into_iter() + .chunk_by(|hunk| hunk.buffer_id); + for (buffer_id, hunks) in &chunk_by { + let hunks = hunks.collect::>(); + for hunk in &hunks { + self.prepare_restore_change(&mut revert_changes, hunk, cx); + } + Self::do_stage_or_unstage(project, false, buffer_id, hunks.into_iter(), &snapshot, cx); } + drop(chunk_by); if !revert_changes.is_empty() { self.transact(window, cx, |editor, window, cx| { editor.revert(revert_changes, window, cx); @@ -7098,7 +7119,7 @@ impl Editor { } } - pub fn prepare_revert_change( + pub fn prepare_restore_change( &self, revert_changes: &mut HashMap, Rope)>>, hunk: &MultiBufferDiffHunk, @@ -12576,89 +12597,134 @@ impl Editor { pub fn toggle_staged_selected_diff_hunks( &mut self, - _: &ToggleStagedSelectedDiffHunks, + _: &::git::ToggleStaged, _window: &mut Window, cx: &mut Context, ) { + let snapshot = self.buffer.read(cx).snapshot(cx); let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); - self.stage_or_unstage_diff_hunks(&ranges, cx); + let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot); + self.stage_or_unstage_diff_hunks(stage, &ranges, cx); + } + + pub fn stage_and_next( + &mut self, + _: &::git::StageAndNext, + window: &mut Window, + cx: &mut Context, + ) { + let head = self.selections.newest_anchor().head(); + self.stage_or_unstage_diff_hunks(true, &[head..head], cx); + self.go_to_next_hunk(&Default::default(), window, cx); + } + + pub fn unstage_and_next( + &mut self, + _: &::git::UnstageAndNext, + window: &mut Window, + cx: &mut Context, + ) { + let head = self.selections.newest_anchor().head(); + self.stage_or_unstage_diff_hunks(false, &[head..head], cx); + self.go_to_next_hunk(&Default::default(), window, cx); } pub fn stage_or_unstage_diff_hunks( &mut self, + stage: bool, ranges: &[Range], cx: &mut Context, ) { + let snapshot = self.buffer.read(cx).snapshot(cx); let Some(project) = &self.project else { return; }; - let snapshot = self.buffer.read(cx).snapshot(cx); - let stage = self.has_stageable_diff_hunks_in_ranges(ranges, &snapshot); let chunk_by = self .diff_hunks_in_ranges(&ranges, &snapshot) .chunk_by(|hunk| hunk.buffer_id); for (buffer_id, hunks) in &chunk_by { - let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { - log::debug!("no buffer for id"); - continue; - }; - let buffer = buffer.read(cx).snapshot(); - let Some((repo, path)) = project - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx) - else { - log::debug!("no git repo for buffer id"); - continue; - }; - let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else { - log::debug!("no diff for buffer id"); - continue; - }; - let Some(secondary_diff) = diff.secondary_diff() else { - log::debug!("no secondary diff for buffer id"); - continue; - }; - - let edits = diff.secondary_edits_for_stage_or_unstage( - stage, - hunks.map(|hunk| { - ( - hunk.diff_base_byte_range.clone(), - hunk.secondary_diff_base_byte_range.clone(), - hunk.buffer_range.clone(), - ) - }), - &buffer, - ); - - let index_base = secondary_diff.base_text().map_or_else( - || Rope::from(""), - |snapshot| snapshot.text.as_rope().clone(), - ); - let index_buffer = cx.new(|cx| { - Buffer::local_normalized(index_base.clone(), text::LineEnding::default(), cx) - }); - let new_index_text = index_buffer.update(cx, |index_buffer, cx| { - index_buffer.edit(edits, None, cx); - index_buffer.snapshot().as_rope().to_string() - }); - let new_index_text = if new_index_text.is_empty() - && (diff.is_single_insertion - || buffer - .file() - .map_or(false, |file| file.disk_state() == DiskState::New)) - { - log::debug!("removing from index"); - None - } else { - Some(new_index_text) - }; - - let _ = repo.read(cx).set_index_text(&path, new_index_text); + Self::do_stage_or_unstage(project, stage, buffer_id, hunks, &snapshot, cx); } } + fn do_stage_or_unstage( + project: &Entity, + stage: bool, + buffer_id: BufferId, + hunks: impl Iterator, + snapshot: &MultiBufferSnapshot, + cx: &mut Context, + ) { + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { + log::debug!("no buffer for id"); + return; + }; + let buffer = buffer.read(cx).snapshot(); + let Some((repo, path)) = project + .read(cx) + .repository_and_path_for_buffer_id(buffer_id, cx) + else { + log::debug!("no git repo for buffer id"); + return; + }; + let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else { + log::debug!("no diff for buffer id"); + return; + }; + let Some(secondary_diff) = diff.secondary_diff() else { + log::debug!("no secondary diff for buffer id"); + return; + }; + + let edits = diff.secondary_edits_for_stage_or_unstage( + stage, + hunks.filter_map(|hunk| { + if stage && hunk.secondary_status == DiffHunkSecondaryStatus::None { + return None; + } else if !stage + && hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk + { + return None; + } + Some(( + hunk.diff_base_byte_range.clone(), + hunk.secondary_diff_base_byte_range.clone(), + hunk.buffer_range.clone(), + )) + }), + &buffer, + ); + + let Some(index_base) = secondary_diff + .base_text() + .map(|snapshot| snapshot.text.as_rope().clone()) + else { + log::debug!("no index base"); + return; + }; + let index_buffer = cx.new(|cx| { + Buffer::local_normalized(index_base.clone(), text::LineEnding::default(), cx) + }); + let new_index_text = index_buffer.update(cx, |index_buffer, cx| { + index_buffer.edit(edits, None, cx); + index_buffer.snapshot().as_rope().to_string() + }); + let new_index_text = if new_index_text.is_empty() + && (diff.is_single_insertion + || buffer + .file() + .map_or(false, |file| file.disk_state() == DiskState::New)) + { + log::debug!("removing from index"); + None + } else { + Some(new_index_text) + }; + + let _ = repo.read(cx).set_index_text(&path, new_index_text); + } + pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); self.buffer diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 0cc96d3e4c..0b6e80e271 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12718,7 +12718,10 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { multibuffer }); - let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx)); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [path!("/").as_ref()], cx).await; + let (editor, cx) = cx + .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx)); editor.update_in(cx, |editor, _window, cx| { for (buffer, diff_base) in [ (buffer_1.clone(), base_text_1), @@ -12736,7 +12739,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { editor.update_in(cx, |editor, window, cx| { assert_eq!(editor.text(cx), "Xaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}"); editor.select_all(&SelectAll, window, cx); - editor.revert_selected_hunks(&RevertSelectedHunks, window, cx); + editor.git_restore(&Default::default(), window, cx); }); cx.executor().run_until_parked(); @@ -12762,7 +12765,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { editor.change_selections(None, window, cx, |s| { s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0))); }); - editor.revert_selected_hunks(&RevertSelectedHunks, window, cx); + editor.git_restore(&Default::default(), window, cx); }); // Now, when all ranges selected belong to buffer_1, the revert should succeed, @@ -14157,7 +14160,7 @@ async fn test_stage_and_unstage_added_file_hunk( cx.assert_index_text(None); cx.update_editor(|editor, window, cx| { - editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx); + editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx); }); executor.run_until_parked(); cx.assert_index_text(Some(&working_copy.replace("ˇ", ""))); @@ -14171,7 +14174,7 @@ async fn test_stage_and_unstage_added_file_hunk( ); cx.update_editor(|editor, window, cx| { - editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx); + editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx); }); executor.run_until_parked(); cx.assert_index_text(None); @@ -16103,7 +16106,7 @@ fn assert_hunk_revert( .map(|hunk| hunk.status()) .collect::>(); - editor.revert_selected_hunks(&RevertSelectedHunks, window, cx); + editor.git_restore(&Default::default(), window, cx); reverted_hunk_statuses }); cx.executor().run_until_parked(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ec99900ba8..f2ebbea8c2 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -19,11 +19,10 @@ use crate::{ 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, - RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, - SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, - CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, - MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, + InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, + RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt, + ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, + GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind}; use client::ParticipantIndex; @@ -418,6 +417,8 @@ impl EditorElement { register_action(editor, window, Editor::toggle_git_blame_inline); register_action(editor, window, Editor::toggle_selected_diff_hunks); register_action(editor, window, Editor::toggle_staged_selected_diff_hunks); + register_action(editor, window, Editor::stage_and_next); + register_action(editor, window, Editor::unstage_and_next); register_action(editor, window, Editor::expand_all_diff_hunks); register_action(editor, window, |editor, action, window, cx| { @@ -491,8 +492,8 @@ impl EditorElement { register_action(editor, window, Editor::unique_lines_case_sensitive); register_action(editor, window, Editor::accept_partial_inline_completion); register_action(editor, window, Editor::accept_edit_prediction); - register_action(editor, window, Editor::revert_file); - register_action(editor, window, Editor::revert_selected_hunks); + register_action(editor, window, Editor::restore_file); + register_action(editor, window, Editor::git_restore); register_action(editor, window, Editor::apply_all_diff_hunks); register_action(editor, window, Editor::apply_selected_diff_hunks); register_action(editor, window, Editor::open_active_item_in_terminal); @@ -9004,8 +9005,8 @@ fn diff_hunk_controls( let focus_handle = editor.focus_handle(cx); move |window, cx| { Tooltip::for_action_in( - "Discard Hunk", - &RevertSelectedHunks, + "Restore Hunk", + &::git::Restore, &focus_handle, window, cx, @@ -9018,7 +9019,7 @@ fn diff_hunk_controls( editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); - editor.revert_hunks_in_ranges([point..point].into_iter(), window, cx); + editor.restore_hunks_in_ranges(vec![point..point], window, cx); }); } }), diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs deleted file mode 100644 index 63d1deec8d..0000000000 --- a/crates/editor/src/hunk_diff.rs +++ /dev/null @@ -1,1565 +0,0 @@ -use collections::{HashMap, HashSet}; -use git::diff::DiffHunkStatus; -use gpui::{ - Action, AppContext as _, Corner, CursorStyle, Focusable as _, Hsla, Model, MouseButton, - Subscription, Task, -}; -use language::{Buffer, BufferId, Point}; -use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, - MultiBufferSnapshot, ToOffset, ToPoint, -}; -use project::buffer_store::BufferChangeSet; -use std::{ops::Range, sync::Arc}; -use sum_tree::TreeMap; -use text::OffsetRangeExt; -use ui::{ - prelude::*, ActiveTheme, Context, Context, ContextMenu, IconButtonShape, InteractiveElement, - IntoElement, ParentElement, PopoverMenu, Styled, Tooltip, Window, -}; -use util::RangeExt; -use workspace::Item; - -use crate::{ - editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks, - ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, - DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, - RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, -}; - -#[derive(Debug, Clone)] -pub(super) struct HoveredHunk { - pub multi_buffer_range: Range, - pub status: DiffHunkStatus, - pub diff_base_byte_range: Range, -} - -#[derive(Default)] -pub(super) struct DiffMap { - pub(crate) hunks: Vec, - pub(crate) diff_bases: HashMap, - pub(crate) snapshot: DiffMapSnapshot, - hunk_update_tasks: HashMap, Task<()>>, - expand_all: bool, -} - -#[derive(Debug, Clone)] -pub(super) struct ExpandedHunk { - pub blocks: Vec, - pub hunk_range: Range, - pub diff_base_byte_range: Range, - pub status: DiffHunkStatus, - pub folded: bool, -} - -#[derive(Clone, Debug, Default)] -pub(crate) struct DiffMapSnapshot(TreeMap); - -pub(crate) struct DiffBaseState { - pub(crate) diff: Model, - pub(crate) last_version: Option, - _subscription: Subscription, -} - -#[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 DiffMap { - pub fn snapshot(&self) -> DiffMapSnapshot { - self.snapshot.clone() - } - - pub fn add_diff( - &mut self, - diff: Model, - window: &mut Window, - cx: &mut Context, - ) { - let buffer_id = diff.read(cx).buffer_id; - self.snapshot - .0 - .insert(buffer_id, diff.read(cx).diff_to_buffer.clone()); - self.diff_bases.insert( - buffer_id, - DiffBaseState { - last_version: None, - _subscription: cx.observe_in(&diff, window, move |editor, diff, window, cx| { - editor - .diff_map - .snapshot - .0 - .insert(buffer_id, diff.read(cx).diff_to_buffer.clone()); - Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, window, cx); - }), - diff, - }, - ); - Editor::sync_expanded_diff_hunks(self, buffer_id, window, cx); - } - - pub fn hunks(&self, include_folded: bool) -> impl Iterator { - self.hunks - .iter() - .filter(move |hunk| include_folded || !hunk.folded) - } -} - -impl DiffMapSnapshot { - pub fn is_empty(&self) -> bool { - self.0.values().all(|diff| diff.is_empty()) - } - - pub fn diff_hunks<'a>( - &'a self, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - self.diff_hunks_in_range(0..buffer_snapshot.len(), buffer_snapshot) - } - - pub fn diff_hunks_in_range<'a, T: ToOffset>( - &'a self, - range: Range, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); - buffer_snapshot - .excerpts_for_range(range.clone()) - .filter_map(move |excerpt| { - let buffer = excerpt.buffer(); - let buffer_id = buffer.remote_id(); - let diff = self.0.get(&buffer_id)?; - let buffer_range = excerpt.map_range_to_buffer(range.clone()); - let buffer_range = - buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range(buffer_range, excerpt.buffer()) - .map(move |hunk| { - let start = - excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0)); - let end = - excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0)); - MultiBufferDiffHunk { - row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row), - buffer_id, - buffer_range: hunk.buffer_range.clone(), - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - } - }), - ) - }) - .flatten() - } - - pub fn diff_hunks_in_range_rev<'a, T: ToOffset>( - &'a self, - range: Range, - buffer_snapshot: &'a MultiBufferSnapshot, - ) -> impl Iterator + 'a { - let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); - buffer_snapshot - .excerpts_for_range_rev(range.clone()) - .filter_map(move |excerpt| { - let buffer = excerpt.buffer(); - let buffer_id = buffer.remote_id(); - let diff = self.0.get(&buffer_id)?; - let buffer_range = excerpt.map_range_to_buffer(range.clone()); - let buffer_range = - buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer()) - .map(move |hunk| { - let start_row = excerpt - .map_point_from_buffer(Point::new(hunk.row_range.start, 0)) - .row; - let end_row = excerpt - .map_point_from_buffer(Point::new(hunk.row_range.end, 0)) - .row; - MultiBufferDiffHunk { - row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row), - buffer_id, - buffer_range: hunk.buffer_range.clone(), - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - } - }), - ) - }) - .flatten() - } -} - -impl Editor { - pub fn set_expand_all_diff_hunks(&mut self) { - self.diff_map.expand_all = true; - } - - pub(super) fn toggle_hovered_hunk( - &mut self, - hovered_hunk: &HoveredHunk, - window: &mut Window, - cx: &mut Context, - ) { - let editor_snapshot = self.snapshot(window, cx); - if let Some(diff_hunk) = to_diff_hunk(hovered_hunk, &editor_snapshot.buffer_snapshot) { - self.toggle_hunks_expanded(vec![diff_hunk], window, cx); - self.change_selections(None, window, cx, |selections| selections.refresh()); - } - } - - pub fn toggle_hunk_diff( - &mut self, - _: &ToggleHunkDiff, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.snapshot(window, cx); - let selections = self.selections.all(cx); - self.toggle_hunks_expanded(hunks_for_selections(&snapshot, &selections), window, cx); - } - - pub fn expand_all_hunk_diffs( - &mut self, - _: &ExpandAllHunkDiffs, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.snapshot(window, cx); - let display_rows_with_expanded_hunks = self - .diff_map - .hunks(false) - .map(|hunk| &hunk.hunk_range) - .map(|anchor_range| { - ( - anchor_range - .start - .to_display_point(&snapshot.display_snapshot) - .row(), - anchor_range - .end - .to_display_point(&snapshot.display_snapshot) - .row(), - ) - }) - .collect::>(); - let hunks = self - .diff_map - .snapshot - .diff_hunks(&snapshot.display_snapshot.buffer_snapshot) - .filter(|hunk| { - let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0) - .to_display_point(&snapshot.display_snapshot) - ..Point::new(hunk.row_range.end.0, 0) - .to_display_point(&snapshot.display_snapshot); - let row_range_end = - display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row()); - row_range_end.is_none() || row_range_end != Some(&hunk_display_row_range.end.row()) - }); - self.toggle_hunks_expanded(hunks.collect(), window, cx); - } - - fn toggle_hunks_expanded( - &mut self, - hunks_to_toggle: Vec, - window: &mut Window, - cx: &mut Context, - ) { - if self.diff_map.expand_all { - return; - } - - let previous_toggle_task = self.diff_map.hunk_update_tasks.remove(&None); - let new_toggle_task = cx.spawn_in(window, move |editor, mut cx| async move { - if let Some(task) = previous_toggle_task { - task.await; - } - - editor - .update_in(&mut cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let mut hunks_to_toggle = hunks_to_toggle.into_iter().fuse().peekable(); - let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len()); - let mut blocks_to_remove = HashSet::default(); - let mut hunks_to_expand = Vec::new(); - editor.diff_map.hunks.retain(|expanded_hunk| { - if expanded_hunk.folded { - return true; - } - let expanded_hunk_row_range = expanded_hunk - .hunk_range - .start - .to_display_point(&snapshot) - .row() - ..expanded_hunk - .hunk_range - .end - .to_display_point(&snapshot) - .row(); - let mut retain = true; - while let Some(hunk_to_toggle) = hunks_to_toggle.peek() { - match diff_hunk_to_display(hunk_to_toggle, &snapshot) { - DisplayDiffHunk::Folded { .. } => { - hunks_to_toggle.next(); - continue; - } - DisplayDiffHunk::Unfolded { - diff_base_byte_range, - display_row_range, - multi_buffer_range, - status, - } => { - let hunk_to_toggle_row_range = display_row_range; - if hunk_to_toggle_row_range.start > expanded_hunk_row_range.end - { - break; - } else if expanded_hunk_row_range == hunk_to_toggle_row_range { - highlights_to_remove.push(expanded_hunk.hunk_range.clone()); - blocks_to_remove - .extend(expanded_hunk.blocks.iter().copied()); - hunks_to_toggle.next(); - retain = false; - break; - } else { - hunks_to_expand.push(HoveredHunk { - status, - multi_buffer_range, - diff_base_byte_range, - }); - hunks_to_toggle.next(); - continue; - } - } - } - } - - retain - }); - for hunk in hunks_to_toggle { - let remaining_hunk_point_range = Point::new(hunk.row_range.start.0, 0) - ..Point::new(hunk.row_range.end.0, 0); - let hunk_start = snapshot - .buffer_snapshot - .anchor_before(remaining_hunk_point_range.start); - let hunk_end = snapshot - .buffer_snapshot - .anchor_in_excerpt(hunk_start.excerpt_id, hunk.buffer_range.end) - .unwrap(); - hunks_to_expand.push(HoveredHunk { - status: hunk_status(&hunk), - multi_buffer_range: hunk_start..hunk_end, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - }); - } - - editor.remove_highlighted_rows::(highlights_to_remove, cx); - editor.remove_blocks(blocks_to_remove, None, cx); - for hunk in hunks_to_expand { - editor.expand_diff_hunk(None, &hunk, window, cx); - } - cx.notify(); - }) - .ok(); - }); - - self.diff_map - .hunk_update_tasks - .insert(None, cx.background_spawn(new_toggle_task)); - } - - pub(super) fn expand_diff_hunk( - &mut self, - diff_base_buffer: Option>, - hunk: &HoveredHunk, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let buffer = self.buffer.clone(); - let multi_buffer_snapshot = buffer.read(cx).snapshot(cx); - let hunk_range = hunk.multi_buffer_range.clone(); - let buffer_id = hunk_range.start.buffer_id?; - let diff_base_buffer = diff_base_buffer.or_else(|| { - self.diff_map - .diff_bases - .get(&buffer_id)? - .diff - .read(cx) - .base_text - .clone() - })?; - - let diff_base = diff_base_buffer.read(cx); - let diff_start_row = diff_base - .offset_to_point(hunk.diff_base_byte_range.start) - .row; - let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row; - let deleted_text_lines = diff_end_row - diff_start_row; - - let block_insert_index = self - .diff_map - .hunks - .binary_search_by(|probe| { - probe - .hunk_range - .start - .cmp(&hunk_range.start, &multi_buffer_snapshot) - }) - .err()?; - - let blocks; - match hunk.status { - DiffHunkStatus::Removed => { - blocks = self.insert_blocks( - [ - self.hunk_header_block(&hunk, cx), - Self::deleted_text_block( - hunk, - diff_base_buffer, - deleted_text_lines, - window, - cx, - ), - ], - None, - cx, - ); - } - DiffHunkStatus::Added => { - self.highlight_rows::( - hunk_range.clone(), - added_hunk_color(cx), - false, - cx, - ); - blocks = self.insert_blocks([self.hunk_header_block(&hunk, cx)], None, cx); - } - DiffHunkStatus::Modified => { - self.highlight_rows::( - hunk_range.clone(), - added_hunk_color(cx), - false, - cx, - ); - blocks = self.insert_blocks( - [ - self.hunk_header_block(&hunk, cx), - Self::deleted_text_block( - hunk, - diff_base_buffer, - deleted_text_lines, - window, - cx, - ), - ], - None, - cx, - ); - } - }; - self.diff_map.hunks.insert( - block_insert_index, - ExpandedHunk { - blocks, - hunk_range, - status: hunk.status, - folded: false, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - }, - ); - - Some(()) - } - - fn apply_diff_hunks_in_range( - &mut self, - range: Range, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let multi_buffer = self.buffer.read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let (excerpt, range) = multi_buffer_snapshot - .range_to_buffer_ranges(range) - .into_iter() - .next()?; - - multi_buffer - .buffer(excerpt.buffer_id()) - .unwrap() - .update(cx, |branch_buffer, cx| { - branch_buffer.merge_into_base(vec![range], cx); - }); - - if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); - } - - None - } - - pub(crate) fn apply_all_diff_hunks( - &mut self, - _: &ApplyAllDiffHunks, - window: &mut Window, - cx: &mut Context, - ) { - let buffers = self.buffer.read(cx).all_buffers(); - for branch_buffer in buffers { - branch_buffer.update(cx, |branch_buffer, cx| { - branch_buffer.merge_into_base(Vec::new(), cx); - }); - } - - if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); - } - } - - pub(crate) fn apply_selected_diff_hunks( - &mut self, - _: &ApplyDiffHunk, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.snapshot(window, cx); - let hunks = hunks_for_selections(&snapshot, &self.selections.all(cx)); - let mut ranges_by_buffer = HashMap::default(); - self.transact(window, cx, |editor, _, cx| { - for hunk in hunks { - if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { - ranges_by_buffer - .entry(buffer.clone()) - .or_insert_with(Vec::new) - .push(hunk.buffer_range.to_offset(buffer.read(cx))); - } - } - - for (buffer, ranges) in ranges_by_buffer { - buffer.update(cx, |buffer, cx| { - buffer.merge_into_base(ranges, cx); - }); - } - }); - - if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); - } - } - - fn has_multiple_hunks(&self, cx: &AppContext) -> bool { - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut hunks = self.diff_map.snapshot.diff_hunks(&snapshot); - hunks.nth(1).is_some() - } - - fn hunk_header_block( - &self, - hunk: &HoveredHunk, - cx: &mut Context, - ) -> BlockProperties { - let is_branch_buffer = self - .buffer - .read(cx) - .point_to_buffer_offset(hunk.multi_buffer_range.start, cx) - .map_or(false, |(buffer, _, _)| { - buffer.read(cx).base_buffer().is_some() - }); - - let border_color = cx.theme().colors().border_variant; - let bg_color = cx.theme().colors().editor_background; - let gutter_color = match hunk.status { - DiffHunkStatus::Added => cx.theme().status().created, - DiffHunkStatus::Modified => cx.theme().status().modified, - DiffHunkStatus::Removed => cx.theme().status().deleted, - }; - - BlockProperties { - placement: BlockPlacement::Above(hunk.multi_buffer_range.start), - height: 1, - style: BlockStyle::Sticky, - priority: 0, - render: Arc::new({ - let editor = cx.entity().clone(); - let hunk = hunk.clone(); - let has_multiple_hunks = self.has_multiple_hunks(cx); - - move |cx| { - let hunk_controls_menu_handle = - editor.read(cx).hunk_controls_menu_handle.clone(); - - h_flex() - .id(cx.block_id) - .block_mouse_down() - .h(cx.window.line_height()) - .w_full() - .border_t_1() - .border_color(border_color) - .bg(bg_color) - .child( - div() - .id("gutter-strip") - .w(EditorElement::diff_hunk_strip_width( - cx.window.line_height(), - )) - .h_full() - .bg(gutter_color) - .cursor(CursorStyle::PointingHand) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, window, cx); - }); - } - }), - ) - .child( - h_flex() - .px_6() - .size_full() - .justify_end() - .child( - h_flex() - .gap_1() - .when(!is_branch_buffer, |row| { - row.child( - IconButton::new("next-hunk", IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_subsequent_hunk( - hunk.multi_buffer_range.end, - window, - cx, - ); - }); - } - }), - ) - .child( - IconButton::new("prev-hunk", IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Previous Hunk", - &GoToPrevHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_preceding_hunk( - hunk.multi_buffer_range.start, - window, - cx, - ); - }); - } - }), - ) - }) - .child( - IconButton::new("discard", IconName::Undo) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Discard Hunk", - &RevertSelectedHunks, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - editor.revert_hunk( - hunk.clone(), - window, - cx, - ); - }); - } - }), - ) - .map(|this| { - if is_branch_buffer { - this.child( - IconButton::new("apply", IconName::Check) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = - editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Apply Hunk", - &ApplyDiffHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - editor - .apply_diff_hunks_in_range( - hunk.multi_buffer_range - .clone(), - window, - cx, - ); - }); - } - }), - ) - } else { - this.child({ - let focus = editor.focus_handle(cx); - PopoverMenu::new("hunk-controls-dropdown") - .trigger_with_tooltip( - IconButton::new( - "toggle_editor_selections_icon", - IconName::EllipsisVertical, - ) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .toggle_state( - hunk_controls_menu_handle - .is_deployed(), - ), - Tooltip::simple("Hunk Controls", cx), - ) - .anchor(Corner::TopRight) - .with_handle(hunk_controls_menu_handle) - .menu(move |window, cx| { - let focus = focus.clone(); - let menu = ContextMenu::build( - window, - cx, - move |menu, _, _| { - menu.context(focus.clone()) - .action( - "Discard All Hunks", - RevertFile - .boxed_clone(), - ) - }, - ); - Some(menu) - }) - }) - } - }), - ) - .when(!is_branch_buffer, |div| { - div.child( - IconButton::new("collapse", IconName::Close) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Collapse Hunk", - &ToggleHunkDiff, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - editor - .toggle_hovered_hunk(&hunk, window, cx); - }); - } - }), - ) - }), - ) - .into_any_element() - } - }), - } - } - - fn deleted_text_block( - hunk: &HoveredHunk, - diff_base_buffer: Model, - deleted_text_height: u32, - window: &mut Window, - cx: &mut Context, - ) -> BlockProperties { - let gutter_color = match hunk.status { - DiffHunkStatus::Added => unreachable!(), - DiffHunkStatus::Modified => cx.theme().status().modified, - DiffHunkStatus::Removed => cx.theme().status().deleted, - }; - let deleted_hunk_color = deleted_hunk_color(cx); - let (editor_height, editor_with_deleted_text) = - editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, window, cx); - let editor = cx.entity().clone(); - let hunk = hunk.clone(); - let height = editor_height.max(deleted_text_height); - BlockProperties { - placement: BlockPlacement::Above(hunk.multi_buffer_range.start), - height, - style: BlockStyle::Flex, - priority: 0, - render: Arc::new(move |cx| { - let width = EditorElement::diff_hunk_strip_width(cx.window.line_height()); - let gutter_dimensions = editor.read(cx.app).gutter_dimensions; - - h_flex() - .id(cx.block_id) - .block_mouse_down() - .bg(deleted_hunk_color) - .h(height as f32 * cx.window.line_height()) - .w_full() - .child( - h_flex() - .id("gutter") - .max_w(gutter_dimensions.full_width()) - .min_w(gutter_dimensions.full_width()) - .size_full() - .child( - h_flex() - .id("gutter hunk") - .bg(gutter_color) - .pl(gutter_dimensions.margin - + gutter_dimensions - .git_blame_entries_width - .unwrap_or_default()) - .max_w(width) - .min_w(width) - .size_full() - .cursor(CursorStyle::PointingHand) - .on_mouse_down(MouseButton::Left, { - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, window, cx); - }); - } - }), - ), - ) - .child(editor_with_deleted_text.clone()) - .into_any_element() - }), - } - } - - pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut Context) -> bool { - if self.diff_map.expand_all { - return false; - } - self.diff_map.hunk_update_tasks.clear(); - self.clear_row_highlights::(); - let to_remove = self - .diff_map - .hunks - .drain(..) - .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter()) - .collect::>(); - if to_remove.is_empty() { - false - } else { - self.remove_blocks(to_remove, None, cx); - true - } - } - - pub(super) fn sync_expanded_diff_hunks( - diff_map: &mut DiffMap, - buffer_id: BufferId, - window: &mut Window, - cx: &mut Context, - ) { - let diff_base_state = diff_map.diff_bases.get_mut(&buffer_id); - let mut diff_base_buffer = None; - let mut diff_base_buffer_unchanged = true; - if let Some(diff_base_state) = diff_base_state { - diff_base_state.diff.update(cx, |diff, _| { - if diff_base_state.last_version != Some(diff.base_text_version) { - diff_base_state.last_version = Some(diff.base_text_version); - diff_base_buffer_unchanged = false; - } - diff_base_buffer = diff.base_text.clone(); - }) - } - - diff_map.hunk_update_tasks.remove(&Some(buffer_id)); - - let new_sync_task = cx.spawn_in(window, move |editor, mut cx| async move { - editor - .update_in(&mut cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let mut recalculated_hunks = snapshot - .diff_map - .diff_hunks(&snapshot.buffer_snapshot) - .filter(|hunk| hunk.buffer_id == buffer_id) - .fuse() - .peekable(); - let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len()); - let mut blocks_to_remove = HashSet::default(); - let mut hunks_to_reexpand = Vec::with_capacity(editor.diff_map.hunks.len()); - editor.diff_map.hunks.retain_mut(|expanded_hunk| { - if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) { - return true; - }; - - let mut retain = false; - if diff_base_buffer_unchanged { - let expanded_hunk_display_range = expanded_hunk - .hunk_range - .start - .to_display_point(&snapshot) - .row() - ..expanded_hunk - .hunk_range - .end - .to_display_point(&snapshot) - .row(); - while let Some(buffer_hunk) = recalculated_hunks.peek() { - match diff_hunk_to_display(buffer_hunk, &snapshot) { - DisplayDiffHunk::Folded { display_row } => { - recalculated_hunks.next(); - if !expanded_hunk.folded - && expanded_hunk_display_range - .to_inclusive() - .contains(&display_row) - { - retain = true; - expanded_hunk.folded = true; - highlights_to_remove - .push(expanded_hunk.hunk_range.clone()); - for block in expanded_hunk.blocks.drain(..) { - blocks_to_remove.insert(block); - } - break; - } else { - continue; - } - } - DisplayDiffHunk::Unfolded { - diff_base_byte_range, - display_row_range, - multi_buffer_range, - status, - } => { - let hunk_display_range = display_row_range; - - if expanded_hunk_display_range.start - > hunk_display_range.end - { - recalculated_hunks.next(); - if editor.diff_map.expand_all { - hunks_to_reexpand.push(HoveredHunk { - status, - multi_buffer_range, - diff_base_byte_range, - }); - } - continue; - } - - if expanded_hunk_display_range.end - < hunk_display_range.start - { - break; - } - - if !expanded_hunk.folded - && expanded_hunk_display_range == hunk_display_range - && expanded_hunk.status == hunk_status(buffer_hunk) - && expanded_hunk.diff_base_byte_range - == buffer_hunk.diff_base_byte_range - { - recalculated_hunks.next(); - retain = true; - } else { - hunks_to_reexpand.push(HoveredHunk { - status, - multi_buffer_range, - diff_base_byte_range, - }); - } - break; - } - } - } - } - if !retain { - blocks_to_remove.extend(expanded_hunk.blocks.drain(..)); - highlights_to_remove.push(expanded_hunk.hunk_range.clone()); - } - retain - }); - - if editor.diff_map.expand_all { - for hunk in recalculated_hunks { - match diff_hunk_to_display(&hunk, &snapshot) { - DisplayDiffHunk::Folded { .. } => {} - DisplayDiffHunk::Unfolded { - diff_base_byte_range, - multi_buffer_range, - status, - .. - } => { - hunks_to_reexpand.push(HoveredHunk { - status, - multi_buffer_range, - diff_base_byte_range, - }); - } - } - } - } else { - drop(recalculated_hunks); - } - - editor.remove_highlighted_rows::(highlights_to_remove, cx); - editor.remove_blocks(blocks_to_remove, None, cx); - - if let Some(diff_base_buffer) = &diff_base_buffer { - for hunk in hunks_to_reexpand { - editor.expand_diff_hunk( - Some(diff_base_buffer.clone()), - &hunk, - window, - cx, - ); - } - } - }) - .ok(); - }); - - diff_map - .hunk_update_tasks - .insert(Some(buffer_id), cx.background_spawn(new_sync_task)); - } - - fn go_to_subsequent_hunk( - &mut self, - position: Anchor, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.snapshot(window, cx); - let position = position.to_point(&snapshot.buffer_snapshot); - if let Some(hunk) = self.go_to_hunk_after_position(&snapshot, position, window, cx) { - let multi_buffer_start = snapshot - .buffer_snapshot - .anchor_before(Point::new(hunk.row_range.start.0, 0)); - let multi_buffer_end = snapshot - .buffer_snapshot - .anchor_after(Point::new(hunk.row_range.end.0, 0)); - self.expand_diff_hunk( - None, - &HoveredHunk { - multi_buffer_range: multi_buffer_start..multi_buffer_end, - status: hunk_status(&hunk), - diff_base_byte_range: hunk.diff_base_byte_range, - }, - window, - cx, - ); - } - } - - fn go_to_preceding_hunk( - &mut self, - position: Anchor, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.snapshot(window, cx); - let position = position.to_point(&snapshot.buffer_snapshot); - let hunk = self.go_to_hunk_before_position(&snapshot, position, window, cx); - if let Some(hunk) = hunk { - let multi_buffer_start = snapshot - .buffer_snapshot - .anchor_before(Point::new(hunk.row_range.start.0, 0)); - let multi_buffer_end = snapshot - .buffer_snapshot - .anchor_after(Point::new(hunk.row_range.end.0, 0)); - self.expand_diff_hunk( - None, - &HoveredHunk { - multi_buffer_range: multi_buffer_start..multi_buffer_end, - status: hunk_status(&hunk), - diff_base_byte_range: hunk.diff_base_byte_range, - }, - window, - cx, - ); - } - } -} - -pub(crate) fn to_diff_hunk( - hovered_hunk: &HoveredHunk, - multi_buffer_snapshot: &MultiBufferSnapshot, -) -> Option { - let buffer_id = hovered_hunk - .multi_buffer_range - .start - .buffer_id - .or(hovered_hunk.multi_buffer_range.end.buffer_id)?; - let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor - ..hovered_hunk.multi_buffer_range.end.text_anchor; - let point_range = hovered_hunk - .multi_buffer_range - .to_point(multi_buffer_snapshot); - Some(MultiBufferDiffHunk { - row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row), - buffer_id, - buffer_range, - diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(), - }) -} - -fn added_hunk_color(cx: &AppContext) -> Hsla { - let mut created_color = cx.theme().status().git().created; - created_color.fade_out(0.7); - created_color -} - -fn deleted_hunk_color(cx: &AppContext) -> Hsla { - let mut deleted_color = cx.theme().status().deleted; - deleted_color.fade_out(0.7); - deleted_color -} - -fn editor_with_deleted_text( - diff_base_buffer: Model, - deleted_color: Hsla, - hunk: &HoveredHunk, - window: &mut Window, - cx: &mut Context, -) -> (u32, Model) { - let parent_editor = cx.entity().downgrade(); - let editor = cx.new(|cx| { - let multi_buffer = cx.new(|_| MultiBuffer::without_headers(language::Capability::ReadOnly)); - multi_buffer.update(cx, |multi_buffer, cx| { - multi_buffer.push_excerpts( - diff_base_buffer, - Some(ExcerptRange { - context: hunk.diff_base_byte_range.clone(), - primary: None, - }), - cx, - ); - }); - - let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); - editor.set_show_wrap_guides(false, cx); - editor.set_show_gutter(false, cx); - editor.set_show_line_numbers(false, cx); - editor.set_show_scrollbars(false, cx); - editor.set_show_runnables(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_show_code_actions(false, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_read_only(true); - editor.set_show_inline_completions(Some(false), window, cx); - - enum DeletedBlockRowHighlight {} - editor.highlight_rows::( - Anchor::min()..Anchor::max(), - deleted_color, - false, - cx, - ); - editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); - editor._subscriptions.extend([cx.on_blur( - &editor.focus_handle, - window, - |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { - s.try_cancel(); - }); - }, - )]); - - editor - .register_action::({ - let hunk = hunk.clone(); - let parent_editor = parent_editor.clone(); - move |_, window, cx| { - parent_editor - .update(cx, |editor, cx| { - editor.revert_hunk(hunk.clone(), window, cx) - }) - .ok(); - } - }) - .detach(); - editor - .register_action::({ - let hunk = hunk.clone(); - move |_, window, cx| { - parent_editor - .update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, window, cx); - }) - .ok(); - } - }) - .detach(); - editor - }); - - let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0); - (editor_height, editor) -} - -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_in_excerpt(multi_buffer_start.excerpt_id, hunk.buffer_range.end) - .unwrap(); - 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 diff_base_1 = " - 1.zero - 1.one - 1.two - 1.three - 1.four - 1.five - 1.six - " - .unindent(); - - let text_1 = " - 1.zero - 1.ONE - 1.TWO - 1.three - 1.FOUR - 1.FIVE - 1.six - " - .unindent(); - - // buffer has a deletion hunk and an insertion hunk - let diff_base_2 = " - 2.zero - 2.one - 2.one-and-a-half - 2.two - 2.three - 2.four - 2.six - " - .unindent(); - - let text_2 = " - 2.zero - 2.one - 2.two - 2.three - 2.four - 2.five - 2.six - " - .unindent(); - - let buffer_1 = project.update(cx, |project, cx| { - project.create_local_buffer(text_1.as_str(), None, cx) - }); - let buffer_2 = project.update(cx, |project, cx| { - project.create_local_buffer(text_2.as_str(), None, cx) - }); - - let multibuffer = cx.new(|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 editor = cx - .add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, false, window, cx)); - editor - .update(cx, |editor, window, cx| { - for (buffer, diff_base) in [ - (buffer_1.clone(), diff_base_1), - (buffer_2.clone(), diff_base_2), - ] { - let diff = cx.new(|cx| { - BufferChangeSet::new_with_base_text( - diff_base.to_string(), - buffer.read(cx).text_snapshot(), - cx, - ) - }); - editor.diff_map.add_diff(diff, window, cx) - } - }) - .unwrap(); - cx.background_executor.run_until_parked(); - - let snapshot = editor - .update(cx, |editor, window, cx| editor.snapshot(window, cx)) - .unwrap(); - - assert_eq!( - snapshot.buffer_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 - .diff_map - .diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot) - .map(|hunk| (hunk_status(&hunk), hunk.row_range)) - .collect::>(), - &expected, - ); - - assert_eq!( - snapshot - .diff_map - .diff_hunks_in_range_rev( - Point::zero()..Point::new(12, 0), - &snapshot.buffer_snapshot - ) - .map(|hunk| (hunk_status(&hunk), hunk.row_range)) - .collect::>(), - expected - .iter() - .rev() - .cloned() - .collect::>() - .as_slice(), - ); - } -} diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index c749e726df..21cd982b09 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -6,6 +6,7 @@ pub mod repository; pub mod status; use anyhow::{anyhow, Context as _, Result}; +use gpui::action_with_deprecated_aliases; use gpui::actions; use serde::{Deserialize, Serialize}; use std::ffi::OsStr; @@ -29,21 +30,24 @@ pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("i actions!( git, [ + // per-hunk + ToggleStaged, + StageAndNext, + UnstageAndNext, + // per-file StageFile, UnstageFile, - ToggleStaged, - // Revert actions are currently in the editor crate: - // editor::RevertFile, - // editor::RevertSelectedHunks + // repo-wide StageAll, UnstageAll, - DiscardTrackedChanges, + RestoreTrackedFiles, TrashUntrackedFiles, Uncommit, Commit, - ClearCommitMessage ] ); +action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]); +action_with_deprecated_aliases!(git, Restore, ["editor::RevertSelectedHunks"]); /// The length of a Git short SHA. pub const SHORT_SHA_LENGTH: usize = 7; diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 272ad4a3d2..ad2884543b 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -13,7 +13,7 @@ use editor::{ }; use git::repository::{CommitDetails, ResetMode}; use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged}; -use git::{DiscardTrackedChanges, StageAll, TrashUntrackedFiles, UnstageAll}; +use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll}; use gpui::*; use itertools::Itertools; use language::{Buffer, File}; @@ -625,7 +625,7 @@ impl GitPanel { fn revert_selected( &mut self, - _: &editor::actions::RevertFile, + _: &git::RestoreFile, window: &mut Window, cx: &mut Context, ) { @@ -763,7 +763,7 @@ impl GitPanel { fn discard_tracked_changes( &mut self, - _: &DiscardTrackedChanges, + _: &RestoreTrackedFiles, window: &mut Window, cx: &mut Context, ) { @@ -2019,7 +2019,7 @@ impl GitPanel { let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .action("Stage File", ToggleStaged.boxed_clone()) - .action(revert_title, editor::actions::RevertFile.boxed_clone()) + .action(revert_title, git::RestoreFile.boxed_clone()) .separator() .action("Open Diff", Confirm.boxed_clone()) .action("Open File", SecondaryConfirm.boxed_clone()) @@ -2040,10 +2040,7 @@ impl GitPanel { .action("Unstage All", UnstageAll.boxed_clone()) .action("Open Diff", project_diff::Diff.boxed_clone()) .separator() - .action( - "Discard Tracked Changes", - DiscardTrackedChanges.boxed_clone(), - ) + .action("Discard Tracked Changes", RestoreTrackedFiles.boxed_clone()) .action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone()) }); self.set_context_menu(context_menu, position, window, cx); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 6ab0c0047b..12b7211b8e 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -841,7 +841,7 @@ fn generate_commands(_: &App) -> Vec { .range(act_on_range), VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks) .range(act_on_range), - VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range), + VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range), VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range), VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| { Some(