agent_diff.rs

   1use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent};
   2use anyhow::Result;
   3use buffer_diff::DiffHunkStatus;
   4use collections::HashSet;
   5use editor::{
   6    Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
   7    actions::{GoToHunk, GoToPreviousHunk},
   8    scroll::Autoscroll,
   9};
  10use gpui::{
  11    Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
  12    Subscription, Task, WeakEntity, Window, prelude::*,
  13};
  14use language::{Capability, DiskState, OffsetRangeExt, Point};
  15use multi_buffer::PathKey;
  16use project::{Project, ProjectPath};
  17use std::{
  18    any::{Any, TypeId},
  19    ops::Range,
  20    sync::Arc,
  21};
  22use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*};
  23use workspace::{
  24    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
  25    Workspace,
  26    item::{BreadcrumbText, ItemEvent, TabContentParams},
  27    searchable::SearchableItemHandle,
  28};
  29use zed_actions::assistant::ToggleFocus;
  30
  31pub struct AgentDiff {
  32    multibuffer: Entity<MultiBuffer>,
  33    editor: Entity<Editor>,
  34    thread: Entity<Thread>,
  35    focus_handle: FocusHandle,
  36    workspace: WeakEntity<Workspace>,
  37    title: SharedString,
  38    _subscriptions: Vec<Subscription>,
  39}
  40
  41impl AgentDiff {
  42    pub fn deploy(
  43        thread: Entity<Thread>,
  44        workspace: WeakEntity<Workspace>,
  45        window: &mut Window,
  46        cx: &mut App,
  47    ) -> Result<Entity<Self>> {
  48        workspace.update(cx, |workspace, cx| {
  49            Self::deploy_in_workspace(thread, workspace, window, cx)
  50        })
  51    }
  52
  53    pub fn deploy_in_workspace(
  54        thread: Entity<Thread>,
  55        workspace: &mut Workspace,
  56        window: &mut Window,
  57        cx: &mut Context<Workspace>,
  58    ) -> Entity<Self> {
  59        let existing_diff = workspace
  60            .items_of_type::<AgentDiff>(cx)
  61            .find(|diff| diff.read(cx).thread == thread);
  62        if let Some(existing_diff) = existing_diff {
  63            workspace.activate_item(&existing_diff, true, true, window, cx);
  64            existing_diff
  65        } else {
  66            let agent_diff =
  67                cx.new(|cx| AgentDiff::new(thread.clone(), workspace.weak_handle(), window, cx));
  68            workspace.add_item_to_center(Box::new(agent_diff.clone()), window, cx);
  69            agent_diff
  70        }
  71    }
  72
  73    pub fn new(
  74        thread: Entity<Thread>,
  75        workspace: WeakEntity<Workspace>,
  76        window: &mut Window,
  77        cx: &mut Context<Self>,
  78    ) -> Self {
  79        let focus_handle = cx.focus_handle();
  80        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
  81
  82        let project = thread.read(cx).project().clone();
  83        let render_diff_hunk_controls = Arc::new({
  84            let agent_diff = cx.entity();
  85            move |row,
  86                  status: &DiffHunkStatus,
  87                  hunk_range,
  88                  is_created_file,
  89                  line_height,
  90                  editor: &Entity<Editor>,
  91                  window: &mut Window,
  92                  cx: &mut App| {
  93                render_diff_hunk_controls(
  94                    row,
  95                    status,
  96                    hunk_range,
  97                    is_created_file,
  98                    line_height,
  99                    &agent_diff,
 100                    editor,
 101                    window,
 102                    cx,
 103                )
 104            }
 105        });
 106        let editor = cx.new(|cx| {
 107            let mut editor =
 108                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
 109            editor.disable_inline_diagnostics();
 110            editor.set_expand_all_diff_hunks(cx);
 111            editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
 112            editor.register_addon(AgentDiffAddon);
 113            editor
 114        });
 115
 116        let action_log = thread.read(cx).action_log().clone();
 117        let mut this = Self {
 118            _subscriptions: vec![
 119                cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
 120                    this.update_excerpts(window, cx)
 121                }),
 122                cx.subscribe(&thread, |this, _thread, event, cx| {
 123                    this.handle_thread_event(event, cx)
 124                }),
 125            ],
 126            title: SharedString::default(),
 127            multibuffer,
 128            editor,
 129            thread,
 130            focus_handle,
 131            workspace,
 132        };
 133        this.update_excerpts(window, cx);
 134        this.update_title(cx);
 135        this
 136    }
 137
 138    fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 139        let thread = self.thread.read(cx);
 140        let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
 141        let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
 142
 143        for (buffer, diff_handle) in changed_buffers {
 144            if buffer.read(cx).file().is_none() {
 145                continue;
 146            }
 147
 148            let path_key = PathKey::for_buffer(&buffer, cx);
 149            paths_to_delete.remove(&path_key);
 150
 151            let snapshot = buffer.read(cx).snapshot();
 152            let diff = diff_handle.read(cx);
 153
 154            let diff_hunk_ranges = diff
 155                .hunks_intersecting_range(
 156                    language::Anchor::MIN..language::Anchor::MAX,
 157                    &snapshot,
 158                    cx,
 159                )
 160                .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
 161                .collect::<Vec<_>>();
 162
 163            let (was_empty, is_excerpt_newly_added) =
 164                self.multibuffer.update(cx, |multibuffer, cx| {
 165                    let was_empty = multibuffer.is_empty();
 166                    let (_, is_excerpt_newly_added) = multibuffer.set_excerpts_for_path(
 167                        path_key.clone(),
 168                        buffer.clone(),
 169                        diff_hunk_ranges,
 170                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
 171                        cx,
 172                    );
 173                    multibuffer.add_diff(diff_handle, cx);
 174                    (was_empty, is_excerpt_newly_added)
 175                });
 176
 177            self.editor.update(cx, |editor, cx| {
 178                if was_empty {
 179                    let first_hunk = editor
 180                        .diff_hunks_in_ranges(
 181                            &[editor::Anchor::min()..editor::Anchor::max()],
 182                            &self.multibuffer.read(cx).read(cx),
 183                        )
 184                        .next();
 185
 186                    if let Some(first_hunk) = first_hunk {
 187                        let first_hunk_start = first_hunk.multi_buffer_range().start;
 188                        editor.change_selections(
 189                            Some(Autoscroll::fit()),
 190                            window,
 191                            cx,
 192                            |selections| {
 193                                selections
 194                                    .select_anchor_ranges([first_hunk_start..first_hunk_start]);
 195                            },
 196                        )
 197                    }
 198                }
 199
 200                if is_excerpt_newly_added
 201                    && buffer
 202                        .read(cx)
 203                        .file()
 204                        .map_or(false, |file| file.disk_state() == DiskState::Deleted)
 205                {
 206                    editor.fold_buffer(snapshot.text.remote_id(), cx)
 207                }
 208            });
 209        }
 210
 211        self.multibuffer.update(cx, |multibuffer, cx| {
 212            for path in paths_to_delete {
 213                multibuffer.remove_excerpts_for_path(path, cx);
 214            }
 215        });
 216
 217        if self.multibuffer.read(cx).is_empty()
 218            && self
 219                .editor
 220                .read(cx)
 221                .focus_handle(cx)
 222                .contains_focused(window, cx)
 223        {
 224            self.focus_handle.focus(window);
 225        } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
 226            self.editor.update(cx, |editor, cx| {
 227                editor.focus_handle(cx).focus(window);
 228            });
 229        }
 230    }
 231
 232    fn update_title(&mut self, cx: &mut Context<Self>) {
 233        let new_title = self
 234            .thread
 235            .read(cx)
 236            .summary()
 237            .unwrap_or("Assistant Changes".into());
 238        if new_title != self.title {
 239            self.title = new_title;
 240            cx.emit(EditorEvent::TitleChanged);
 241        }
 242    }
 243
 244    fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
 245        match event {
 246            ThreadEvent::SummaryGenerated => self.update_title(cx),
 247            _ => {}
 248        }
 249    }
 250
 251    pub fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
 252        if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
 253            self.editor.update(cx, |editor, cx| {
 254                let first_hunk = editor
 255                    .diff_hunks_in_ranges(
 256                        &[position..editor::Anchor::max()],
 257                        &self.multibuffer.read(cx).read(cx),
 258                    )
 259                    .next();
 260
 261                if let Some(first_hunk) = first_hunk {
 262                    let first_hunk_start = first_hunk.multi_buffer_range().start;
 263                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
 264                        selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
 265                    })
 266                }
 267            });
 268        }
 269    }
 270
 271    fn keep(&mut self, _: &crate::Keep, window: &mut Window, cx: &mut Context<Self>) {
 272        let ranges = self
 273            .editor
 274            .read(cx)
 275            .selections
 276            .disjoint_anchor_ranges()
 277            .collect::<Vec<_>>();
 278        self.keep_edits_in_ranges(ranges, window, cx);
 279    }
 280
 281    fn reject(&mut self, _: &crate::Reject, window: &mut Window, cx: &mut Context<Self>) {
 282        let ranges = self
 283            .editor
 284            .read(cx)
 285            .selections
 286            .disjoint_anchor_ranges()
 287            .collect::<Vec<_>>();
 288        self.reject_edits_in_ranges(ranges, window, cx);
 289    }
 290
 291    fn reject_all(&mut self, _: &crate::RejectAll, window: &mut Window, cx: &mut Context<Self>) {
 292        self.reject_edits_in_ranges(
 293            vec![editor::Anchor::min()..editor::Anchor::max()],
 294            window,
 295            cx,
 296        );
 297    }
 298
 299    fn keep_all(&mut self, _: &crate::KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
 300        self.thread
 301            .update(cx, |thread, cx| thread.keep_all_edits(cx));
 302    }
 303
 304    fn keep_edits_in_ranges(
 305        &mut self,
 306        ranges: Vec<Range<editor::Anchor>>,
 307        window: &mut Window,
 308        cx: &mut Context<Self>,
 309    ) {
 310        let snapshot = self.multibuffer.read(cx).snapshot(cx);
 311        let diff_hunks_in_ranges = self
 312            .editor
 313            .read(cx)
 314            .diff_hunks_in_ranges(&ranges, &snapshot)
 315            .collect::<Vec<_>>();
 316        let newest_cursor = self.editor.update(cx, |editor, cx| {
 317            editor.selections.newest::<Point>(cx).head()
 318        });
 319        if diff_hunks_in_ranges.iter().any(|hunk| {
 320            hunk.row_range
 321                .contains(&multi_buffer::MultiBufferRow(newest_cursor.row))
 322        }) {
 323            self.update_selection(&diff_hunks_in_ranges, window, cx);
 324        }
 325
 326        for hunk in &diff_hunks_in_ranges {
 327            let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
 328            if let Some(buffer) = buffer {
 329                self.thread.update(cx, |thread, cx| {
 330                    thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
 331                });
 332            }
 333        }
 334    }
 335
 336    fn reject_edits_in_ranges(
 337        &mut self,
 338        ranges: Vec<Range<editor::Anchor>>,
 339        window: &mut Window,
 340        cx: &mut Context<Self>,
 341    ) {
 342        let snapshot = self.multibuffer.read(cx).snapshot(cx);
 343        let diff_hunks_in_ranges = self
 344            .editor
 345            .read(cx)
 346            .diff_hunks_in_ranges(&ranges, &snapshot)
 347            .collect::<Vec<_>>();
 348        let newest_cursor = self.editor.update(cx, |editor, cx| {
 349            editor.selections.newest::<Point>(cx).head()
 350        });
 351        if diff_hunks_in_ranges.iter().any(|hunk| {
 352            hunk.row_range
 353                .contains(&multi_buffer::MultiBufferRow(newest_cursor.row))
 354        }) {
 355            self.update_selection(&diff_hunks_in_ranges, window, cx);
 356        }
 357
 358        for hunk in &diff_hunks_in_ranges {
 359            let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
 360            if let Some(buffer) = buffer {
 361                self.thread
 362                    .update(cx, |thread, cx| {
 363                        thread.reject_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
 364                    })
 365                    .detach_and_log_err(cx);
 366            }
 367        }
 368    }
 369
 370    fn update_selection(
 371        &mut self,
 372        diff_hunks: &[multi_buffer::MultiBufferDiffHunk],
 373        window: &mut Window,
 374        cx: &mut Context<Self>,
 375    ) {
 376        let snapshot = self.multibuffer.read(cx).snapshot(cx);
 377        let target_hunk = diff_hunks
 378            .last()
 379            .and_then(|last_kept_hunk| {
 380                let last_kept_hunk_end = last_kept_hunk.multi_buffer_range().end;
 381                self.editor
 382                    .read(cx)
 383                    .diff_hunks_in_ranges(&[last_kept_hunk_end..editor::Anchor::max()], &snapshot)
 384                    .skip(1)
 385                    .next()
 386            })
 387            .or_else(|| {
 388                let first_kept_hunk = diff_hunks.first()?;
 389                let first_kept_hunk_start = first_kept_hunk.multi_buffer_range().start;
 390                self.editor
 391                    .read(cx)
 392                    .diff_hunks_in_ranges(
 393                        &[editor::Anchor::min()..first_kept_hunk_start],
 394                        &snapshot,
 395                    )
 396                    .next()
 397            });
 398
 399        if let Some(target_hunk) = target_hunk {
 400            self.editor.update(cx, |editor, cx| {
 401                editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
 402                    let next_hunk_start = target_hunk.multi_buffer_range().start;
 403                    selections.select_anchor_ranges([next_hunk_start..next_hunk_start]);
 404                })
 405            });
 406        }
 407    }
 408}
 409
 410impl EventEmitter<EditorEvent> for AgentDiff {}
 411
 412impl Focusable for AgentDiff {
 413    fn focus_handle(&self, cx: &App) -> FocusHandle {
 414        if self.multibuffer.read(cx).is_empty() {
 415            self.focus_handle.clone()
 416        } else {
 417            self.editor.focus_handle(cx)
 418        }
 419    }
 420}
 421
 422impl Item for AgentDiff {
 423    type Event = EditorEvent;
 424
 425    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 426        Some(Icon::new(IconName::ZedAssistant).color(Color::Muted))
 427    }
 428
 429    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 430        Editor::to_item_events(event, f)
 431    }
 432
 433    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 434        self.editor
 435            .update(cx, |editor, cx| editor.deactivated(window, cx));
 436    }
 437
 438    fn navigate(
 439        &mut self,
 440        data: Box<dyn Any>,
 441        window: &mut Window,
 442        cx: &mut Context<Self>,
 443    ) -> bool {
 444        self.editor
 445            .update(cx, |editor, cx| editor.navigate(data, window, cx))
 446    }
 447
 448    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
 449        Some("Agent Diff".into())
 450    }
 451
 452    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
 453        let summary = self
 454            .thread
 455            .read(cx)
 456            .summary()
 457            .unwrap_or("Assistant Changes".into());
 458        Label::new(format!("Review: {}", summary))
 459            .color(if params.selected {
 460                Color::Default
 461            } else {
 462                Color::Muted
 463            })
 464            .into_any_element()
 465    }
 466
 467    fn telemetry_event_text(&self) -> Option<&'static str> {
 468        Some("Assistant Diff Opened")
 469    }
 470
 471    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
 472        Some(Box::new(self.editor.clone()))
 473    }
 474
 475    fn for_each_project_item(
 476        &self,
 477        cx: &App,
 478        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
 479    ) {
 480        self.editor.for_each_project_item(cx, f)
 481    }
 482
 483    fn is_singleton(&self, _: &App) -> bool {
 484        false
 485    }
 486
 487    fn set_nav_history(
 488        &mut self,
 489        nav_history: ItemNavHistory,
 490        _: &mut Window,
 491        cx: &mut Context<Self>,
 492    ) {
 493        self.editor.update(cx, |editor, _| {
 494            editor.set_nav_history(Some(nav_history));
 495        });
 496    }
 497
 498    fn clone_on_split(
 499        &self,
 500        _workspace_id: Option<workspace::WorkspaceId>,
 501        window: &mut Window,
 502        cx: &mut Context<Self>,
 503    ) -> Option<Entity<Self>>
 504    where
 505        Self: Sized,
 506    {
 507        Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
 508    }
 509
 510    fn is_dirty(&self, cx: &App) -> bool {
 511        self.multibuffer.read(cx).is_dirty(cx)
 512    }
 513
 514    fn has_conflict(&self, cx: &App) -> bool {
 515        self.multibuffer.read(cx).has_conflict(cx)
 516    }
 517
 518    fn can_save(&self, _: &App) -> bool {
 519        true
 520    }
 521
 522    fn save(
 523        &mut self,
 524        format: bool,
 525        project: Entity<Project>,
 526        window: &mut Window,
 527        cx: &mut Context<Self>,
 528    ) -> Task<Result<()>> {
 529        self.editor.save(format, project, window, cx)
 530    }
 531
 532    fn save_as(
 533        &mut self,
 534        _: Entity<Project>,
 535        _: ProjectPath,
 536        _window: &mut Window,
 537        _: &mut Context<Self>,
 538    ) -> Task<Result<()>> {
 539        unreachable!()
 540    }
 541
 542    fn reload(
 543        &mut self,
 544        project: Entity<Project>,
 545        window: &mut Window,
 546        cx: &mut Context<Self>,
 547    ) -> Task<Result<()>> {
 548        self.editor.reload(project, window, cx)
 549    }
 550
 551    fn act_as_type<'a>(
 552        &'a self,
 553        type_id: TypeId,
 554        self_handle: &'a Entity<Self>,
 555        _: &'a App,
 556    ) -> Option<AnyView> {
 557        if type_id == TypeId::of::<Self>() {
 558            Some(self_handle.to_any())
 559        } else if type_id == TypeId::of::<Editor>() {
 560            Some(self.editor.to_any())
 561        } else {
 562            None
 563        }
 564    }
 565
 566    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
 567        ToolbarItemLocation::PrimaryLeft
 568    }
 569
 570    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
 571        self.editor.breadcrumbs(theme, cx)
 572    }
 573
 574    fn added_to_workspace(
 575        &mut self,
 576        workspace: &mut Workspace,
 577        window: &mut Window,
 578        cx: &mut Context<Self>,
 579    ) {
 580        self.editor.update(cx, |editor, cx| {
 581            editor.added_to_workspace(workspace, window, cx)
 582        });
 583    }
 584}
 585
 586impl Render for AgentDiff {
 587    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 588        let is_empty = self.multibuffer.read(cx).is_empty();
 589        let focus_handle = &self.focus_handle;
 590
 591        div()
 592            .track_focus(focus_handle)
 593            .key_context(if is_empty { "EmptyPane" } else { "AgentDiff" })
 594            .on_action(cx.listener(Self::keep))
 595            .on_action(cx.listener(Self::reject))
 596            .on_action(cx.listener(Self::reject_all))
 597            .on_action(cx.listener(Self::keep_all))
 598            .bg(cx.theme().colors().editor_background)
 599            .flex()
 600            .items_center()
 601            .justify_center()
 602            .size_full()
 603            .when(is_empty, |el| {
 604                el.child(
 605                    v_flex()
 606                        .items_center()
 607                        .gap_2()
 608                        .child("No changes to review")
 609                        .child(
 610                            Button::new("continue-iterating", "Continue Iterating")
 611                                .style(ButtonStyle::Filled)
 612                                .icon(IconName::ForwardArrow)
 613                                .icon_position(IconPosition::Start)
 614                                .icon_size(IconSize::Small)
 615                                .icon_color(Color::Muted)
 616                                .full_width()
 617                                .key_binding(KeyBinding::for_action_in(
 618                                    &ToggleFocus,
 619                                    &focus_handle.clone(),
 620                                    window,
 621                                    cx,
 622                                ))
 623                                .on_click(|_event, window, cx| {
 624                                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
 625                                }),
 626                        ),
 627                )
 628            })
 629            .when(!is_empty, |el| el.child(self.editor.clone()))
 630    }
 631}
 632
 633fn render_diff_hunk_controls(
 634    row: u32,
 635    _status: &DiffHunkStatus,
 636    hunk_range: Range<editor::Anchor>,
 637    is_created_file: bool,
 638    line_height: Pixels,
 639    agent_diff: &Entity<AgentDiff>,
 640    editor: &Entity<Editor>,
 641    window: &mut Window,
 642    cx: &mut App,
 643) -> AnyElement {
 644    let editor = editor.clone();
 645    h_flex()
 646        .h(line_height)
 647        .mr_0p5()
 648        .gap_1()
 649        .px_0p5()
 650        .pb_1()
 651        .border_x_1()
 652        .border_b_1()
 653        .border_color(cx.theme().colors().border)
 654        .rounded_b_md()
 655        .bg(cx.theme().colors().editor_background)
 656        .gap_1()
 657        .occlude()
 658        .shadow_md()
 659        .children(vec![
 660            Button::new(("reject", row as u64), "Reject")
 661                .disabled(is_created_file)
 662                .key_binding(
 663                    KeyBinding::for_action_in(
 664                        &Reject,
 665                        &editor.read(cx).focus_handle(cx),
 666                        window,
 667                        cx,
 668                    )
 669                    .map(|kb| kb.size(rems_from_px(12.))),
 670                )
 671                .on_click({
 672                    let agent_diff = agent_diff.clone();
 673                    move |_event, window, cx| {
 674                        agent_diff.update(cx, |diff, cx| {
 675                            diff.reject_edits_in_ranges(
 676                                vec![hunk_range.start..hunk_range.start],
 677                                window,
 678                                cx,
 679                            );
 680                        });
 681                    }
 682                }),
 683            Button::new(("keep", row as u64), "Keep")
 684                .key_binding(
 685                    KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), window, cx)
 686                        .map(|kb| kb.size(rems_from_px(12.))),
 687                )
 688                .on_click({
 689                    let agent_diff = agent_diff.clone();
 690                    move |_event, window, cx| {
 691                        agent_diff.update(cx, |diff, cx| {
 692                            diff.keep_edits_in_ranges(
 693                                vec![hunk_range.start..hunk_range.start],
 694                                window,
 695                                cx,
 696                            );
 697                        });
 698                    }
 699                }),
 700        ])
 701        .when(
 702            !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
 703            |el| {
 704                el.child(
 705                    IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
 706                        .shape(IconButtonShape::Square)
 707                        .icon_size(IconSize::Small)
 708                        // .disabled(!has_multiple_hunks)
 709                        .tooltip({
 710                            let focus_handle = editor.focus_handle(cx);
 711                            move |window, cx| {
 712                                Tooltip::for_action_in(
 713                                    "Next Hunk",
 714                                    &GoToHunk,
 715                                    &focus_handle,
 716                                    window,
 717                                    cx,
 718                                )
 719                            }
 720                        })
 721                        .on_click({
 722                            let editor = editor.clone();
 723                            move |_event, window, cx| {
 724                                editor.update(cx, |editor, cx| {
 725                                    let snapshot = editor.snapshot(window, cx);
 726                                    let position =
 727                                        hunk_range.end.to_point(&snapshot.buffer_snapshot);
 728                                    editor.go_to_hunk_before_or_after_position(
 729                                        &snapshot,
 730                                        position,
 731                                        Direction::Next,
 732                                        window,
 733                                        cx,
 734                                    );
 735                                    editor.expand_selected_diff_hunks(cx);
 736                                });
 737                            }
 738                        }),
 739                )
 740                .child(
 741                    IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
 742                        .shape(IconButtonShape::Square)
 743                        .icon_size(IconSize::Small)
 744                        // .disabled(!has_multiple_hunks)
 745                        .tooltip({
 746                            let focus_handle = editor.focus_handle(cx);
 747                            move |window, cx| {
 748                                Tooltip::for_action_in(
 749                                    "Previous Hunk",
 750                                    &GoToPreviousHunk,
 751                                    &focus_handle,
 752                                    window,
 753                                    cx,
 754                                )
 755                            }
 756                        })
 757                        .on_click({
 758                            let editor = editor.clone();
 759                            move |_event, window, cx| {
 760                                editor.update(cx, |editor, cx| {
 761                                    let snapshot = editor.snapshot(window, cx);
 762                                    let point =
 763                                        hunk_range.start.to_point(&snapshot.buffer_snapshot);
 764                                    editor.go_to_hunk_before_or_after_position(
 765                                        &snapshot,
 766                                        point,
 767                                        Direction::Prev,
 768                                        window,
 769                                        cx,
 770                                    );
 771                                    editor.expand_selected_diff_hunks(cx);
 772                                });
 773                            }
 774                        }),
 775                )
 776            },
 777        )
 778        .into_any_element()
 779}
 780
 781struct AgentDiffAddon;
 782
 783impl editor::Addon for AgentDiffAddon {
 784    fn to_any(&self) -> &dyn std::any::Any {
 785        self
 786    }
 787
 788    fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
 789        key_context.add("agent_diff");
 790    }
 791}
 792
 793pub struct AgentDiffToolbar {
 794    agent_diff: Option<WeakEntity<AgentDiff>>,
 795}
 796
 797impl AgentDiffToolbar {
 798    pub fn new() -> Self {
 799        Self { agent_diff: None }
 800    }
 801
 802    fn agent_diff(&self, _: &App) -> Option<Entity<AgentDiff>> {
 803        self.agent_diff.as_ref()?.upgrade()
 804    }
 805
 806    fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
 807        if let Some(agent_diff) = self.agent_diff(cx) {
 808            agent_diff.focus_handle(cx).focus(window);
 809        }
 810        let action = action.boxed_clone();
 811        cx.defer(move |cx| {
 812            cx.dispatch_action(action.as_ref());
 813        })
 814    }
 815}
 816
 817impl EventEmitter<ToolbarItemEvent> for AgentDiffToolbar {}
 818
 819impl ToolbarItemView for AgentDiffToolbar {
 820    fn set_active_pane_item(
 821        &mut self,
 822        active_pane_item: Option<&dyn ItemHandle>,
 823        _: &mut Window,
 824        cx: &mut Context<Self>,
 825    ) -> ToolbarItemLocation {
 826        self.agent_diff = active_pane_item
 827            .and_then(|item| item.act_as::<AgentDiff>(cx))
 828            .map(|entity| entity.downgrade());
 829        if self.agent_diff.is_some() {
 830            ToolbarItemLocation::PrimaryRight
 831        } else {
 832            ToolbarItemLocation::Hidden
 833        }
 834    }
 835
 836    fn pane_focus_update(
 837        &mut self,
 838        _pane_focused: bool,
 839        _window: &mut Window,
 840        _cx: &mut Context<Self>,
 841    ) {
 842    }
 843}
 844
 845impl Render for AgentDiffToolbar {
 846    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 847        let agent_diff = match self.agent_diff(cx) {
 848            Some(ad) => ad,
 849            None => return div(),
 850        };
 851
 852        let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
 853
 854        if is_empty {
 855            return div();
 856        }
 857
 858        let focus_handle = agent_diff.focus_handle(cx);
 859
 860        h_group_xl()
 861            .my_neg_1()
 862            .items_center()
 863            .p_1()
 864            .flex_wrap()
 865            .justify_between()
 866            .child(
 867                h_group_sm()
 868                    .child(
 869                        Button::new("reject-all", "Reject All")
 870                            .key_binding({
 871                                KeyBinding::for_action_in(&RejectAll, &focus_handle, window, cx)
 872                                    .map(|kb| kb.size(rems_from_px(12.)))
 873                            })
 874                            .on_click(cx.listener(|this, _, window, cx| {
 875                                this.dispatch_action(&RejectAll, window, cx)
 876                            })),
 877                    )
 878                    .child(
 879                        Button::new("keep-all", "Keep All")
 880                            .key_binding({
 881                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
 882                                    .map(|kb| kb.size(rems_from_px(12.)))
 883                            })
 884                            .on_click(cx.listener(|this, _, window, cx| {
 885                                this.dispatch_action(&KeepAll, window, cx)
 886                            })),
 887                    ),
 888            )
 889    }
 890}
 891
 892#[cfg(test)]
 893mod tests {
 894    use super::*;
 895    use crate::{ThreadStore, thread_store};
 896    use assistant_settings::AssistantSettings;
 897    use context_server::ContextServerSettings;
 898    use editor::EditorSettings;
 899    use gpui::TestAppContext;
 900    use project::{FakeFs, Project};
 901    use prompt_store::PromptBuilder;
 902    use serde_json::json;
 903    use settings::{Settings, SettingsStore};
 904    use std::sync::Arc;
 905    use theme::ThemeSettings;
 906    use util::path;
 907
 908    #[gpui::test]
 909    async fn test_agent_diff(cx: &mut TestAppContext) {
 910        cx.update(|cx| {
 911            let settings_store = SettingsStore::test(cx);
 912            cx.set_global(settings_store);
 913            language::init(cx);
 914            Project::init_settings(cx);
 915            AssistantSettings::register(cx);
 916            thread_store::init(cx);
 917            workspace::init_settings(cx);
 918            ThemeSettings::register(cx);
 919            ContextServerSettings::register(cx);
 920            EditorSettings::register(cx);
 921        });
 922
 923        let fs = FakeFs::new(cx.executor());
 924        fs.insert_tree(
 925            path!("/test"),
 926            json!({"file1": "abc\ndef\nghi\njkl\nmno\npqr\nstu\nvwx\nyz"}),
 927        )
 928        .await;
 929        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
 930        let buffer_path = project
 931            .read_with(cx, |project, cx| {
 932                project.find_project_path("test/file1", cx)
 933            })
 934            .unwrap();
 935
 936        let thread_store = cx
 937            .update(|cx| {
 938                ThreadStore::load(
 939                    project.clone(),
 940                    Arc::default(),
 941                    Arc::new(PromptBuilder::new(None).unwrap()),
 942                    cx,
 943                )
 944            })
 945            .await;
 946        let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
 947        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
 948
 949        let (workspace, cx) =
 950            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 951        let agent_diff = cx.new_window_entity(|window, cx| {
 952            AgentDiff::new(thread.clone(), workspace.downgrade(), window, cx)
 953        });
 954        let editor = agent_diff.read_with(cx, |diff, _cx| diff.editor.clone());
 955
 956        let buffer = project
 957            .update(cx, |project, cx| project.open_buffer(buffer_path, cx))
 958            .await
 959            .unwrap();
 960        cx.update(|_, cx| {
 961            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
 962            buffer.update(cx, |buffer, cx| {
 963                buffer
 964                    .edit(
 965                        [
 966                            (Point::new(1, 1)..Point::new(1, 2), "E"),
 967                            (Point::new(3, 2)..Point::new(3, 3), "L"),
 968                            (Point::new(5, 0)..Point::new(5, 1), "P"),
 969                            (Point::new(7, 1)..Point::new(7, 2), "W"),
 970                        ],
 971                        None,
 972                        cx,
 973                    )
 974                    .unwrap()
 975            });
 976            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
 977        });
 978        cx.run_until_parked();
 979
 980        // When opening the assistant diff, the cursor is positioned on the first hunk.
 981        assert_eq!(
 982            editor.read_with(cx, |editor, cx| editor.text(cx)),
 983            "abc\ndef\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
 984        );
 985        assert_eq!(
 986            editor
 987                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
 988                .range(),
 989            Point::new(1, 0)..Point::new(1, 0)
 990        );
 991
 992        // After keeping a hunk, the cursor should be positioned on the second hunk.
 993        agent_diff.update_in(cx, |diff, window, cx| diff.keep(&crate::Keep, window, cx));
 994        cx.run_until_parked();
 995        assert_eq!(
 996            editor.read_with(cx, |editor, cx| editor.text(cx)),
 997            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
 998        );
 999        assert_eq!(
1000            editor
1001                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
1002                .range(),
1003            Point::new(3, 0)..Point::new(3, 0)
1004        );
1005
1006        // Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
1007        editor.update_in(cx, |editor, window, cx| {
1008            editor.change_selections(None, window, cx, |selections| {
1009                selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
1010            });
1011        });
1012        agent_diff.update_in(cx, |diff, window, cx| {
1013            diff.reject(&crate::Reject, window, cx)
1014        });
1015        cx.run_until_parked();
1016        assert_eq!(
1017            editor.read_with(cx, |editor, cx| editor.text(cx)),
1018            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nyz"
1019        );
1020        assert_eq!(
1021            editor
1022                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
1023                .range(),
1024            Point::new(3, 0)..Point::new(3, 0)
1025        );
1026
1027        // Keeping a range that doesn't intersect the current selection doesn't move it.
1028        agent_diff.update_in(cx, |diff, window, cx| {
1029            let position = editor
1030                .read(cx)
1031                .buffer()
1032                .read(cx)
1033                .read(cx)
1034                .anchor_before(Point::new(7, 0));
1035            diff.keep_edits_in_ranges(vec![position..position], window, cx)
1036        });
1037        cx.run_until_parked();
1038        assert_eq!(
1039            editor.read_with(cx, |editor, cx| editor.text(cx)),
1040            "abc\ndEf\nghi\njkl\njkL\nmno\nPqr\nstu\nvwx\nyz"
1041        );
1042        assert_eq!(
1043            editor
1044                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
1045                .range(),
1046            Point::new(3, 0)..Point::new(3, 0)
1047        );
1048    }
1049}