Styling for Apply/Discard buttons (#21017)

Change the "Apply" and "Discard" buttons to match @danilo-leal's design!
Here are some different states:

### Cursor in the first hunk

Now that the cursor is in a particular hunk, we show the "Apply" and
"Discard" names, and the keyboard shortcut. If I press the keyboard
shortcut, it will only apply to this hunk.

<img width="759" alt="Screenshot 2024-11-23 at 10 54 45 PM"
src="https://github.com/user-attachments/assets/68e0f109-9493-4ca2-a99c-dfcbb4d1ce0c">

### Cursor in the second hunk

Moving the cursor to a different hunk changes which buttons get the
keyboard shortcut treatment. Now the keyboard shortcut is shown next to
the hunk that will actually be affected if you press that shortcut.

<img width="749" alt="Screenshot 2024-11-23 at 10 56 27 PM"
src="https://github.com/user-attachments/assets/59c2ace3-6972-4a60-b806-f45e8c25eaae">


Release Notes:

- Restyled Apply/Discard buttons

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
This commit is contained in:
Richard Feldman 2024-11-26 11:09:43 -05:00 committed by GitHub
parent 8f1ec3d11b
commit 884748038e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 411 additions and 293 deletions

View file

@ -522,7 +522,7 @@
{ {
"context": "ProposedChangesEditor", "context": "ProposedChangesEditor",
"bindings": { "bindings": {
"ctrl-shift-y": "editor::ApplyDiffHunk", "ctrl-shift-y": "editor::ApplySelectedDiffHunks",
"ctrl-alt-a": "editor::ApplyAllDiffHunks" "ctrl-alt-a": "editor::ApplyAllDiffHunks"
} }
}, },

View file

@ -562,7 +562,7 @@
{ {
"context": "ProposedChangesEditor", "context": "ProposedChangesEditor",
"bindings": { "bindings": {
"cmd-shift-y": "editor::ApplyDiffHunk", "cmd-shift-y": "editor::ApplySelectedDiffHunks",
"cmd-shift-a": "editor::ApplyAllDiffHunks" "cmd-shift-a": "editor::ApplyAllDiffHunks"
} }
}, },

View file

@ -209,7 +209,7 @@ gpui::actions!(
AddSelectionAbove, AddSelectionAbove,
AddSelectionBelow, AddSelectionBelow,
ApplyAllDiffHunks, ApplyAllDiffHunks,
ApplyDiffHunk, ApplySelectedDiffHunks,
Backspace, Backspace,
Cancel, Cancel,
CancelLanguageServerWork, CancelLanguageServerWork,

View file

@ -99,7 +99,8 @@ use language::{
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges; use linked_editing_ranges::refresh_linked_ranges;
pub use proposed_changes_editor::{ pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, ProposedChangeLocation, ProposedChangesEditor, ProposedChangesToolbar,
ProposedChangesToolbarControls,
}; };
use similar::{ChangeTag, TextDiff}; use similar::{ChangeTag, TextDiff};
use std::iter::Peekable; use std::iter::Peekable;
@ -160,7 +161,7 @@ use theme::{
}; };
use ui::{ use ui::{
h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
ListItem, Popover, PopoverMenuHandle, Tooltip, ListItem, Popover, Tooltip,
}; };
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::{ItemHandle, PreviewTabsSettings}; use workspace::item::{ItemHandle, PreviewTabsSettings};
@ -590,7 +591,6 @@ pub struct Editor {
nav_history: Option<ItemNavHistory>, nav_history: Option<ItemNavHistory>,
context_menu: RwLock<Option<ContextMenu>>, context_menu: RwLock<Option<ContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>, mouse_context_menu: Option<MouseContextMenu>,
hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>, completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
signature_help_state: SignatureHelpState, signature_help_state: SignatureHelpState,
auto_signature_help: Option<bool>, auto_signature_help: Option<bool>,
@ -2112,7 +2112,6 @@ impl Editor {
nav_history: None, nav_history: None,
context_menu: RwLock::new(None), context_menu: RwLock::new(None),
mouse_context_menu: None, mouse_context_menu: None,
hunk_controls_menu_handle: PopoverMenuHandle::default(),
completion_tasks: Default::default(), completion_tasks: Default::default(),
signature_help_state: SignatureHelpState::default(), signature_help_state: SignatureHelpState::default(),
auto_signature_help: None, auto_signature_help: None,
@ -13558,20 +13557,24 @@ fn test_wrap_with_prefix() {
); );
} }
fn is_hunk_selected(hunk: &MultiBufferDiffHunk, selections: &[Selection<Point>]) -> bool {
let mut buffer_rows_for_selections = selections.iter().map(|selection| {
let start = MultiBufferRow(selection.start.row);
let end = MultiBufferRow(selection.end.row);
start..end
});
buffer_rows_for_selections.any(|range| does_selection_touch_hunk(&range, hunk))
}
fn hunks_for_selections( fn hunks_for_selections(
multi_buffer_snapshot: &MultiBufferSnapshot, multi_buffer_snapshot: &MultiBufferSnapshot,
selections: &[Selection<Anchor>], selections: &[Selection<Anchor>],
) -> Vec<MultiBufferDiffHunk> { ) -> Vec<MultiBufferDiffHunk> {
let buffer_rows_for_selections = selections.iter().map(|selection| { let buffer_rows_for_selections = selections.iter().map(|selection| {
let head = selection.head(); let start = MultiBufferRow(selection.start.to_point(multi_buffer_snapshot).row);
let tail = selection.tail(); let end = MultiBufferRow(selection.end.to_point(multi_buffer_snapshot).row);
let start = MultiBufferRow(tail.to_point(multi_buffer_snapshot).row); start..end
let end = MultiBufferRow(head.to_point(multi_buffer_snapshot).row);
if start > end {
end..start
} else {
start..end
}
}); });
hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot) hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot)
@ -13588,19 +13591,8 @@ pub fn hunks_for_rows(
let query_rows = let query_rows =
selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row(); selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row();
for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) { for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) {
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it let related_to_selection =
// when the caret is just above or just below the deleted hunk. does_selection_touch_hunk(&selected_multi_buffer_rows, &hunk);
let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed;
let related_to_selection = if allow_adjacent {
hunk.row_range.overlaps(&query_rows)
|| hunk.row_range.start == query_rows.end
|| hunk.row_range.end == query_rows.start
} else {
// `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected)
// `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected)
hunk.row_range.overlaps(&selected_multi_buffer_rows)
|| selected_multi_buffer_rows.end == hunk.row_range.start
};
if related_to_selection { if related_to_selection {
if !processed_buffer_rows if !processed_buffer_rows
.entry(hunk.buffer_id) .entry(hunk.buffer_id)
@ -13617,6 +13609,26 @@ pub fn hunks_for_rows(
hunks hunks
} }
fn does_selection_touch_hunk(
selected_multi_buffer_rows: &Range<MultiBufferRow>,
hunk: &MultiBufferDiffHunk,
) -> bool {
let query_rows = selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row();
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
// when the caret is just above or just below the deleted hunk.
let allow_adjacent = hunk_status(hunk) == DiffHunkStatus::Removed;
if allow_adjacent {
hunk.row_range.overlaps(&query_rows)
|| hunk.row_range.start == query_rows.end
|| hunk.row_range.end == query_rows.start
} else {
// `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected)
// `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected)
hunk.row_range.overlaps(selected_multi_buffer_rows)
|| selected_multi_buffer_rows.end == hunk.row_range.start
}
}
pub trait CollaborationHub { pub trait CollaborationHub {
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>; fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>;
fn user_participant_indices<'a>( fn user_participant_indices<'a>(

View file

@ -12552,7 +12552,7 @@ async fn test_edits_around_expanded_insertion_hunks(
executor.run_until_parked(); executor.run_until_parked();
cx.assert_diff_hunks( cx.assert_diff_hunks(
r#" r#"
use some::mod1; - use some::mod1;
- use some::mod2; - use some::mod2;
- -
- const A: u32 = 42; - const A: u32 = 42;

View file

@ -2509,6 +2509,7 @@ impl EditorElement {
element, element,
available_space: size(AvailableSpace::MinContent, element_size.height.into()), available_space: size(AvailableSpace::MinContent, element_size.height.into()),
style: BlockStyle::Fixed, style: BlockStyle::Fixed,
is_zero_height: block.height() == 0,
}); });
} }
for (row, block) in non_fixed_blocks { for (row, block) in non_fixed_blocks {
@ -2555,6 +2556,7 @@ impl EditorElement {
element, element,
available_space: size(width.into(), element_size.height.into()), available_space: size(width.into(), element_size.height.into()),
style, style,
is_zero_height: block.height() == 0,
}); });
} }
@ -2602,6 +2604,7 @@ impl EditorElement {
element, element,
available_space: size(width, element_size.height.into()), available_space: size(width, element_size.height.into()),
style, style,
is_zero_height: block.height() == 0,
}); });
} }
} }
@ -3947,8 +3950,23 @@ impl EditorElement {
} }
fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
for mut block in layout.blocks.drain(..) { cx.paint_layer(layout.text_hitbox.bounds, |cx| {
block.element.paint(cx); layout.blocks.retain_mut(|block| {
if !block.is_zero_height {
block.element.paint(cx);
}
block.is_zero_height
});
});
// Paint all the zero-height blocks in a higher layer (if there were any remaining to paint).
if !layout.blocks.is_empty() {
cx.paint_layer(layout.text_hitbox.bounds, |cx| {
for mut block in layout.blocks.drain(..) {
block.element.paint(cx);
}
});
} }
} }
@ -6011,6 +6029,7 @@ struct BlockLayout {
element: AnyElement, element: AnyElement,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
style: BlockStyle, style: BlockStyle,
is_zero_height: bool,
} }
fn layout_line( fn layout_line(

View file

@ -1,6 +1,8 @@
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
use git::diff::DiffHunkStatus; use git::diff::DiffHunkStatus;
use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Task, View}; use gpui::{
AppContext, ClickEvent, CursorStyle, FocusableView, Hsla, Model, MouseButton, Task, View,
};
use language::{Buffer, BufferId, Point}; use language::{Buffer, BufferId, Point};
use multi_buffer::{ use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
@ -9,17 +11,18 @@ use multi_buffer::{
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use text::OffsetRangeExt; use text::OffsetRangeExt;
use ui::{ use ui::{
prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement, prelude::*, ActiveTheme, IconButtonShape, InteractiveElement, IntoElement, KeyBinding,
ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext, ParentElement, Styled, TintColor, Tooltip, ViewContext, VisualContext,
}; };
use util::RangeExt; use util::RangeExt;
use workspace::Item; use workspace::Item;
use crate::{ use crate::{
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks, editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, is_hunk_selected,
ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, ApplyAllDiffHunks, ApplySelectedDiffHunks, BlockPlacement, BlockProperties, BlockStyle,
DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, Editor, EditorElement,
RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertSelectedHunks, ToDisplayPoint,
ToggleHunkDiff,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -57,7 +60,6 @@ pub enum DisplayDiffHunk {
Folded { Folded {
display_row: DisplayRow, display_row: DisplayRow,
}, },
Unfolded { Unfolded {
diff_base_byte_range: Range<usize>, diff_base_byte_range: Range<usize>,
display_row_range: Range<DisplayRow>, display_row_range: Range<DisplayRow>,
@ -371,26 +373,35 @@ impl Editor {
pub(crate) fn apply_selected_diff_hunks( pub(crate) fn apply_selected_diff_hunks(
&mut self, &mut self,
_: &ApplyDiffHunk, _: &ApplySelectedDiffHunks,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors()); let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors());
let mut ranges_by_buffer = HashMap::default();
self.transact(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 { self.transact(cx, |editor, cx| {
buffer.update(cx, |buffer, cx| { if hunks.is_empty() {
buffer.merge_into_base(ranges, cx); // If there are no selected hunks, e.g. because we're using the keybinding with nothing selected, apply the first hunk.
}); if let Some(first_hunk) = editor.expanded_hunks.hunks.first() {
editor.apply_diff_hunks_in_range(first_hunk.hunk_range.clone(), cx);
}
} else {
let mut ranges_by_buffer = HashMap::default();
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);
});
}
} }
}); });
@ -412,246 +423,238 @@ impl Editor {
buffer.read(cx).diff_base_buffer().is_some() buffer.read(cx).diff_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 { BlockProperties {
placement: BlockPlacement::Above(hunk.multi_buffer_range.start), placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
height: 1, height: 0,
style: BlockStyle::Sticky, style: BlockStyle::Sticky,
priority: 0, priority: 1,
render: Arc::new({ render: Arc::new({
let editor = cx.view().clone(); let editor = cx.view().clone();
let hunk = hunk.clone(); let hunk = hunk.clone();
move |cx| { move |cx| {
let hunk_controls_menu_handle = let is_hunk_selected = editor.update(&mut **cx, |editor, cx| {
editor.read(cx).hunk_controls_menu_handle.clone(); let snapshot = editor.buffer.read(cx).snapshot(cx);
let selections = &editor.selections.all::<Point>(cx);
if editor.focus_handle(cx).is_focused(cx) && !selections.is_empty() {
if let Some(hunk) = to_diff_hunk(&hunk, &snapshot) {
is_hunk_selected(&hunk, selections)
} else {
false
}
} else {
// If we have no cursor, or aren't focused, then default to the first hunk
// because that's what the keyboard shortcuts do.
editor
.expanded_hunks
.hunks
.first()
.map(|first_hunk| first_hunk.hunk_range == hunk.multi_buffer_range)
.unwrap_or(false)
}
});
let focus_handle = editor.focus_handle(cx);
let handle_discard_click = {
let editor = editor.clone();
let hunk = hunk.clone();
move |_event: &ClickEvent, cx: &mut WindowContext| {
let multi_buffer = editor.read(cx).buffer().clone();
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
let mut revert_changes = HashMap::default();
if let Some(hunk) =
crate::hunk_diff::to_diff_hunk(&hunk, &multi_buffer_snapshot)
{
Editor::prepare_revert_change(
&mut revert_changes,
&multi_buffer,
&hunk,
cx,
);
}
if !revert_changes.is_empty() {
editor.update(cx, |editor, cx| editor.revert(revert_changes, cx));
}
}
};
let handle_apply_click = {
let editor = editor.clone();
let hunk = hunk.clone();
move |_event: &ClickEvent, cx: &mut WindowContext| {
editor.update(cx, |editor, cx| {
editor
.apply_diff_hunks_in_range(hunk.multi_buffer_range.clone(), cx);
});
}
};
let discard_key_binding =
KeyBinding::for_action_in(&RevertSelectedHunks, &focus_handle, cx);
let discard_tooltip = {
let focus_handle = editor.focus_handle(cx);
move |cx: &mut WindowContext| {
Tooltip::for_action_in(
"Discard Hunk",
&RevertSelectedHunks,
&focus_handle,
cx,
)
}
};
h_flex() h_flex()
.id(cx.block_id) .id(cx.block_id)
.block_mouse_down() .pr_5()
.h(cx.line_height())
.w_full() .w_full()
.border_t_1() .justify_end()
.border_color(border_color)
.bg(bg_color)
.child(
div()
.id("gutter-strip")
.w(EditorElement::diff_hunk_strip_width(cx.line_height()))
.h_full()
.bg(gutter_color)
.cursor(CursorStyle::PointingHand)
.on_click({
let editor = editor.clone();
let hunk = hunk.clone();
move |_event, cx| {
editor.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&hunk, cx);
});
}
}),
)
.child( .child(
h_flex() h_flex()
.px_6() .h(cx.line_height())
.size_full() .gap_1()
.justify_end() .px_1()
.child( .pb_1()
h_flex() .border_x_1()
.gap_1() .border_b_1()
.when(!is_branch_buffer, |row| { .border_color(cx.theme().colors().border_variant)
row.child( .rounded_b_lg()
IconButton::new("next-hunk", IconName::ArrowDown) .bg(cx.theme().colors().editor_background)
.shape(IconButtonShape::Square) .shadow(smallvec::smallvec![gpui::BoxShadow {
.icon_size(IconSize::Small) color: gpui::hsla(0.0, 0.0, 0.0, 0.1),
.tooltip({ blur_radius: px(1.0),
let focus_handle = editor.focus_handle(cx); spread_radius: px(1.0),
move |cx| { offset: gpui::point(px(0.), px(1.0)),
Tooltip::for_action_in( }])
"Next Hunk", .when(!is_branch_buffer, |row| {
&GoToHunk, row.child(
&focus_handle, IconButton::new("next-hunk", IconName::ArrowDown)
cx, .shape(IconButtonShape::Square)
) .icon_size(IconSize::Small)
} .tooltip({
}) let focus_handle = editor.focus_handle(cx);
.on_click({ move |cx| {
let editor = editor.clone(); Tooltip::for_action_in(
let hunk = hunk.clone(); "Next Hunk",
move |_event, cx| { &GoToHunk,
editor.update(cx, |editor, cx| { &focus_handle.clone(),
editor.go_to_subsequent_hunk( cx,
hunk.multi_buffer_range.end, )
cx, }
); })
}); .on_click({
} let editor = editor.clone();
}), let hunk = hunk.clone();
) move |_event, cx| {
.child( editor.update(cx, |editor, cx| {
IconButton::new("prev-hunk", IconName::ArrowUp) editor.go_to_subsequent_hunk(
.shape(IconButtonShape::Square) hunk.multi_buffer_range.end,
.icon_size(IconSize::Small) cx,
.tooltip({ );
let focus_handle = editor.focus_handle(cx); });
move |cx| { }
Tooltip::for_action_in( }),
"Previous Hunk", )
&GoToPrevHunk, .child(
&focus_handle, IconButton::new("prev-hunk", IconName::ArrowUp)
cx, .shape(IconButtonShape::Square)
) .icon_size(IconSize::Small)
} .tooltip({
}) let focus_handle = editor.focus_handle(cx);
.on_click({ move |cx| {
let editor = editor.clone(); Tooltip::for_action_in(
let hunk = hunk.clone(); "Previous Hunk",
move |_event, cx| { &GoToPrevHunk,
editor.update(cx, |editor, cx| { &focus_handle,
editor.go_to_preceding_hunk( cx,
hunk.multi_buffer_range.start, )
cx, }
); })
}); .on_click({
} let editor = editor.clone();
}), let hunk = hunk.clone();
) move |_event, cx| {
}) editor.update(cx, |editor, cx| {
.child( editor.go_to_preceding_hunk(
IconButton::new("discard", IconName::Undo) hunk.multi_buffer_range.start,
cx,
);
});
}
}),
)
})
.child(if is_branch_buffer {
if is_hunk_selected {
Button::new("discard", "Discard")
.style(ButtonStyle::Tinted(TintColor::Negative))
.label_size(LabelSize::Small)
.key_binding(discard_key_binding)
.on_click(handle_discard_click.clone())
.into_any_element()
} else {
IconButton::new("discard", IconName::Close)
.style(ButtonStyle::Tinted(TintColor::Negative))
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(discard_tooltip.clone())
.on_click(handle_discard_click.clone())
.into_any_element()
}
} else {
if is_hunk_selected {
Button::new("undo", "Undo")
.style(ButtonStyle::Tinted(TintColor::Negative))
.label_size(LabelSize::Small)
.key_binding(discard_key_binding)
.on_click(handle_discard_click.clone())
.into_any_element()
} else {
IconButton::new("undo", IconName::Undo)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(discard_tooltip.clone())
.on_click(handle_discard_click.clone())
.into_any_element()
}
})
.when(is_branch_buffer, |this| {
this.child({
let button = Button::new("apply", "Apply")
.style(ButtonStyle::Tinted(TintColor::Positive))
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&ApplySelectedDiffHunks,
&focus_handle,
cx,
))
.on_click(handle_apply_click.clone())
.into_any_element();
if is_hunk_selected {
button
} else {
IconButton::new("apply", IconName::Check)
.style(ButtonStyle::Tinted(TintColor::Positive))
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::XSmall)
.tooltip({ .tooltip({
let focus_handle = editor.focus_handle(cx); let focus_handle = editor.focus_handle(cx);
move |cx| { move |cx| {
Tooltip::for_action_in( Tooltip::for_action_in(
"Discard Hunk", "Apply Hunk",
&RevertSelectedHunks, &ApplySelectedDiffHunks,
&focus_handle, &focus_handle,
cx, cx,
) )
} }
}) })
.on_click({ .on_click(handle_apply_click.clone())
let editor = editor.clone(); .into_any_element()
let hunk = hunk.clone(); }
move |_event, cx| { })
let multi_buffer = })
editor.read(cx).buffer().clone();
let multi_buffer_snapshot =
multi_buffer.read(cx).snapshot(cx);
let mut revert_changes = HashMap::default();
if let Some(hunk) =
crate::hunk_diff::to_diff_hunk(
&hunk,
&multi_buffer_snapshot,
)
{
Editor::prepare_revert_change(
&mut revert_changes,
&multi_buffer,
&hunk,
cx,
);
}
if !revert_changes.is_empty() {
editor.update(cx, |editor, cx| {
editor.revert(revert_changes, 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 |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_diff_hunks_in_range(
hunk.multi_buffer_range
.clone(),
cx,
);
});
}
}),
)
} else {
this.child({
let focus = editor.focus_handle(cx);
PopoverMenu::new("hunk-controls-dropdown")
.trigger(
IconButton::new(
"toggle_editor_selections_icon",
IconName::EllipsisVertical,
)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(
hunk_controls_menu_handle
.is_deployed(),
)
.when(
!hunk_controls_menu_handle
.is_deployed(),
|this| {
this.tooltip(|cx| {
Tooltip::text(
"Hunk Controls",
cx,
)
})
},
),
)
.anchor(AnchorCorner::TopRight)
.with_handle(hunk_controls_menu_handle)
.menu(move |cx| {
let focus = focus.clone();
let menu = ContextMenu::build(
cx,
move |menu, _| {
menu.context(focus.clone())
.action(
"Discard All Hunks",
RevertFile
.boxed_clone(),
)
},
);
Some(menu)
})
})
}
}),
)
.when(!is_branch_buffer, |div| { .when(!is_branch_buffer, |div| {
div.child( div.child(
IconButton::new("collapse", IconName::Close) IconButton::new("collapse", IconName::Close)
@ -707,7 +710,7 @@ impl Editor {
placement: BlockPlacement::Above(hunk.multi_buffer_range.start), placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
height, height,
style: BlockStyle::Flex, style: BlockStyle::Flex,
priority: 0, priority: 1,
render: Arc::new(move |cx| { render: Arc::new(move |cx| {
let width = EditorElement::diff_hunk_strip_width(cx.line_height()); let width = EditorElement::diff_hunk_strip_width(cx.line_height());
let gutter_dimensions = editor.read(cx.context).gutter_dimensions; let gutter_dimensions = editor.read(cx.context).gutter_dimensions;

View file

@ -5,10 +5,11 @@ use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription,
use language::{Buffer, BufferEvent, Capability}; use language::{Buffer, BufferEvent, Capability};
use multi_buffer::{ExcerptRange, MultiBuffer}; use multi_buffer::{ExcerptRange, MultiBuffer};
use project::Project; use project::Project;
use settings::Settings;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration}; use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
use text::ToOffset; use text::ToOffset;
use ui::{prelude::*, ButtonLike, KeyBinding}; use ui::{prelude::*, KeyBinding};
use workspace::{ use workspace::{
searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace, ToolbarItemView, Workspace,
@ -34,7 +35,11 @@ struct BufferEntry {
_subscription: Subscription, _subscription: Subscription,
} }
pub struct ProposedChangesEditorToolbar { pub struct ProposedChangesToolbarControls {
current_editor: Option<View<ProposedChangesEditor>>,
}
pub struct ProposedChangesToolbar {
current_editor: Option<View<ProposedChangesEditor>>, current_editor: Option<View<ProposedChangesEditor>>,
} }
@ -228,6 +233,10 @@ impl ProposedChangesEditor {
_ => (), _ => (),
} }
} }
fn all_changes_accepted(&self) -> bool {
false // In the future, we plan to compute this based on the current state of patches.
}
} }
impl Render for ProposedChangesEditor { impl Render for ProposedChangesEditor {
@ -251,7 +260,11 @@ impl Item for ProposedChangesEditor {
type Event = EditorEvent; type Event = EditorEvent;
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> { fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
Some(Icon::new(IconName::Diff)) if self.all_changes_accepted() {
Some(Icon::new(IconName::Check).color(Color::Success))
} else {
Some(Icon::new(IconName::ZedAssistant))
}
} }
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> { fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
@ -317,7 +330,7 @@ impl Item for ProposedChangesEditor {
} }
} }
impl ProposedChangesEditorToolbar { impl ProposedChangesToolbarControls {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
current_editor: None, current_editor: None,
@ -333,28 +346,97 @@ impl ProposedChangesEditorToolbar {
} }
} }
impl Render for ProposedChangesEditorToolbar { impl Render for ProposedChangesToolbarControls {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All")); if let Some(editor) = &self.current_editor {
let focus_handle = editor.focus_handle(cx);
let action = &ApplyAllDiffHunks;
let keybinding = KeyBinding::for_action_in(action, &focus_handle, cx);
match &self.current_editor { let editor = editor.read(cx);
Some(editor) => {
let focus_handle = editor.focus_handle(cx);
let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx)
.map(|binding| binding.into_any_element());
button_like.children(keybinding).on_click({ let apply_all_button = if editor.all_changes_accepted() {
move |_event, cx| focus_handle.dispatch_action(&ApplyAllDiffHunks, cx) None
}) } else {
} Some(
None => button_like.disabled(true), Button::new("apply-changes", "Apply All")
.style(ButtonStyle::Filled)
.key_binding(keybinding)
.on_click(move |_event, cx| focus_handle.dispatch_action(action, cx)),
)
};
h_flex()
.gap_1()
.children([apply_all_button].into_iter().flatten())
.into_any_element()
} else {
gpui::Empty.into_any_element()
} }
} }
} }
impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {} impl EventEmitter<ToolbarItemEvent> for ProposedChangesToolbarControls {}
impl ToolbarItemView for ProposedChangesEditorToolbar { impl ToolbarItemView for ProposedChangesToolbarControls {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
_cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.current_editor =
active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
self.get_toolbar_item_location()
}
}
impl ProposedChangesToolbar {
pub fn new() -> Self {
Self {
current_editor: None,
}
}
fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
if self.current_editor.is_some() {
ToolbarItemLocation::PrimaryLeft
} else {
ToolbarItemLocation::Hidden
}
}
}
impl Render for ProposedChangesToolbar {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(editor) = &self.current_editor {
let editor = editor.read(cx);
let all_changes_accepted = editor.all_changes_accepted();
let icon = if all_changes_accepted {
Icon::new(IconName::Check).color(Color::Success)
} else {
Icon::new(IconName::ZedAssistant)
};
h_flex()
.gap_2p5()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.child(icon.size(IconSize::Small))
.child(
Label::new(editor.title.clone())
.color(Color::Muted)
.single_line()
.strikethrough(all_changes_accepted),
)
.into_any_element()
} else {
gpui::Empty.into_any_element()
}
}
}
impl EventEmitter<ToolbarItemEvent> for ProposedChangesToolbar {}
impl ToolbarItemView for ProposedChangesToolbar {
fn set_active_pane_item( fn set_active_pane_item(
&mut self, &mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>, active_pane_item: Option<&dyn workspace::ItemHandle>,

View file

@ -17,8 +17,8 @@ use breadcrumbs::Breadcrumbs;
use client::{zed_urls, ZED_URL_SCHEME}; use client::{zed_urls, ZED_URL_SCHEME};
use collections::VecDeque; use collections::VecDeque;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use editor::ProposedChangesEditorToolbar;
use editor::{scroll::Autoscroll, Editor, MultiBuffer}; use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use editor::{ProposedChangesToolbar, ProposedChangesToolbarControls};
use feature_flags::FeatureFlagAppExt; use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, select_biased, StreamExt}; use futures::{channel::mpsc, select_biased, StreamExt};
use gpui::{ use gpui::{
@ -644,8 +644,10 @@ fn initialize_pane(workspace: &Workspace, pane: &View<Pane>, cx: &mut ViewContex
let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
toolbar.add_item(buffer_search_bar.clone(), cx); toolbar.add_item(buffer_search_bar.clone(), cx);
let proposed_change_bar = cx.new_view(|_| ProposedChangesEditorToolbar::new()); let proposed_changes_bar = cx.new_view(|_| ProposedChangesToolbar::new());
toolbar.add_item(proposed_change_bar, cx); toolbar.add_item(proposed_changes_bar, cx);
let proposed_changes_controls = cx.new_view(|_| ProposedChangesToolbarControls::new());
toolbar.add_item(proposed_changes_controls, cx);
let quick_action_bar = let quick_action_bar =
cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx)); cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
toolbar.add_item(quick_action_bar, cx); toolbar.add_item(quick_action_bar, cx);