assistant_diff.rs

  1use crate::{Thread, ThreadEvent, ToggleKeep};
  2use anyhow::Result;
  3use buffer_diff::DiffHunkStatus;
  4use collections::HashSet;
  5use editor::{
  6    Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
  7    actions::{GoToHunk, GoToPreviousHunk},
  8};
  9use gpui::{
 10    Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
 11    Subscription, Task, WeakEntity, Window, prelude::*,
 12};
 13use language::{Capability, DiskState, OffsetRangeExt, Point};
 14use multi_buffer::PathKey;
 15use project::{Project, ProjectPath};
 16use std::{
 17    any::{Any, TypeId},
 18    ops::Range,
 19    sync::Arc,
 20};
 21use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*};
 22use workspace::{
 23    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
 24    Workspace,
 25    item::{BreadcrumbText, ItemEvent, TabContentParams},
 26    searchable::SearchableItemHandle,
 27};
 28
 29pub struct AssistantDiff {
 30    multibuffer: Entity<MultiBuffer>,
 31    editor: Entity<Editor>,
 32    thread: Entity<Thread>,
 33    focus_handle: FocusHandle,
 34    workspace: WeakEntity<Workspace>,
 35    title: SharedString,
 36    _subscriptions: Vec<Subscription>,
 37}
 38
 39impl AssistantDiff {
 40    pub fn deploy(
 41        thread: Entity<Thread>,
 42        workspace: WeakEntity<Workspace>,
 43        window: &mut Window,
 44        cx: &mut App,
 45    ) -> Result<()> {
 46        let existing_diff = workspace.update(cx, |workspace, cx| {
 47            workspace
 48                .items_of_type::<AssistantDiff>(cx)
 49                .find(|diff| diff.read(cx).thread == thread)
 50        })?;
 51        if let Some(existing_diff) = existing_diff {
 52            workspace.update(cx, |workspace, cx| {
 53                workspace.activate_item(&existing_diff, true, true, window, cx);
 54            })
 55        } else {
 56            let assistant_diff =
 57                cx.new(|cx| AssistantDiff::new(thread.clone(), workspace.clone(), window, cx));
 58            workspace.update(cx, |workspace, cx| {
 59                workspace.add_item_to_center(Box::new(assistant_diff), window, cx);
 60            })
 61        }
 62    }
 63
 64    pub fn new(
 65        thread: Entity<Thread>,
 66        workspace: WeakEntity<Workspace>,
 67        window: &mut Window,
 68        cx: &mut Context<Self>,
 69    ) -> Self {
 70        let focus_handle = cx.focus_handle();
 71        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
 72
 73        let project = thread.read(cx).project().clone();
 74        let render_diff_hunk_controls = Arc::new({
 75            let assistant_diff = cx.entity();
 76            move |row,
 77                  status: &DiffHunkStatus,
 78                  hunk_range,
 79                  is_created_file,
 80                  line_height,
 81                  _editor: &Entity<Editor>,
 82                  window: &mut Window,
 83                  cx: &mut App| {
 84                render_diff_hunk_controls(
 85                    row,
 86                    status,
 87                    hunk_range,
 88                    is_created_file,
 89                    line_height,
 90                    &assistant_diff,
 91                    window,
 92                    cx,
 93                )
 94            }
 95        });
 96        let editor = cx.new(|cx| {
 97            let mut editor =
 98                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
 99            editor.disable_inline_diagnostics();
100            editor.set_expand_all_diff_hunks(cx);
101            editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
102            editor.register_addon(AssistantDiffAddon);
103            editor
104        });
105
106        let action_log = thread.read(cx).action_log().clone();
107        let mut this = Self {
108            _subscriptions: vec![
109                cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
110                    this.update_excerpts(window, cx)
111                }),
112                cx.subscribe(&thread, |this, _thread, event, cx| {
113                    this.handle_thread_event(event, cx)
114                }),
115            ],
116            title: SharedString::default(),
117            multibuffer,
118            editor,
119            thread,
120            focus_handle,
121            workspace,
122        };
123        this.update_excerpts(window, cx);
124        this.update_title(cx);
125        this
126    }
127
128    fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
129        let thread = self.thread.read(cx);
130        let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
131        let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
132
133        for (buffer, changed) in changed_buffers {
134            let Some(file) = buffer.read(cx).file().cloned() else {
135                continue;
136            };
137
138            let path_key = PathKey::namespaced(0, file.full_path(cx).into());
139            paths_to_delete.remove(&path_key);
140
141            let snapshot = buffer.read(cx).snapshot();
142            let diff = changed.diff.read(cx);
143            let diff_hunk_ranges = diff
144                .hunks_intersecting_range(
145                    language::Anchor::MIN..language::Anchor::MAX,
146                    &snapshot,
147                    cx,
148                )
149                .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
150                .collect::<Vec<_>>();
151
152            let (was_empty, is_excerpt_newly_added) =
153                self.multibuffer.update(cx, |multibuffer, cx| {
154                    let was_empty = multibuffer.is_empty();
155                    let (_, is_excerpt_newly_added) = multibuffer.set_excerpts_for_path(
156                        path_key.clone(),
157                        buffer.clone(),
158                        diff_hunk_ranges,
159                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
160                        cx,
161                    );
162                    multibuffer.add_diff(changed.diff.clone(), cx);
163                    (was_empty, is_excerpt_newly_added)
164                });
165
166            self.editor.update(cx, |editor, cx| {
167                if was_empty {
168                    editor.change_selections(None, window, cx, |selections| {
169                        selections.select_ranges([0..0])
170                    });
171                }
172
173                if is_excerpt_newly_added
174                    && buffer
175                        .read(cx)
176                        .file()
177                        .map_or(false, |file| file.disk_state() == DiskState::Deleted)
178                {
179                    editor.fold_buffer(snapshot.text.remote_id(), cx)
180                }
181            });
182        }
183
184        self.multibuffer.update(cx, |multibuffer, cx| {
185            for path in paths_to_delete {
186                multibuffer.remove_excerpts_for_path(path, cx);
187            }
188        });
189
190        if self.multibuffer.read(cx).is_empty()
191            && self
192                .editor
193                .read(cx)
194                .focus_handle(cx)
195                .contains_focused(window, cx)
196        {
197            self.focus_handle.focus(window);
198        } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
199            self.editor.update(cx, |editor, cx| {
200                editor.focus_handle(cx).focus(window);
201            });
202        }
203    }
204
205    fn update_title(&mut self, cx: &mut Context<Self>) {
206        let new_title = self
207            .thread
208            .read(cx)
209            .summary()
210            .unwrap_or("Assistant Changes".into());
211        if new_title != self.title {
212            self.title = new_title;
213            cx.emit(EditorEvent::TitleChanged);
214        }
215    }
216
217    fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
218        match event {
219            ThreadEvent::SummaryChanged => self.update_title(cx),
220            _ => {}
221        }
222    }
223
224    fn toggle_keep(&mut self, _: &crate::ToggleKeep, _window: &mut Window, cx: &mut Context<Self>) {
225        let ranges = self
226            .editor
227            .read(cx)
228            .selections
229            .disjoint_anchor_ranges()
230            .collect::<Vec<_>>();
231
232        let snapshot = self.multibuffer.read(cx).snapshot(cx);
233        let diff_hunks_in_ranges = self
234            .editor
235            .read(cx)
236            .diff_hunks_in_ranges(&ranges, &snapshot)
237            .collect::<Vec<_>>();
238
239        for hunk in diff_hunks_in_ranges {
240            let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
241            if let Some(buffer) = buffer {
242                self.thread.update(cx, |thread, cx| {
243                    let accept = hunk.status().has_secondary_hunk();
244                    thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
245                });
246            }
247        }
248    }
249
250    fn reject(&mut self, _: &crate::Reject, window: &mut Window, cx: &mut Context<Self>) {
251        let ranges = self
252            .editor
253            .update(cx, |editor, cx| editor.selections.ranges(cx));
254        self.editor.update(cx, |editor, cx| {
255            editor.restore_hunks_in_ranges(ranges, window, cx)
256        })
257    }
258
259    fn reject_all(&mut self, _: &crate::RejectAll, window: &mut Window, cx: &mut Context<Self>) {
260        self.editor.update(cx, |editor, cx| {
261            let max_point = editor.buffer().read(cx).read(cx).max_point();
262            editor.restore_hunks_in_ranges(vec![Point::zero()..max_point], window, cx)
263        })
264    }
265
266    fn keep_all(&mut self, _: &crate::KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
267        self.thread
268            .update(cx, |thread, cx| thread.keep_all_edits(cx));
269    }
270
271    fn review_diff_hunks(
272        &mut self,
273        hunk_ranges: Vec<Range<editor::Anchor>>,
274        accept: bool,
275        cx: &mut Context<Self>,
276    ) {
277        let snapshot = self.multibuffer.read(cx).snapshot(cx);
278        let diff_hunks_in_ranges = self
279            .editor
280            .read(cx)
281            .diff_hunks_in_ranges(&hunk_ranges, &snapshot)
282            .collect::<Vec<_>>();
283
284        for hunk in diff_hunks_in_ranges {
285            let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
286            if let Some(buffer) = buffer {
287                self.thread.update(cx, |thread, cx| {
288                    thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
289                });
290            }
291        }
292    }
293}
294
295impl EventEmitter<EditorEvent> for AssistantDiff {}
296
297impl Focusable for AssistantDiff {
298    fn focus_handle(&self, cx: &App) -> FocusHandle {
299        if self.multibuffer.read(cx).is_empty() {
300            self.focus_handle.clone()
301        } else {
302            self.editor.focus_handle(cx)
303        }
304    }
305}
306
307impl Item for AssistantDiff {
308    type Event = EditorEvent;
309
310    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
311        Some(Icon::new(IconName::ZedAssistant).color(Color::Muted))
312    }
313
314    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
315        Editor::to_item_events(event, f)
316    }
317
318    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
319        self.editor
320            .update(cx, |editor, cx| editor.deactivated(window, cx));
321    }
322
323    fn navigate(
324        &mut self,
325        data: Box<dyn Any>,
326        window: &mut Window,
327        cx: &mut Context<Self>,
328    ) -> bool {
329        self.editor
330            .update(cx, |editor, cx| editor.navigate(data, window, cx))
331    }
332
333    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
334        Some("Assistant Diff".into())
335    }
336
337    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
338        let summary = self
339            .thread
340            .read(cx)
341            .summary()
342            .unwrap_or("Assistant Changes".into());
343        Label::new(format!("Review: {}", summary))
344            .color(if params.selected {
345                Color::Default
346            } else {
347                Color::Muted
348            })
349            .into_any_element()
350    }
351
352    fn telemetry_event_text(&self) -> Option<&'static str> {
353        Some("Assistant Diff Opened")
354    }
355
356    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
357        Some(Box::new(self.editor.clone()))
358    }
359
360    fn for_each_project_item(
361        &self,
362        cx: &App,
363        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
364    ) {
365        self.editor.for_each_project_item(cx, f)
366    }
367
368    fn is_singleton(&self, _: &App) -> bool {
369        false
370    }
371
372    fn set_nav_history(
373        &mut self,
374        nav_history: ItemNavHistory,
375        _: &mut Window,
376        cx: &mut Context<Self>,
377    ) {
378        self.editor.update(cx, |editor, _| {
379            editor.set_nav_history(Some(nav_history));
380        });
381    }
382
383    fn clone_on_split(
384        &self,
385        _workspace_id: Option<workspace::WorkspaceId>,
386        window: &mut Window,
387        cx: &mut Context<Self>,
388    ) -> Option<Entity<Self>>
389    where
390        Self: Sized,
391    {
392        Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
393    }
394
395    fn is_dirty(&self, cx: &App) -> bool {
396        self.multibuffer.read(cx).is_dirty(cx)
397    }
398
399    fn has_conflict(&self, cx: &App) -> bool {
400        self.multibuffer.read(cx).has_conflict(cx)
401    }
402
403    fn can_save(&self, _: &App) -> bool {
404        true
405    }
406
407    fn save(
408        &mut self,
409        format: bool,
410        project: Entity<Project>,
411        window: &mut Window,
412        cx: &mut Context<Self>,
413    ) -> Task<Result<()>> {
414        self.editor.save(format, project, window, cx)
415    }
416
417    fn save_as(
418        &mut self,
419        _: Entity<Project>,
420        _: ProjectPath,
421        _window: &mut Window,
422        _: &mut Context<Self>,
423    ) -> Task<Result<()>> {
424        unreachable!()
425    }
426
427    fn reload(
428        &mut self,
429        project: Entity<Project>,
430        window: &mut Window,
431        cx: &mut Context<Self>,
432    ) -> Task<Result<()>> {
433        self.editor.reload(project, window, cx)
434    }
435
436    fn act_as_type<'a>(
437        &'a self,
438        type_id: TypeId,
439        self_handle: &'a Entity<Self>,
440        _: &'a App,
441    ) -> Option<AnyView> {
442        if type_id == TypeId::of::<Self>() {
443            Some(self_handle.to_any())
444        } else if type_id == TypeId::of::<Editor>() {
445            Some(self.editor.to_any())
446        } else {
447            None
448        }
449    }
450
451    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
452        ToolbarItemLocation::PrimaryLeft
453    }
454
455    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
456        self.editor.breadcrumbs(theme, cx)
457    }
458
459    fn added_to_workspace(
460        &mut self,
461        workspace: &mut Workspace,
462        window: &mut Window,
463        cx: &mut Context<Self>,
464    ) {
465        self.editor.update(cx, |editor, cx| {
466            editor.added_to_workspace(workspace, window, cx)
467        });
468    }
469}
470
471impl Render for AssistantDiff {
472    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
473        let is_empty = self.multibuffer.read(cx).is_empty();
474
475        div()
476            .track_focus(&self.focus_handle)
477            .key_context(if is_empty {
478                "EmptyPane"
479            } else {
480                "AssistantDiff"
481            })
482            .on_action(cx.listener(Self::toggle_keep))
483            .on_action(cx.listener(Self::reject))
484            .on_action(cx.listener(Self::reject_all))
485            .on_action(cx.listener(Self::keep_all))
486            .bg(cx.theme().colors().editor_background)
487            .flex()
488            .items_center()
489            .justify_center()
490            .size_full()
491            .when(is_empty, |el| el.child("No changes to review"))
492            .when(!is_empty, |el| el.child(self.editor.clone()))
493    }
494}
495
496fn render_diff_hunk_controls(
497    row: u32,
498    status: &DiffHunkStatus,
499    hunk_range: Range<editor::Anchor>,
500    is_created_file: bool,
501    line_height: Pixels,
502    assistant_diff: &Entity<AssistantDiff>,
503    window: &mut Window,
504    cx: &mut App,
505) -> AnyElement {
506    let editor = assistant_diff.read(cx).editor.clone();
507
508    h_flex()
509        .h(line_height)
510        .mr_0p5()
511        .gap_1()
512        .px_0p5()
513        .pb_1()
514        .border_x_1()
515        .border_b_1()
516        .border_color(cx.theme().colors().border)
517        .rounded_b_md()
518        .bg(cx.theme().colors().editor_background)
519        .gap_1()
520        .occlude()
521        .shadow_md()
522        .children(if status.has_secondary_hunk() {
523            vec![
524                Button::new("reject", "Reject")
525                    .disabled(is_created_file)
526                    .key_binding(
527                        KeyBinding::for_action_in(
528                            &crate::Reject,
529                            &editor.read(cx).focus_handle(cx),
530                            window,
531                            cx,
532                        )
533                        .map(|kb| kb.size(rems_from_px(12.))),
534                    )
535                    .on_click({
536                        let editor = editor.clone();
537                        move |_event, window, cx| {
538                            editor.update(cx, |editor, cx| {
539                                let snapshot = editor.snapshot(window, cx);
540                                let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
541                                editor.restore_hunks_in_ranges(vec![point..point], window, cx);
542                            });
543                        }
544                    }),
545                Button::new(("keep", row as u64), "Keep")
546                    .key_binding(
547                        KeyBinding::for_action_in(
548                            &crate::ToggleKeep,
549                            &editor.read(cx).focus_handle(cx),
550                            window,
551                            cx,
552                        )
553                        .map(|kb| kb.size(rems_from_px(12.))),
554                    )
555                    .on_click({
556                        let assistant_diff = assistant_diff.clone();
557                        move |_event, _window, cx| {
558                            assistant_diff.update(cx, |diff, cx| {
559                                diff.review_diff_hunks(
560                                    vec![hunk_range.start..hunk_range.start],
561                                    true,
562                                    cx,
563                                );
564                            });
565                        }
566                    }),
567            ]
568        } else {
569            vec![
570                Button::new(("review", row as u64), "Review")
571                    .key_binding(KeyBinding::for_action_in(
572                        &ToggleKeep,
573                        &editor.read(cx).focus_handle(cx),
574                        window,
575                        cx,
576                    ))
577                    .on_click({
578                        let assistant_diff = assistant_diff.clone();
579                        move |_event, _window, cx| {
580                            assistant_diff.update(cx, |diff, cx| {
581                                diff.review_diff_hunks(
582                                    vec![hunk_range.start..hunk_range.start],
583                                    false,
584                                    cx,
585                                );
586                            });
587                        }
588                    }),
589            ]
590        })
591        .when(
592            !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
593            |el| {
594                el.child(
595                    IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
596                        .shape(IconButtonShape::Square)
597                        .icon_size(IconSize::Small)
598                        // .disabled(!has_multiple_hunks)
599                        .tooltip({
600                            let focus_handle = editor.focus_handle(cx);
601                            move |window, cx| {
602                                Tooltip::for_action_in(
603                                    "Next Hunk",
604                                    &GoToHunk,
605                                    &focus_handle,
606                                    window,
607                                    cx,
608                                )
609                            }
610                        })
611                        .on_click({
612                            let editor = editor.clone();
613                            move |_event, window, cx| {
614                                editor.update(cx, |editor, cx| {
615                                    let snapshot = editor.snapshot(window, cx);
616                                    let position =
617                                        hunk_range.end.to_point(&snapshot.buffer_snapshot);
618                                    editor.go_to_hunk_before_or_after_position(
619                                        &snapshot,
620                                        position,
621                                        Direction::Next,
622                                        window,
623                                        cx,
624                                    );
625                                    editor.expand_selected_diff_hunks(cx);
626                                });
627                            }
628                        }),
629                )
630                .child(
631                    IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
632                        .shape(IconButtonShape::Square)
633                        .icon_size(IconSize::Small)
634                        // .disabled(!has_multiple_hunks)
635                        .tooltip({
636                            let focus_handle = editor.focus_handle(cx);
637                            move |window, cx| {
638                                Tooltip::for_action_in(
639                                    "Previous Hunk",
640                                    &GoToPreviousHunk,
641                                    &focus_handle,
642                                    window,
643                                    cx,
644                                )
645                            }
646                        })
647                        .on_click({
648                            let editor = editor.clone();
649                            move |_event, window, cx| {
650                                editor.update(cx, |editor, cx| {
651                                    let snapshot = editor.snapshot(window, cx);
652                                    let point =
653                                        hunk_range.start.to_point(&snapshot.buffer_snapshot);
654                                    editor.go_to_hunk_before_or_after_position(
655                                        &snapshot,
656                                        point,
657                                        Direction::Prev,
658                                        window,
659                                        cx,
660                                    );
661                                    editor.expand_selected_diff_hunks(cx);
662                                });
663                            }
664                        }),
665                )
666            },
667        )
668        .into_any_element()
669}
670
671struct AssistantDiffAddon;
672
673impl editor::Addon for AssistantDiffAddon {
674    fn to_any(&self) -> &dyn std::any::Any {
675        self
676    }
677
678    fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
679        key_context.add("assistant_diff");
680    }
681}
682
683pub struct AssistantDiffToolbar {
684    assistant_diff: Option<WeakEntity<AssistantDiff>>,
685    _workspace: WeakEntity<Workspace>,
686}
687
688impl AssistantDiffToolbar {
689    pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
690        Self {
691            assistant_diff: None,
692            _workspace: workspace.weak_handle(),
693        }
694    }
695
696    fn assistant_diff(&self, _: &App) -> Option<Entity<AssistantDiff>> {
697        self.assistant_diff.as_ref()?.upgrade()
698    }
699
700    fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
701        if let Some(assistant_diff) = self.assistant_diff(cx) {
702            assistant_diff.focus_handle(cx).focus(window);
703        }
704        let action = action.boxed_clone();
705        cx.defer(move |cx| {
706            cx.dispatch_action(action.as_ref());
707        })
708    }
709}
710
711impl EventEmitter<ToolbarItemEvent> for AssistantDiffToolbar {}
712
713impl ToolbarItemView for AssistantDiffToolbar {
714    fn set_active_pane_item(
715        &mut self,
716        active_pane_item: Option<&dyn ItemHandle>,
717        _: &mut Window,
718        cx: &mut Context<Self>,
719    ) -> ToolbarItemLocation {
720        self.assistant_diff = active_pane_item
721            .and_then(|item| item.act_as::<AssistantDiff>(cx))
722            .map(|entity| entity.downgrade());
723        if self.assistant_diff.is_some() {
724            ToolbarItemLocation::PrimaryRight
725        } else {
726            ToolbarItemLocation::Hidden
727        }
728    }
729
730    fn pane_focus_update(
731        &mut self,
732        _pane_focused: bool,
733        _window: &mut Window,
734        _cx: &mut Context<Self>,
735    ) {
736    }
737}
738
739impl Render for AssistantDiffToolbar {
740    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
741        let assistant_diff = match self.assistant_diff(cx) {
742            Some(ad) => ad,
743            None => return div(),
744        };
745
746        let is_empty = assistant_diff.read(cx).multibuffer.read(cx).is_empty();
747
748        if is_empty {
749            return div();
750        }
751
752        h_group_xl()
753            .my_neg_1()
754            .items_center()
755            .p_1()
756            .flex_wrap()
757            .justify_between()
758            .child(
759                h_group_sm()
760                    .child(
761                        Button::new("reject-all", "Reject All").on_click(cx.listener(
762                            |this, _, window, cx| {
763                                this.dispatch_action(&crate::RejectAll, window, cx)
764                            },
765                        )),
766                    )
767                    .child(Button::new("keep-all", "Keep All").on_click(cx.listener(
768                        |this, _, window, cx| this.dispatch_action(&crate::KeepAll, window, cx),
769                    ))),
770            )
771    }
772}