Add an apply button to hunks in proposed changes editor (#18592)

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-10-01 11:07:52 -06:00 committed by GitHub
parent eb962b7bfc
commit d14e36b323
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 249 additions and 164 deletions

View file

@ -193,6 +193,7 @@ gpui::actions!(
AcceptPartialInlineCompletion, AcceptPartialInlineCompletion,
AddSelectionAbove, AddSelectionAbove,
AddSelectionBelow, AddSelectionBelow,
ApplyDiffHunk,
Backspace, Backspace,
Cancel, Cancel,
CancelLanguageServerWork, CancelLanguageServerWork,

View file

@ -6205,6 +6205,20 @@ impl Editor {
} }
} }
fn apply_selected_diff_hunks(&mut self, _: &ApplyDiffHunk, cx: &mut ViewContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors());
self.transact(cx, |editor, cx| {
for hunk in hunks {
if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.merge_into_base(Some(hunk.buffer_range.to_offset(buffer)), cx);
});
}
}
});
}
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) { pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
let project_path = buffer.read(cx).project_path(cx)?; let project_path = buffer.read(cx).project_path(cx)?;

View file

@ -436,6 +436,7 @@ impl EditorElement {
register_action(view, cx, Editor::accept_inline_completion); register_action(view, cx, Editor::accept_inline_completion);
register_action(view, cx, Editor::revert_file); register_action(view, cx, Editor::revert_file);
register_action(view, cx, Editor::revert_selected_hunks); register_action(view, cx, Editor::revert_selected_hunks);
register_action(view, cx, Editor::apply_selected_diff_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal) register_action(view, cx, Editor::open_active_item_in_terminal)
} }

View file

@ -14,9 +14,9 @@ use ui::{
use util::RangeExt; use util::RangeExt;
use crate::{ use crate::{
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, BlockDisposition, editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk,
BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
}; };
@ -238,19 +238,14 @@ impl Editor {
cx: &mut ViewContext<'_, Editor>, cx: &mut ViewContext<'_, Editor>,
) -> Option<()> { ) -> Option<()> {
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let multi_buffer_row_range = hunk let hunk_range = hunk.multi_buffer_range.clone();
.multi_buffer_range let hunk_point_range = hunk_range.to_point(&multi_buffer_snapshot);
.start
.to_point(&multi_buffer_snapshot)
..hunk.multi_buffer_range.end.to_point(&multi_buffer_snapshot);
let hunk_start = hunk.multi_buffer_range.start;
let hunk_end = hunk.multi_buffer_range.end;
let buffer = self.buffer().clone(); let buffer = self.buffer().clone();
let snapshot = self.snapshot(cx); let snapshot = self.snapshot(cx);
let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| { let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| {
let hunk = buffer_diff_hunk(&snapshot.buffer_snapshot, multi_buffer_row_range.clone())?; let hunk = buffer_diff_hunk(&snapshot.buffer_snapshot, hunk_point_range.clone())?;
let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx); let mut buffer_ranges = buffer.range_to_buffer_ranges(hunk_point_range, cx);
if buffer_ranges.len() == 1 { if buffer_ranges.len() == 1 {
let (buffer, _, _) = buffer_ranges.pop()?; let (buffer, _, _) = buffer_ranges.pop()?;
let diff_base_buffer = diff_base_buffer let diff_base_buffer = diff_base_buffer
@ -275,7 +270,7 @@ impl Editor {
probe probe
.hunk_range .hunk_range
.start .start
.cmp(&hunk_start, &multi_buffer_snapshot) .cmp(&hunk_range.start, &multi_buffer_snapshot)
}) { }) {
Ok(_already_present) => return None, Ok(_already_present) => return None,
Err(ix) => ix, Err(ix) => ix,
@ -295,7 +290,7 @@ impl Editor {
} }
DiffHunkStatus::Added => { DiffHunkStatus::Added => {
self.highlight_rows::<DiffRowHighlight>( self.highlight_rows::<DiffRowHighlight>(
hunk_start..hunk_end, hunk_range.clone(),
added_hunk_color(cx), added_hunk_color(cx),
false, false,
cx, cx,
@ -304,7 +299,7 @@ impl Editor {
} }
DiffHunkStatus::Modified => { DiffHunkStatus::Modified => {
self.highlight_rows::<DiffRowHighlight>( self.highlight_rows::<DiffRowHighlight>(
hunk_start..hunk_end, hunk_range.clone(),
added_hunk_color(cx), added_hunk_color(cx),
false, false,
cx, cx,
@ -323,7 +318,7 @@ impl Editor {
block_insert_index, block_insert_index,
ExpandedHunk { ExpandedHunk {
blocks, blocks,
hunk_range: hunk_start..hunk_end, hunk_range,
status: hunk.status, status: hunk.status,
folded: false, folded: false,
diff_base_byte_range: hunk.diff_base_byte_range.clone(), diff_base_byte_range: hunk.diff_base_byte_range.clone(),
@ -333,11 +328,47 @@ impl Editor {
Some(()) Some(())
} }
fn apply_changes_in_range(
&mut self,
range: Range<Anchor>,
cx: &mut ViewContext<'_, Editor>,
) -> Option<()> {
let (buffer, range, _) = self
.buffer
.read(cx)
.range_to_buffer_ranges(range, cx)
.into_iter()
.next()?;
buffer.update(cx, |branch_buffer, cx| {
branch_buffer.merge_into_base(Some(range), cx);
});
None
}
pub(crate) fn apply_all_changes(&self, cx: &mut ViewContext<Self>) {
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(None, cx);
});
}
}
fn hunk_header_block( fn hunk_header_block(
&self, &self,
hunk: &HoveredHunk, hunk: &HoveredHunk,
cx: &mut ViewContext<'_, Editor>, cx: &mut ViewContext<'_, Editor>,
) -> BlockProperties<Anchor> { ) -> BlockProperties<Anchor> {
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).diff_base_buffer().is_some()
});
let border_color = cx.theme().colors().border_variant; let border_color = cx.theme().colors().border_variant;
let gutter_color = match hunk.status { let gutter_color = match hunk.status {
DiffHunkStatus::Added => cx.theme().status().created, DiffHunkStatus::Added => cx.theme().status().created,
@ -388,6 +419,10 @@ impl Editor {
.pr_6() .pr_6()
.size_full() .size_full()
.justify_between() .justify_between()
.border_t_1()
.pl_6()
.pr_6()
.border_color(border_color)
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
@ -411,43 +446,10 @@ impl Editor {
let hunk = hunk.clone(); let hunk = hunk.clone();
move |_event, cx| { move |_event, cx| {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx); editor.go_to_subsequent_hunk(
let position = hunk hunk.multi_buffer_range.end,
.multi_buffer_range
.end
.to_point(
&snapshot.buffer_snapshot,
);
if let Some(hunk) = editor
.go_to_hunk_after_position(
&snapshot, position, 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,
));
editor.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,
},
cx, cx,
); );
}
}); });
} }
}), }),
@ -472,43 +474,10 @@ impl Editor {
let hunk = hunk.clone(); let hunk = hunk.clone();
move |_event, cx| { move |_event, cx| {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx); editor.go_to_preceding_hunk(
let position = hunk hunk.multi_buffer_range.start,
.multi_buffer_range
.start
.to_point(
&snapshot.buffer_snapshot,
);
let hunk = editor
.go_to_hunk_before_position(
&snapshot, position, 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,
));
editor.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,
},
cx, cx,
); );
}
}); });
} }
}), }),
@ -558,6 +527,36 @@ impl Editor {
} }
}), }),
) )
.when(is_branch_buffer, |this| {
this.child(
IconButton::new("apply", IconName::Check)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {
Tooltip::for_action_in(
"Apply Hunk",
&ApplyDiffHunk,
&focus_handle,
cx,
)
}
})
.on_click({
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, cx| {
editor.update(cx, |editor, cx| {
editor.apply_changes_in_range(
hunk.multi_buffer_range.clone(),
cx,
);
});
}
}),
)
})
.child({ .child({
let focus = editor.focus_handle(cx); let focus = editor.focus_handle(cx);
PopoverMenu::new("hunk-controls-dropdown") PopoverMenu::new("hunk-controls-dropdown")
@ -597,7 +596,6 @@ impl Editor {
}), }),
) )
.child( .child(
div().child(
IconButton::new("collapse", IconName::Close) IconButton::new("collapse", IconName::Close)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
@ -622,7 +620,6 @@ impl Editor {
} }
}), }),
), ),
),
) )
.into_any_element() .into_any_element()
} }
@ -876,6 +873,51 @@ impl Editor {
} }
}) })
} }
fn go_to_subsequent_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
let snapshot = self.snapshot(cx);
let position = position.to_point(&snapshot.buffer_snapshot);
if let Some(hunk) = self.go_to_hunk_after_position(&snapshot, position, 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,
},
cx,
);
}
}
fn go_to_preceding_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
let snapshot = self.snapshot(cx);
let position = position.to_point(&snapshot.buffer_snapshot);
let hunk = self.go_to_hunk_before_position(&snapshot, position, 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,
},
cx,
);
}
}
} }
fn to_diff_hunk( fn to_diff_hunk(

View file

@ -18,7 +18,7 @@ pub struct ProposedChangesEditor {
editor: View<Editor>, editor: View<Editor>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
_recalculate_diffs_task: Task<Option<()>>, _recalculate_diffs_task: Task<Option<()>>,
recalculate_diffs_tx: mpsc::UnboundedSender<Model<Buffer>>, recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
} }
pub struct ProposedChangesBuffer<T> { pub struct ProposedChangesBuffer<T> {
@ -30,6 +30,11 @@ pub struct ProposedChangesEditorToolbar {
current_editor: Option<View<ProposedChangesEditor>>, current_editor: Option<View<ProposedChangesEditor>>,
} }
struct RecalculateDiff {
buffer: Model<Buffer>,
debounce: bool,
}
impl ProposedChangesEditor { impl ProposedChangesEditor {
pub fn new<T: ToOffset>( pub fn new<T: ToOffset>(
buffers: Vec<ProposedChangesBuffer<T>>, buffers: Vec<ProposedChangesBuffer<T>>,
@ -63,16 +68,18 @@ impl ProposedChangesEditor {
recalculate_diffs_tx, recalculate_diffs_tx,
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move { _recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
let mut buffers_to_diff = HashSet::default(); let mut buffers_to_diff = HashSet::default();
while let Some(buffer) = recalculate_diffs_rx.next().await { while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
buffers_to_diff.insert(buffer); buffers_to_diff.insert(recalculate_diff.buffer);
loop { while recalculate_diff.debounce {
cx.background_executor() cx.background_executor()
.timer(Duration::from_millis(250)) .timer(Duration::from_millis(250))
.await; .await;
let mut had_further_changes = false; let mut had_further_changes = false;
while let Ok(next_buffer) = recalculate_diffs_rx.try_next() { while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
buffers_to_diff.insert(next_buffer?); let next_recalculate_diff = next_recalculate_diff?;
recalculate_diff.debounce &= next_recalculate_diff.debounce;
buffers_to_diff.insert(next_recalculate_diff.buffer);
had_further_changes = true; had_further_changes = true;
} }
if !had_further_changes { if !had_further_changes {
@ -99,19 +106,24 @@ impl ProposedChangesEditor {
event: &BufferEvent, event: &BufferEvent,
_cx: &mut ViewContext<Self>, _cx: &mut ViewContext<Self>,
) { ) {
if let BufferEvent::Edited = event { match event {
self.recalculate_diffs_tx.unbounded_send(buffer).ok(); BufferEvent::Operation { .. } => {
self.recalculate_diffs_tx
.unbounded_send(RecalculateDiff {
buffer,
debounce: true,
})
.ok();
} }
BufferEvent::DiffBaseChanged => {
self.recalculate_diffs_tx
.unbounded_send(RecalculateDiff {
buffer,
debounce: false,
})
.ok();
} }
_ => (),
fn apply_all_changes(&self, cx: &mut ViewContext<Self>) {
let buffers = self.editor.read(cx).buffer.read(cx).all_buffers();
for branch_buffer in buffers {
if let Some(base_buffer) = branch_buffer.read(cx).diff_base_buffer() {
base_buffer.update(cx, |base_buffer, cx| {
base_buffer.merge(&branch_buffer, None, cx)
});
}
} }
} }
} }
@ -208,7 +220,9 @@ impl Render for ProposedChangesEditorToolbar {
Button::new("apply-changes", "Apply All").on_click(move |_, cx| { Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
if let Some(editor) = &editor { if let Some(editor) = &editor {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.editor.update(cx, |editor, cx| {
editor.apply_all_changes(cx); editor.apply_all_changes(cx);
})
}); });
} }
}) })

View file

@ -62,7 +62,7 @@ pub use text::{
use theme::SyntaxTheme; use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter; use util::RandomCharIter;
use util::RangeExt; use util::{debug_panic, RangeExt};
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript}; pub use {tree_sitter_rust, tree_sitter_typescript};
@ -823,40 +823,41 @@ impl Buffer {
}) })
} }
/// Applies all of the changes in `branch` buffer that intersect the given `range` /// Applies all of the changes in this buffer that intersect the given `range`
/// to this buffer. /// to its base buffer. This buffer must be a branch buffer to call this method.
pub fn merge( pub fn merge_into_base(&mut self, range: Option<Range<usize>>, cx: &mut ModelContext<Self>) {
&mut self, let Some(base_buffer) = self.diff_base_buffer() else {
branch: &Model<Self>, debug_panic!("not a branch buffer");
range: Option<Range<Anchor>>, return;
cx: &mut ModelContext<Self>, };
) {
let edits = branch.read_with(cx, |branch, _| { base_buffer.update(cx, |base_buffer, cx| {
branch let edits = self
.edits_since_in_range::<usize>( .edits_since::<usize>(&base_buffer.version)
&self.version, .filter_map(|edit| {
range.unwrap_or(Anchor::MIN..Anchor::MAX), if range
) .as_ref()
.map(|edit| { .map_or(true, |range| range.overlaps(&edit.new))
( {
edit.old, Some((edit.old, self.text_for_range(edit.new).collect::<String>()))
branch.text_for_range(edit.new).collect::<String>(), } else {
) None
}
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>();
});
let operation = self.edit(edits, None, cx); let operation = base_buffer.edit(edits, None, cx);
// Prevent this operation from being reapplied to the branch. // Prevent this operation from being reapplied to the branch.
branch.update(cx, |branch, cx| {
if let Some(BufferDiffBase::PastBufferVersion { if let Some(BufferDiffBase::PastBufferVersion {
operations_to_ignore, operations_to_ignore,
.. ..
}) = &mut branch.diff_base }) = &mut self.diff_base
{ {
operations_to_ignore.extend(operation); operations_to_ignore.extend(operation);
} }
cx.emit(BufferEvent::Edited)
cx.emit(BufferEvent::DiffBaseChanged);
}); });
} }

View file

@ -2471,8 +2471,8 @@ fn test_branch_and_merge(cx: &mut TestAppContext) {
}); });
// Merging the branch applies all of its changes to the base. // Merging the branch applies all of its changes to the base.
base_buffer.update(cx, |base_buffer, cx| { branch_buffer.update(cx, |branch_buffer, cx| {
base_buffer.merge(&branch_buffer, None, cx); branch_buffer.merge_into_base(None, cx);
}); });
branch_buffer.update(cx, |branch_buffer, cx| { branch_buffer.update(cx, |branch_buffer, cx| {
@ -2484,6 +2484,18 @@ fn test_branch_and_merge(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
fn test_merge_into_base(cx: &mut AppContext) {
init_settings(cx, |_| {});
let base = cx.new_model(|cx| Buffer::local("abcdefghijk", cx));
let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
branch.update(cx, |branch, cx| {
branch.edit([(0..3, "ABC"), (7..9, "HI")], None, cx);
branch.merge_into_base(Some(5..8), cx);
});
assert_eq!(base.read(cx).text(), "abcdefgHIjk");
}
fn start_recalculating_diff(buffer: &Model<Buffer>, cx: &mut TestAppContext) { fn start_recalculating_diff(buffer: &Model<Buffer>, cx: &mut TestAppContext) {
buffer buffer
.update(cx, |buffer, cx| buffer.recalculate_diff(cx).unwrap()) .update(cx, |buffer, cx| buffer.recalculate_diff(cx).unwrap())