assistant_diff.rs

  1use crate::{Thread, ThreadEvent, ToggleKeep};
  2use anyhow::Result;
  3use buffer_diff::DiffHunkStatus;
  4use collections::HashSet;
  5use editor::{
  6    actions::{GoToHunk, GoToPreviousHunk},
  7    Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
  8};
  9use gpui::{
 10    prelude::*, Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable,
 11    SharedString, Subscription, Task, WeakEntity, Window,
 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::{prelude::*, IconButtonShape, KeyBinding, Tooltip};
 22use workspace::{
 23    item::{BreadcrumbText, ItemEvent, TabContentParams},
 24    searchable::SearchableItemHandle,
 25    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
 26    Workspace,
 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("", 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        div()
475            .track_focus(&self.focus_handle)
476            .key_context(if is_empty {
477                "EmptyPane"
478            } else {
479                "AssistantDiff"
480            })
481            .on_action(cx.listener(Self::toggle_keep))
482            .on_action(cx.listener(Self::reject))
483            .on_action(cx.listener(Self::reject_all))
484            .on_action(cx.listener(Self::keep_all))
485            .bg(cx.theme().colors().editor_background)
486            .flex()
487            .items_center()
488            .justify_center()
489            .size_full()
490            .when(is_empty, |el| el.child("No changes to review"))
491            .when(!is_empty, |el| el.child(self.editor.clone()))
492    }
493}
494
495fn render_diff_hunk_controls(
496    row: u32,
497    status: &DiffHunkStatus,
498    hunk_range: Range<editor::Anchor>,
499    is_created_file: bool,
500    line_height: Pixels,
501    assistant_diff: &Entity<AssistantDiff>,
502    window: &mut Window,
503    cx: &mut App,
504) -> AnyElement {
505    let editor = assistant_diff.read(cx).editor.clone();
506    h_flex()
507        .h(line_height)
508        .mr_1()
509        .gap_1()
510        .px_0p5()
511        .pb_1()
512        .border_x_1()
513        .border_b_1()
514        .border_color(cx.theme().colors().border_variant)
515        .rounded_b_lg()
516        .bg(cx.theme().colors().editor_background)
517        .gap_1()
518        .occlude()
519        .shadow_md()
520        .children(if status.has_secondary_hunk() {
521            vec![
522                Button::new("reject", "Reject")
523                    .key_binding(KeyBinding::for_action_in(
524                        &crate::Reject,
525                        &editor.read(cx).focus_handle(cx),
526                        window,
527                        cx,
528                    ))
529                    .tooltip({
530                        let focus_handle = editor.focus_handle(cx);
531                        move |window, cx| {
532                            Tooltip::for_action_in(
533                                "Reject Hunk",
534                                &crate::Reject,
535                                &focus_handle,
536                                window,
537                                cx,
538                            )
539                        }
540                    })
541                    .on_click({
542                        let editor = editor.clone();
543                        move |_event, window, cx| {
544                            editor.update(cx, |editor, cx| {
545                                let snapshot = editor.snapshot(window, cx);
546                                let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
547                                editor.restore_hunks_in_ranges(vec![point..point], window, cx);
548                            });
549                        }
550                    })
551                    .disabled(is_created_file),
552                Button::new(("keep", row as u64), "Keep")
553                    .key_binding(KeyBinding::for_action_in(
554                        &crate::ToggleKeep,
555                        &editor.read(cx).focus_handle(cx),
556                        window,
557                        cx,
558                    ))
559                    .tooltip({
560                        let focus_handle = editor.focus_handle(cx);
561                        move |window, cx| {
562                            Tooltip::for_action_in(
563                                "Keep Hunk",
564                                &crate::ToggleKeep,
565                                &focus_handle,
566                                window,
567                                cx,
568                            )
569                        }
570                    })
571                    .on_click({
572                        let assistant_diff = assistant_diff.clone();
573                        move |_event, _window, cx| {
574                            assistant_diff.update(cx, |diff, cx| {
575                                diff.review_diff_hunks(
576                                    vec![hunk_range.start..hunk_range.start],
577                                    true,
578                                    cx,
579                                );
580                            });
581                        }
582                    }),
583            ]
584        } else {
585            vec![Button::new(("review", row as u64), "Review")
586                .tooltip({
587                    let focus_handle = editor.focus_handle(cx);
588                    move |window, cx| {
589                        Tooltip::for_action_in("Review", &ToggleKeep, &focus_handle, window, cx)
590                    }
591                })
592                .on_click({
593                    let assistant_diff = assistant_diff.clone();
594                    move |_event, _window, cx| {
595                        assistant_diff.update(cx, |diff, cx| {
596                            diff.review_diff_hunks(
597                                vec![hunk_range.start..hunk_range.start],
598                                false,
599                                cx,
600                            );
601                        });
602                    }
603                })]
604        })
605        .when(
606            !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
607            |el| {
608                el.child(
609                    IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
610                        .shape(IconButtonShape::Square)
611                        .icon_size(IconSize::Small)
612                        // .disabled(!has_multiple_hunks)
613                        .tooltip({
614                            let focus_handle = editor.focus_handle(cx);
615                            move |window, cx| {
616                                Tooltip::for_action_in(
617                                    "Next Hunk",
618                                    &GoToHunk,
619                                    &focus_handle,
620                                    window,
621                                    cx,
622                                )
623                            }
624                        })
625                        .on_click({
626                            let editor = editor.clone();
627                            move |_event, window, cx| {
628                                editor.update(cx, |editor, cx| {
629                                    let snapshot = editor.snapshot(window, cx);
630                                    let position =
631                                        hunk_range.end.to_point(&snapshot.buffer_snapshot);
632                                    editor.go_to_hunk_before_or_after_position(
633                                        &snapshot,
634                                        position,
635                                        Direction::Next,
636                                        window,
637                                        cx,
638                                    );
639                                    editor.expand_selected_diff_hunks(cx);
640                                });
641                            }
642                        }),
643                )
644                .child(
645                    IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
646                        .shape(IconButtonShape::Square)
647                        .icon_size(IconSize::Small)
648                        // .disabled(!has_multiple_hunks)
649                        .tooltip({
650                            let focus_handle = editor.focus_handle(cx);
651                            move |window, cx| {
652                                Tooltip::for_action_in(
653                                    "Previous Hunk",
654                                    &GoToPreviousHunk,
655                                    &focus_handle,
656                                    window,
657                                    cx,
658                                )
659                            }
660                        })
661                        .on_click({
662                            let editor = editor.clone();
663                            move |_event, window, cx| {
664                                editor.update(cx, |editor, cx| {
665                                    let snapshot = editor.snapshot(window, cx);
666                                    let point =
667                                        hunk_range.start.to_point(&snapshot.buffer_snapshot);
668                                    editor.go_to_hunk_before_or_after_position(
669                                        &snapshot,
670                                        point,
671                                        Direction::Prev,
672                                        window,
673                                        cx,
674                                    );
675                                    editor.expand_selected_diff_hunks(cx);
676                                });
677                            }
678                        }),
679                )
680            },
681        )
682        .into_any_element()
683}
684
685struct AssistantDiffAddon;
686
687impl editor::Addon for AssistantDiffAddon {
688    fn to_any(&self) -> &dyn std::any::Any {
689        self
690    }
691
692    fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
693        key_context.add("assistant_diff");
694    }
695}
696
697pub struct AssistantDiffToolbar {
698    assistant_diff: Option<WeakEntity<AssistantDiff>>,
699    _workspace: WeakEntity<Workspace>,
700}
701
702impl AssistantDiffToolbar {
703    pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
704        Self {
705            assistant_diff: None,
706            _workspace: workspace.weak_handle(),
707        }
708    }
709
710    fn assistant_diff(&self, _: &App) -> Option<Entity<AssistantDiff>> {
711        self.assistant_diff.as_ref()?.upgrade()
712    }
713
714    fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
715        if let Some(assistant_diff) = self.assistant_diff(cx) {
716            assistant_diff.focus_handle(cx).focus(window);
717        }
718        let action = action.boxed_clone();
719        cx.defer(move |cx| {
720            cx.dispatch_action(action.as_ref());
721        })
722    }
723}
724
725impl EventEmitter<ToolbarItemEvent> for AssistantDiffToolbar {}
726
727impl ToolbarItemView for AssistantDiffToolbar {
728    fn set_active_pane_item(
729        &mut self,
730        active_pane_item: Option<&dyn ItemHandle>,
731        _: &mut Window,
732        cx: &mut Context<Self>,
733    ) -> ToolbarItemLocation {
734        self.assistant_diff = active_pane_item
735            .and_then(|item| item.act_as::<AssistantDiff>(cx))
736            .map(|entity| entity.downgrade());
737        if self.assistant_diff.is_some() {
738            ToolbarItemLocation::PrimaryRight
739        } else {
740            ToolbarItemLocation::Hidden
741        }
742    }
743
744    fn pane_focus_update(
745        &mut self,
746        _pane_focused: bool,
747        _window: &mut Window,
748        _cx: &mut Context<Self>,
749    ) {
750    }
751}
752
753impl Render for AssistantDiffToolbar {
754    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
755        if self.assistant_diff(cx).is_none() {
756            return div();
757        }
758
759        h_group_xl()
760            .my_neg_1()
761            .items_center()
762            .py_1()
763            .pl_2()
764            .pr_1()
765            .flex_wrap()
766            .justify_between()
767            .child(
768                h_group_sm()
769                    .child(
770                        Button::new("reject-all", "Reject All").on_click(cx.listener(
771                            |this, _, window, cx| {
772                                this.dispatch_action(&crate::RejectAll, window, cx)
773                            },
774                        )),
775                    )
776                    .child(Button::new("keep-all", "Keep All").on_click(cx.listener(
777                        |this, _, window, cx| this.dispatch_action(&crate::KeepAll, window, cx),
778                    ))),
779            )
780    }
781}