agent_diff.rs

   1use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
   2use acp_thread::{AcpThread, AcpThreadEvent};
   3use action_log::ActionLogTelemetry;
   4use agent_settings::AgentSettings;
   5use anyhow::Result;
   6use buffer_diff::DiffHunkStatus;
   7use collections::{HashMap, HashSet};
   8use editor::{
   9    Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
  10    SelectionEffects, ToPoint,
  11    actions::{GoToHunk, GoToPreviousHunk},
  12    multibuffer_context_lines,
  13    scroll::Autoscroll,
  14};
  15
  16use gpui::{
  17    Action, AnyElement, App, AppContext, Empty, Entity, EventEmitter, FocusHandle, Focusable,
  18    Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
  19};
  20
  21use language::{Buffer, Capability, OffsetRangeExt, Point};
  22use multi_buffer::PathKey;
  23use project::{Project, ProjectItem, ProjectPath};
  24use settings::{Settings, SettingsStore};
  25use std::{
  26    any::{Any, TypeId},
  27    collections::hash_map::Entry,
  28    ops::Range,
  29    sync::Arc,
  30};
  31use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
  32use util::ResultExt;
  33use workspace::{
  34    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
  35    Workspace,
  36    item::{ItemEvent, SaveOptions, TabContentParams},
  37    searchable::SearchableItemHandle,
  38};
  39use zed_actions::assistant::ToggleFocus;
  40
  41pub struct AgentDiffPane {
  42    multibuffer: Entity<MultiBuffer>,
  43    editor: Entity<Editor>,
  44    thread: Entity<AcpThread>,
  45    focus_handle: FocusHandle,
  46    workspace: WeakEntity<Workspace>,
  47    title: SharedString,
  48    _subscriptions: Vec<Subscription>,
  49}
  50
  51impl AgentDiffPane {
  52    pub fn deploy(
  53        thread: Entity<AcpThread>,
  54        workspace: WeakEntity<Workspace>,
  55        window: &mut Window,
  56        cx: &mut App,
  57    ) -> Result<Entity<Self>> {
  58        workspace.update(cx, |workspace, cx| {
  59            Self::deploy_in_workspace(thread, workspace, window, cx)
  60        })
  61    }
  62
  63    pub fn deploy_in_workspace(
  64        thread: Entity<AcpThread>,
  65        workspace: &mut Workspace,
  66        window: &mut Window,
  67        cx: &mut Context<Workspace>,
  68    ) -> Entity<Self> {
  69        let existing_diff = workspace
  70            .items_of_type::<AgentDiffPane>(cx)
  71            .find(|diff| diff.read(cx).thread == thread);
  72
  73        if let Some(existing_diff) = existing_diff {
  74            workspace.activate_item(&existing_diff, true, true, window, cx);
  75            existing_diff
  76        } else {
  77            let agent_diff = cx
  78                .new(|cx| AgentDiffPane::new(thread.clone(), workspace.weak_handle(), window, cx));
  79            workspace.add_item_to_center(Box::new(agent_diff.clone()), window, cx);
  80            agent_diff
  81        }
  82    }
  83
  84    pub fn new(
  85        thread: Entity<AcpThread>,
  86        workspace: WeakEntity<Workspace>,
  87        window: &mut Window,
  88        cx: &mut Context<Self>,
  89    ) -> Self {
  90        let focus_handle = cx.focus_handle();
  91        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
  92
  93        let project = thread.read(cx).project().clone();
  94        let editor = cx.new(|cx| {
  95            let mut editor =
  96                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
  97            editor.disable_inline_diagnostics();
  98            editor.set_expand_all_diff_hunks(cx);
  99            editor.set_render_diff_hunk_controls(diff_hunk_controls(&thread), cx);
 100            editor.register_addon(AgentDiffAddon);
 101            editor
 102        });
 103
 104        let action_log = thread.read(cx).action_log().clone();
 105
 106        let mut this = Self {
 107            _subscriptions: vec![
 108                cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
 109                    this.update_excerpts(window, cx)
 110                }),
 111                cx.subscribe(&thread, |this, _thread, event, cx| {
 112                    this.handle_acp_thread_event(event, cx)
 113                }),
 114            ],
 115            title: SharedString::default(),
 116            multibuffer,
 117            editor,
 118            thread,
 119            focus_handle,
 120            workspace,
 121        };
 122        this.update_excerpts(window, cx);
 123        this.update_title(cx);
 124        this
 125    }
 126
 127    fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 128        let changed_buffers = self
 129            .thread
 130            .read(cx)
 131            .action_log()
 132            .read(cx)
 133            .changed_buffers(cx);
 134
 135        // Sort edited files alphabetically for consistency with Git diff view
 136        let mut sorted_buffers: Vec<_> = changed_buffers.iter().collect();
 137        sorted_buffers.sort_by(|(buffer_a, _), (buffer_b, _)| {
 138            let path_a = buffer_a.read(cx).file().map(|f| f.path().clone());
 139            let path_b = buffer_b.read(cx).file().map(|f| f.path().clone());
 140            path_a.cmp(&path_b)
 141        });
 142
 143        let mut paths_to_delete = self
 144            .multibuffer
 145            .read(cx)
 146            .paths()
 147            .cloned()
 148            .collect::<HashSet<_>>();
 149
 150        for (buffer, diff_handle) in sorted_buffers {
 151            if buffer.read(cx).file().is_none() {
 152                continue;
 153            }
 154
 155            let path_key = PathKey::for_buffer(&buffer, cx);
 156            paths_to_delete.remove(&path_key);
 157
 158            let snapshot = buffer.read(cx).snapshot();
 159
 160            let diff_hunk_ranges = diff_handle
 161                .read(cx)
 162                .snapshot(cx)
 163                .hunks_intersecting_range(
 164                    language::Anchor::min_max_range_for_buffer(snapshot.remote_id()),
 165                    &snapshot,
 166                )
 167                .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
 168                .collect::<Vec<_>>();
 169
 170            let (was_empty, is_excerpt_newly_added) =
 171                self.multibuffer.update(cx, |multibuffer, cx| {
 172                    let was_empty = multibuffer.is_empty();
 173                    let (_, is_excerpt_newly_added) = multibuffer.set_excerpts_for_path(
 174                        path_key.clone(),
 175                        buffer.clone(),
 176                        diff_hunk_ranges,
 177                        multibuffer_context_lines(cx),
 178                        cx,
 179                    );
 180                    multibuffer.add_diff(diff_handle.clone(), cx);
 181                    (was_empty, is_excerpt_newly_added)
 182                });
 183
 184            self.editor.update(cx, |editor, cx| {
 185                if was_empty {
 186                    let first_hunk = editor
 187                        .diff_hunks_in_ranges(
 188                            &[editor::Anchor::min()..editor::Anchor::max()],
 189                            &self.multibuffer.read(cx).read(cx),
 190                        )
 191                        .next();
 192
 193                    if let Some(first_hunk) = first_hunk {
 194                        let first_hunk_start = first_hunk.multi_buffer_range().start;
 195                        editor.change_selections(Default::default(), window, cx, |selections| {
 196                            selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
 197                        })
 198                    }
 199                }
 200
 201                if is_excerpt_newly_added
 202                    && buffer
 203                        .read(cx)
 204                        .file()
 205                        .is_some_and(|file| file.disk_state().is_deleted())
 206                {
 207                    editor.fold_buffer(snapshot.text.remote_id(), cx)
 208                }
 209            });
 210        }
 211
 212        self.multibuffer.update(cx, |multibuffer, cx| {
 213            for path in paths_to_delete {
 214                multibuffer.remove_excerpts_for_path(path, cx);
 215            }
 216        });
 217
 218        if self.multibuffer.read(cx).is_empty()
 219            && self
 220                .editor
 221                .read(cx)
 222                .focus_handle(cx)
 223                .contains_focused(window, cx)
 224        {
 225            self.focus_handle.focus(window, cx);
 226        } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
 227            self.editor.update(cx, |editor, cx| {
 228                editor.focus_handle(cx).focus(window, cx);
 229            });
 230        }
 231    }
 232
 233    fn update_title(&mut self, cx: &mut Context<Self>) {
 234        let new_title = self.thread.read(cx).title();
 235        if new_title != self.title {
 236            self.title = new_title;
 237            cx.emit(EditorEvent::TitleChanged);
 238        }
 239    }
 240
 241    fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) {
 242        if let AcpThreadEvent::TitleUpdated = event {
 243            self.update_title(cx)
 244        }
 245    }
 246
 247    pub fn move_to_path(&self, path_key: PathKey, window: &mut Window, cx: &mut App) {
 248        if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
 249            self.editor.update(cx, |editor, cx| {
 250                let first_hunk = editor
 251                    .diff_hunks_in_ranges(
 252                        &[position..editor::Anchor::max()],
 253                        &self.multibuffer.read(cx).read(cx),
 254                    )
 255                    .next();
 256
 257                if let Some(first_hunk) = first_hunk {
 258                    let first_hunk_start = first_hunk.multi_buffer_range().start;
 259                    editor.change_selections(Default::default(), window, cx, |selections| {
 260                        selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
 261                    })
 262                }
 263            });
 264        }
 265    }
 266
 267    fn keep(&mut self, _: &Keep, window: &mut Window, cx: &mut Context<Self>) {
 268        self.editor.update(cx, |editor, cx| {
 269            let snapshot = editor.buffer().read(cx).snapshot(cx);
 270            keep_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
 271        });
 272    }
 273
 274    fn reject(&mut self, _: &Reject, window: &mut Window, cx: &mut Context<Self>) {
 275        self.editor.update(cx, |editor, cx| {
 276            let snapshot = editor.buffer().read(cx).snapshot(cx);
 277            reject_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
 278        });
 279    }
 280
 281    fn reject_all(&mut self, _: &RejectAll, window: &mut Window, cx: &mut Context<Self>) {
 282        self.editor.update(cx, |editor, cx| {
 283            let snapshot = editor.buffer().read(cx).snapshot(cx);
 284            reject_edits_in_ranges(
 285                editor,
 286                &snapshot,
 287                &self.thread,
 288                vec![editor::Anchor::min()..editor::Anchor::max()],
 289                window,
 290                cx,
 291            );
 292        });
 293    }
 294
 295    fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
 296        let telemetry = ActionLogTelemetry::from(self.thread.read(cx));
 297        let action_log = self.thread.read(cx).action_log().clone();
 298        action_log.update(cx, |action_log, cx| {
 299            action_log.keep_all_edits(Some(telemetry), cx)
 300        });
 301    }
 302}
 303
 304fn keep_edits_in_selection(
 305    editor: &mut Editor,
 306    buffer_snapshot: &MultiBufferSnapshot,
 307    thread: &Entity<AcpThread>,
 308    window: &mut Window,
 309    cx: &mut Context<Editor>,
 310) {
 311    let ranges = editor
 312        .selections
 313        .disjoint_anchor_ranges()
 314        .collect::<Vec<_>>();
 315
 316    keep_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx)
 317}
 318
 319fn reject_edits_in_selection(
 320    editor: &mut Editor,
 321    buffer_snapshot: &MultiBufferSnapshot,
 322    thread: &Entity<AcpThread>,
 323    window: &mut Window,
 324    cx: &mut Context<Editor>,
 325) {
 326    let ranges = editor
 327        .selections
 328        .disjoint_anchor_ranges()
 329        .collect::<Vec<_>>();
 330    reject_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx)
 331}
 332
 333fn keep_edits_in_ranges(
 334    editor: &mut Editor,
 335    buffer_snapshot: &MultiBufferSnapshot,
 336    thread: &Entity<AcpThread>,
 337    ranges: Vec<Range<editor::Anchor>>,
 338    window: &mut Window,
 339    cx: &mut Context<Editor>,
 340) {
 341    let diff_hunks_in_ranges = editor
 342        .diff_hunks_in_ranges(&ranges, buffer_snapshot)
 343        .collect::<Vec<_>>();
 344
 345    update_editor_selection(editor, buffer_snapshot, &diff_hunks_in_ranges, window, cx);
 346
 347    let multibuffer = editor.buffer().clone();
 348    for hunk in &diff_hunks_in_ranges {
 349        let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
 350        if let Some(buffer) = buffer {
 351            let action_log = thread.read(cx).action_log().clone();
 352            let telemetry = ActionLogTelemetry::from(thread.read(cx));
 353            action_log.update(cx, |action_log, cx| {
 354                action_log.keep_edits_in_range(
 355                    buffer,
 356                    hunk.buffer_range.clone(),
 357                    Some(telemetry),
 358                    cx,
 359                )
 360            });
 361        }
 362    }
 363}
 364
 365fn reject_edits_in_ranges(
 366    editor: &mut Editor,
 367    buffer_snapshot: &MultiBufferSnapshot,
 368    thread: &Entity<AcpThread>,
 369    ranges: Vec<Range<editor::Anchor>>,
 370    window: &mut Window,
 371    cx: &mut Context<Editor>,
 372) {
 373    let diff_hunks_in_ranges = editor
 374        .diff_hunks_in_ranges(&ranges, buffer_snapshot)
 375        .collect::<Vec<_>>();
 376
 377    update_editor_selection(editor, buffer_snapshot, &diff_hunks_in_ranges, window, cx);
 378
 379    let multibuffer = editor.buffer().clone();
 380
 381    let mut ranges_by_buffer = HashMap::default();
 382    for hunk in &diff_hunks_in_ranges {
 383        let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
 384        if let Some(buffer) = buffer {
 385            ranges_by_buffer
 386                .entry(buffer.clone())
 387                .or_insert_with(Vec::new)
 388                .push(hunk.buffer_range.clone());
 389        }
 390    }
 391
 392    let action_log = thread.read(cx).action_log().clone();
 393    let telemetry = ActionLogTelemetry::from(thread.read(cx));
 394    for (buffer, ranges) in ranges_by_buffer {
 395        action_log
 396            .update(cx, |action_log, cx| {
 397                action_log.reject_edits_in_ranges(buffer, ranges, Some(telemetry.clone()), cx)
 398            })
 399            .detach_and_log_err(cx);
 400    }
 401}
 402
 403fn update_editor_selection(
 404    editor: &mut Editor,
 405    buffer_snapshot: &MultiBufferSnapshot,
 406    diff_hunks: &[multi_buffer::MultiBufferDiffHunk],
 407    window: &mut Window,
 408    cx: &mut Context<Editor>,
 409) {
 410    let newest_cursor = editor
 411        .selections
 412        .newest::<Point>(&editor.display_snapshot(cx))
 413        .head();
 414
 415    if !diff_hunks.iter().any(|hunk| {
 416        hunk.row_range
 417            .contains(&multi_buffer::MultiBufferRow(newest_cursor.row))
 418    }) {
 419        return;
 420    }
 421
 422    let target_hunk = {
 423        diff_hunks
 424            .last()
 425            .and_then(|last_kept_hunk| {
 426                let last_kept_hunk_end = last_kept_hunk.multi_buffer_range().end;
 427                editor
 428                    .diff_hunks_in_ranges(
 429                        &[last_kept_hunk_end..editor::Anchor::max()],
 430                        buffer_snapshot,
 431                    )
 432                    .nth(1)
 433            })
 434            .or_else(|| {
 435                let first_kept_hunk = diff_hunks.first()?;
 436                let first_kept_hunk_start = first_kept_hunk.multi_buffer_range().start;
 437                editor
 438                    .diff_hunks_in_ranges(
 439                        &[editor::Anchor::min()..first_kept_hunk_start],
 440                        buffer_snapshot,
 441                    )
 442                    .next()
 443            })
 444    };
 445
 446    if let Some(target_hunk) = target_hunk {
 447        editor.change_selections(Default::default(), window, cx, |selections| {
 448            let next_hunk_start = target_hunk.multi_buffer_range().start;
 449            selections.select_anchor_ranges([next_hunk_start..next_hunk_start]);
 450        })
 451    }
 452}
 453
 454impl EventEmitter<EditorEvent> for AgentDiffPane {}
 455
 456impl Focusable for AgentDiffPane {
 457    fn focus_handle(&self, cx: &App) -> FocusHandle {
 458        if self.multibuffer.read(cx).is_empty() {
 459            self.focus_handle.clone()
 460        } else {
 461            self.editor.focus_handle(cx)
 462        }
 463    }
 464}
 465
 466impl Item for AgentDiffPane {
 467    type Event = EditorEvent;
 468
 469    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 470        Some(Icon::new(IconName::ZedAssistant).color(Color::Muted))
 471    }
 472
 473    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 474        Editor::to_item_events(event, f)
 475    }
 476
 477    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 478        self.editor
 479            .update(cx, |editor, cx| editor.deactivated(window, cx));
 480    }
 481
 482    fn navigate(
 483        &mut self,
 484        data: Arc<dyn Any + Send>,
 485        window: &mut Window,
 486        cx: &mut Context<Self>,
 487    ) -> bool {
 488        self.editor
 489            .update(cx, |editor, cx| editor.navigate(data, window, cx))
 490    }
 491
 492    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
 493        Some("Agent Diff".into())
 494    }
 495
 496    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
 497        let title = self.thread.read(cx).title();
 498        Label::new(format!("Review: {}", title))
 499            .color(if params.selected {
 500                Color::Default
 501            } else {
 502                Color::Muted
 503            })
 504            .into_any_element()
 505    }
 506
 507    fn telemetry_event_text(&self) -> Option<&'static str> {
 508        Some("Assistant Diff Opened")
 509    }
 510
 511    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
 512        Some(Box::new(self.editor.clone()))
 513    }
 514
 515    fn for_each_project_item(
 516        &self,
 517        cx: &App,
 518        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
 519    ) {
 520        self.editor.for_each_project_item(cx, f)
 521    }
 522
 523    fn set_nav_history(
 524        &mut self,
 525        nav_history: ItemNavHistory,
 526        _: &mut Window,
 527        cx: &mut Context<Self>,
 528    ) {
 529        self.editor.update(cx, |editor, _| {
 530            editor.set_nav_history(Some(nav_history));
 531        });
 532    }
 533
 534    fn can_split(&self) -> bool {
 535        true
 536    }
 537
 538    fn clone_on_split(
 539        &self,
 540        _workspace_id: Option<workspace::WorkspaceId>,
 541        window: &mut Window,
 542        cx: &mut Context<Self>,
 543    ) -> Task<Option<Entity<Self>>>
 544    where
 545        Self: Sized,
 546    {
 547        Task::ready(Some(cx.new(|cx| {
 548            Self::new(self.thread.clone(), self.workspace.clone(), window, cx)
 549        })))
 550    }
 551
 552    fn is_dirty(&self, cx: &App) -> bool {
 553        self.multibuffer.read(cx).is_dirty(cx)
 554    }
 555
 556    fn has_conflict(&self, cx: &App) -> bool {
 557        self.multibuffer.read(cx).has_conflict(cx)
 558    }
 559
 560    fn can_save(&self, _: &App) -> bool {
 561        true
 562    }
 563
 564    fn save(
 565        &mut self,
 566        options: SaveOptions,
 567        project: Entity<Project>,
 568        window: &mut Window,
 569        cx: &mut Context<Self>,
 570    ) -> Task<Result<()>> {
 571        self.editor.save(options, project, window, cx)
 572    }
 573
 574    fn save_as(
 575        &mut self,
 576        _: Entity<Project>,
 577        _: ProjectPath,
 578        _window: &mut Window,
 579        _: &mut Context<Self>,
 580    ) -> Task<Result<()>> {
 581        unreachable!()
 582    }
 583
 584    fn reload(
 585        &mut self,
 586        project: Entity<Project>,
 587        window: &mut Window,
 588        cx: &mut Context<Self>,
 589    ) -> Task<Result<()>> {
 590        self.editor.reload(project, window, cx)
 591    }
 592
 593    fn act_as_type<'a>(
 594        &'a self,
 595        type_id: TypeId,
 596        self_handle: &'a Entity<Self>,
 597        _: &'a App,
 598    ) -> Option<gpui::AnyEntity> {
 599        if type_id == TypeId::of::<Self>() {
 600            Some(self_handle.clone().into())
 601        } else if type_id == TypeId::of::<Editor>() {
 602            Some(self.editor.clone().into())
 603        } else {
 604            None
 605        }
 606    }
 607
 608    fn added_to_workspace(
 609        &mut self,
 610        workspace: &mut Workspace,
 611        window: &mut Window,
 612        cx: &mut Context<Self>,
 613    ) {
 614        self.editor.update(cx, |editor, cx| {
 615            editor.added_to_workspace(workspace, window, cx)
 616        });
 617    }
 618
 619    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 620        "Agent Diff".into()
 621    }
 622}
 623
 624impl Render for AgentDiffPane {
 625    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 626        let is_empty = self.multibuffer.read(cx).is_empty();
 627        let focus_handle = &self.focus_handle;
 628
 629        div()
 630            .track_focus(focus_handle)
 631            .key_context(if is_empty { "EmptyPane" } else { "AgentDiff" })
 632            .on_action(cx.listener(Self::keep))
 633            .on_action(cx.listener(Self::reject))
 634            .on_action(cx.listener(Self::reject_all))
 635            .on_action(cx.listener(Self::keep_all))
 636            .bg(cx.theme().colors().editor_background)
 637            .flex()
 638            .items_center()
 639            .justify_center()
 640            .size_full()
 641            .when(is_empty, |el| {
 642                el.child(
 643                    v_flex()
 644                        .items_center()
 645                        .gap_2()
 646                        .child("No changes to review")
 647                        .child(
 648                            Button::new("continue-iterating", "Continue Iterating")
 649                                .style(ButtonStyle::Filled)
 650                                .icon(IconName::ForwardArrow)
 651                                .icon_position(IconPosition::Start)
 652                                .icon_size(IconSize::Small)
 653                                .icon_color(Color::Muted)
 654                                .full_width()
 655                                .key_binding(KeyBinding::for_action_in(
 656                                    &ToggleFocus,
 657                                    &focus_handle.clone(),
 658                                    cx,
 659                                ))
 660                                .on_click(|_event, window, cx| {
 661                                    window.dispatch_action(ToggleFocus.boxed_clone(), cx)
 662                                }),
 663                        ),
 664                )
 665            })
 666            .when(!is_empty, |el| el.child(self.editor.clone()))
 667    }
 668}
 669
 670fn diff_hunk_controls(thread: &Entity<AcpThread>) -> editor::RenderDiffHunkControlsFn {
 671    let thread = thread.clone();
 672
 673    Arc::new(
 674        move |row, status, hunk_range, is_created_file, line_height, editor, _, cx| {
 675            {
 676                render_diff_hunk_controls(
 677                    row,
 678                    status,
 679                    hunk_range,
 680                    is_created_file,
 681                    line_height,
 682                    &thread,
 683                    editor,
 684                    cx,
 685                )
 686            }
 687        },
 688    )
 689}
 690
 691fn render_diff_hunk_controls(
 692    row: u32,
 693    _status: &DiffHunkStatus,
 694    hunk_range: Range<editor::Anchor>,
 695    is_created_file: bool,
 696    line_height: Pixels,
 697    thread: &Entity<AcpThread>,
 698    editor: &Entity<Editor>,
 699    cx: &mut App,
 700) -> AnyElement {
 701    let editor = editor.clone();
 702
 703    h_flex()
 704        .h(line_height)
 705        .mr_0p5()
 706        .gap_1()
 707        .px_0p5()
 708        .pb_1()
 709        .border_x_1()
 710        .border_b_1()
 711        .border_color(cx.theme().colors().border)
 712        .rounded_b_md()
 713        .bg(cx.theme().colors().editor_background)
 714        .gap_1()
 715        .block_mouse_except_scroll()
 716        .shadow_md()
 717        .children(vec![
 718            Button::new(("reject", row as u64), "Reject")
 719                .disabled(is_created_file)
 720                .key_binding(
 721                    KeyBinding::for_action_in(&Reject, &editor.read(cx).focus_handle(cx), cx)
 722                        .map(|kb| kb.size(rems_from_px(12.))),
 723                )
 724                .on_click({
 725                    let editor = editor.clone();
 726                    let thread = thread.clone();
 727                    move |_event, window, cx| {
 728                        editor.update(cx, |editor, cx| {
 729                            let snapshot = editor.buffer().read(cx).snapshot(cx);
 730                            reject_edits_in_ranges(
 731                                editor,
 732                                &snapshot,
 733                                &thread,
 734                                vec![hunk_range.start..hunk_range.start],
 735                                window,
 736                                cx,
 737                            );
 738                        })
 739                    }
 740                }),
 741            Button::new(("keep", row as u64), "Keep")
 742                .key_binding(
 743                    KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), cx)
 744                        .map(|kb| kb.size(rems_from_px(12.))),
 745                )
 746                .on_click({
 747                    let editor = editor.clone();
 748                    let thread = thread.clone();
 749                    move |_event, window, cx| {
 750                        editor.update(cx, |editor, cx| {
 751                            let snapshot = editor.buffer().read(cx).snapshot(cx);
 752                            keep_edits_in_ranges(
 753                                editor,
 754                                &snapshot,
 755                                &thread,
 756                                vec![hunk_range.start..hunk_range.start],
 757                                window,
 758                                cx,
 759                            );
 760                        });
 761                    }
 762                }),
 763        ])
 764        .when(
 765            !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
 766            |el| {
 767                el.child(
 768                    IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
 769                        .shape(IconButtonShape::Square)
 770                        .icon_size(IconSize::Small)
 771                        // .disabled(!has_multiple_hunks)
 772                        .tooltip({
 773                            let focus_handle = editor.focus_handle(cx);
 774                            move |_window, cx| {
 775                                Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, cx)
 776                            }
 777                        })
 778                        .on_click({
 779                            let editor = editor.clone();
 780                            move |_event, window, cx| {
 781                                editor.update(cx, |editor, cx| {
 782                                    let snapshot = editor.snapshot(window, cx);
 783                                    let position =
 784                                        hunk_range.end.to_point(&snapshot.buffer_snapshot());
 785                                    editor.go_to_hunk_before_or_after_position(
 786                                        &snapshot,
 787                                        position,
 788                                        Direction::Next,
 789                                        window,
 790                                        cx,
 791                                    );
 792                                    editor.expand_selected_diff_hunks(cx);
 793                                });
 794                            }
 795                        }),
 796                )
 797                .child(
 798                    IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
 799                        .shape(IconButtonShape::Square)
 800                        .icon_size(IconSize::Small)
 801                        // .disabled(!has_multiple_hunks)
 802                        .tooltip({
 803                            let focus_handle = editor.focus_handle(cx);
 804                            move |_window, cx| {
 805                                Tooltip::for_action_in(
 806                                    "Previous Hunk",
 807                                    &GoToPreviousHunk,
 808                                    &focus_handle,
 809                                    cx,
 810                                )
 811                            }
 812                        })
 813                        .on_click({
 814                            let editor = editor.clone();
 815                            move |_event, window, cx| {
 816                                editor.update(cx, |editor, cx| {
 817                                    let snapshot = editor.snapshot(window, cx);
 818                                    let point =
 819                                        hunk_range.start.to_point(&snapshot.buffer_snapshot());
 820                                    editor.go_to_hunk_before_or_after_position(
 821                                        &snapshot,
 822                                        point,
 823                                        Direction::Prev,
 824                                        window,
 825                                        cx,
 826                                    );
 827                                    editor.expand_selected_diff_hunks(cx);
 828                                });
 829                            }
 830                        }),
 831                )
 832            },
 833        )
 834        .into_any_element()
 835}
 836
 837struct AgentDiffAddon;
 838
 839impl editor::Addon for AgentDiffAddon {
 840    fn to_any(&self) -> &dyn std::any::Any {
 841        self
 842    }
 843
 844    fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
 845        key_context.add("agent_diff");
 846    }
 847}
 848
 849pub struct AgentDiffToolbar {
 850    active_item: Option<AgentDiffToolbarItem>,
 851    _settings_subscription: Subscription,
 852}
 853
 854pub enum AgentDiffToolbarItem {
 855    Pane(WeakEntity<AgentDiffPane>),
 856    Editor {
 857        editor: WeakEntity<Editor>,
 858        state: EditorState,
 859        _diff_subscription: Subscription,
 860    },
 861}
 862
 863impl AgentDiffToolbar {
 864    pub fn new(cx: &mut Context<Self>) -> Self {
 865        Self {
 866            active_item: None,
 867            _settings_subscription: cx.observe_global::<SettingsStore>(Self::update_location),
 868        }
 869    }
 870
 871    fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
 872        let Some(active_item) = self.active_item.as_ref() else {
 873            return;
 874        };
 875
 876        match active_item {
 877            AgentDiffToolbarItem::Pane(agent_diff) => {
 878                if let Some(agent_diff) = agent_diff.upgrade() {
 879                    agent_diff.focus_handle(cx).focus(window, cx);
 880                }
 881            }
 882            AgentDiffToolbarItem::Editor { editor, .. } => {
 883                if let Some(editor) = editor.upgrade() {
 884                    editor.read(cx).focus_handle(cx).focus(window, cx);
 885                }
 886            }
 887        }
 888
 889        let action = action.boxed_clone();
 890        cx.defer(move |cx| {
 891            cx.dispatch_action(action.as_ref());
 892        })
 893    }
 894
 895    fn handle_diff_notify(&mut self, agent_diff: Entity<AgentDiff>, cx: &mut Context<Self>) {
 896        let Some(AgentDiffToolbarItem::Editor { editor, state, .. }) = self.active_item.as_mut()
 897        else {
 898            return;
 899        };
 900
 901        *state = agent_diff.read(cx).editor_state(editor);
 902        self.update_location(cx);
 903        cx.notify();
 904    }
 905
 906    fn update_location(&mut self, cx: &mut Context<Self>) {
 907        let location = self.location(cx);
 908        cx.emit(ToolbarItemEvent::ChangeLocation(location));
 909    }
 910
 911    fn location(&self, cx: &App) -> ToolbarItemLocation {
 912        if !EditorSettings::get_global(cx).toolbar.agent_review {
 913            return ToolbarItemLocation::Hidden;
 914        }
 915
 916        match &self.active_item {
 917            None => ToolbarItemLocation::Hidden,
 918            Some(AgentDiffToolbarItem::Pane(_)) => ToolbarItemLocation::PrimaryRight,
 919            Some(AgentDiffToolbarItem::Editor { state, .. }) => match state {
 920                EditorState::Reviewing => ToolbarItemLocation::PrimaryRight,
 921                EditorState::Idle => ToolbarItemLocation::Hidden,
 922            },
 923        }
 924    }
 925}
 926
 927impl EventEmitter<ToolbarItemEvent> for AgentDiffToolbar {}
 928
 929impl ToolbarItemView for AgentDiffToolbar {
 930    fn set_active_pane_item(
 931        &mut self,
 932        active_pane_item: Option<&dyn ItemHandle>,
 933        _: &mut Window,
 934        cx: &mut Context<Self>,
 935    ) -> ToolbarItemLocation {
 936        if let Some(item) = active_pane_item {
 937            if let Some(pane) = item.act_as::<AgentDiffPane>(cx) {
 938                self.active_item = Some(AgentDiffToolbarItem::Pane(pane.downgrade()));
 939                return self.location(cx);
 940            }
 941
 942            if let Some(editor) = item.act_as::<Editor>(cx)
 943                && editor.read(cx).mode().is_full()
 944            {
 945                let agent_diff = AgentDiff::global(cx);
 946
 947                self.active_item = Some(AgentDiffToolbarItem::Editor {
 948                    editor: editor.downgrade(),
 949                    state: agent_diff.read(cx).editor_state(&editor.downgrade()),
 950                    _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify),
 951                });
 952
 953                return self.location(cx);
 954            }
 955        }
 956
 957        self.active_item = None;
 958        self.location(cx)
 959    }
 960
 961    fn pane_focus_update(
 962        &mut self,
 963        _pane_focused: bool,
 964        _window: &mut Window,
 965        _cx: &mut Context<Self>,
 966    ) {
 967    }
 968}
 969
 970impl Render for AgentDiffToolbar {
 971    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 972        let spinner_icon = div()
 973            .px_0p5()
 974            .id("generating")
 975            .tooltip(Tooltip::text("Generating Changes…"))
 976            .child(
 977                Icon::new(IconName::LoadCircle)
 978                    .size(IconSize::Small)
 979                    .color(Color::Accent)
 980                    .with_rotate_animation(3),
 981            )
 982            .into_any();
 983
 984        let Some(active_item) = self.active_item.as_ref() else {
 985            return Empty.into_any();
 986        };
 987
 988        match active_item {
 989            AgentDiffToolbarItem::Editor { editor, state, .. } => {
 990                let Some(editor) = editor.upgrade() else {
 991                    return Empty.into_any();
 992                };
 993
 994                let editor_focus_handle = editor.read(cx).focus_handle(cx);
 995
 996                let content = match state {
 997                    EditorState::Idle => return Empty.into_any(),
 998                    EditorState::Reviewing => vec![
 999                        h_flex()
1000                            .child(
1001                                IconButton::new("hunk-up", IconName::ArrowUp)
1002                                    .icon_size(IconSize::Small)
1003                                    .tooltip(Tooltip::for_action_title_in(
1004                                        "Previous Hunk",
1005                                        &GoToPreviousHunk,
1006                                        &editor_focus_handle,
1007                                    ))
1008                                    .on_click({
1009                                        let editor_focus_handle = editor_focus_handle.clone();
1010                                        move |_, window, cx| {
1011                                            editor_focus_handle.dispatch_action(
1012                                                &GoToPreviousHunk,
1013                                                window,
1014                                                cx,
1015                                            );
1016                                        }
1017                                    }),
1018                            )
1019                            .child(
1020                                IconButton::new("hunk-down", IconName::ArrowDown)
1021                                    .icon_size(IconSize::Small)
1022                                    .tooltip(Tooltip::for_action_title_in(
1023                                        "Next Hunk",
1024                                        &GoToHunk,
1025                                        &editor_focus_handle,
1026                                    ))
1027                                    .on_click({
1028                                        let editor_focus_handle = editor_focus_handle.clone();
1029                                        move |_, window, cx| {
1030                                            editor_focus_handle
1031                                                .dispatch_action(&GoToHunk, window, cx);
1032                                        }
1033                                    }),
1034                            )
1035                            .into_any_element(),
1036                        vertical_divider().into_any_element(),
1037                        h_flex()
1038                            .gap_0p5()
1039                            .child(
1040                                Button::new("reject-all", "Reject All")
1041                                    .key_binding({
1042                                        KeyBinding::for_action_in(
1043                                            &RejectAll,
1044                                            &editor_focus_handle,
1045                                            cx,
1046                                        )
1047                                        .map(|kb| kb.size(rems_from_px(12.)))
1048                                    })
1049                                    .on_click(cx.listener(|this, _, window, cx| {
1050                                        this.dispatch_action(&RejectAll, window, cx)
1051                                    })),
1052                            )
1053                            .child(
1054                                Button::new("keep-all", "Keep All")
1055                                    .key_binding({
1056                                        KeyBinding::for_action_in(
1057                                            &KeepAll,
1058                                            &editor_focus_handle,
1059                                            cx,
1060                                        )
1061                                        .map(|kb| kb.size(rems_from_px(12.)))
1062                                    })
1063                                    .on_click(cx.listener(|this, _, window, cx| {
1064                                        this.dispatch_action(&KeepAll, window, cx)
1065                                    })),
1066                            )
1067                            .into_any_element(),
1068                    ],
1069                };
1070
1071                h_flex()
1072                    .track_focus(&editor_focus_handle)
1073                    .size_full()
1074                    .px_1()
1075                    .mr_1()
1076                    .gap_1()
1077                    .children(content)
1078                    .child(vertical_divider())
1079                    .when_some(editor.read(cx).workspace(), |this, _workspace| {
1080                        this.child(
1081                            IconButton::new("review", IconName::ListTodo)
1082                                .icon_size(IconSize::Small)
1083                                .tooltip(Tooltip::for_action_title_in(
1084                                    "Review All Files",
1085                                    &OpenAgentDiff,
1086                                    &editor_focus_handle,
1087                                ))
1088                                .on_click({
1089                                    cx.listener(move |this, _, window, cx| {
1090                                        this.dispatch_action(&OpenAgentDiff, window, cx);
1091                                    })
1092                                }),
1093                        )
1094                    })
1095                    .child(vertical_divider())
1096                    .on_action({
1097                        let editor = editor.clone();
1098                        move |_action: &OpenAgentDiff, window, cx| {
1099                            AgentDiff::global(cx).update(cx, |agent_diff, cx| {
1100                                agent_diff.deploy_pane_from_editor(&editor, window, cx);
1101                            });
1102                        }
1103                    })
1104                    .into_any()
1105            }
1106            AgentDiffToolbarItem::Pane(agent_diff) => {
1107                let Some(agent_diff) = agent_diff.upgrade() else {
1108                    return Empty.into_any();
1109                };
1110
1111                let has_pending_edit_tool_use = agent_diff
1112                    .read(cx)
1113                    .thread
1114                    .read(cx)
1115                    .has_pending_edit_tool_calls();
1116
1117                if has_pending_edit_tool_use {
1118                    return div().px_2().child(spinner_icon).into_any();
1119                }
1120
1121                let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
1122                if is_empty {
1123                    return Empty.into_any();
1124                }
1125
1126                let focus_handle = agent_diff.focus_handle(cx);
1127
1128                h_group_xl()
1129                    .my_neg_1()
1130                    .py_1()
1131                    .items_center()
1132                    .flex_wrap()
1133                    .child(
1134                        h_group_sm()
1135                            .child(
1136                                Button::new("reject-all", "Reject All")
1137                                    .key_binding({
1138                                        KeyBinding::for_action_in(&RejectAll, &focus_handle, cx)
1139                                            .map(|kb| kb.size(rems_from_px(12.)))
1140                                    })
1141                                    .on_click(cx.listener(|this, _, window, cx| {
1142                                        this.dispatch_action(&RejectAll, window, cx)
1143                                    })),
1144                            )
1145                            .child(
1146                                Button::new("keep-all", "Keep All")
1147                                    .key_binding({
1148                                        KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
1149                                            .map(|kb| kb.size(rems_from_px(12.)))
1150                                    })
1151                                    .on_click(cx.listener(|this, _, window, cx| {
1152                                        this.dispatch_action(&KeepAll, window, cx)
1153                                    })),
1154                            ),
1155                    )
1156                    .into_any()
1157            }
1158        }
1159    }
1160}
1161
1162#[derive(Default)]
1163pub struct AgentDiff {
1164    reviewing_editors: HashMap<WeakEntity<Editor>, EditorState>,
1165    workspace_threads: HashMap<WeakEntity<Workspace>, WorkspaceThread>,
1166}
1167
1168#[derive(Clone, Debug, PartialEq, Eq)]
1169pub enum EditorState {
1170    Idle,
1171    Reviewing,
1172}
1173
1174struct WorkspaceThread {
1175    thread: WeakEntity<AcpThread>,
1176    _thread_subscriptions: (Subscription, Subscription),
1177    singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
1178    _settings_subscription: Subscription,
1179    _workspace_subscription: Option<Subscription>,
1180}
1181
1182struct AgentDiffGlobal(Entity<AgentDiff>);
1183
1184impl Global for AgentDiffGlobal {}
1185
1186impl AgentDiff {
1187    fn global(cx: &mut App) -> Entity<Self> {
1188        cx.try_global::<AgentDiffGlobal>()
1189            .map(|global| global.0.clone())
1190            .unwrap_or_else(|| {
1191                let entity = cx.new(|_cx| Self::default());
1192                let global = AgentDiffGlobal(entity.clone());
1193                cx.set_global(global);
1194                entity
1195            })
1196    }
1197
1198    pub fn set_active_thread(
1199        workspace: &WeakEntity<Workspace>,
1200        thread: Entity<AcpThread>,
1201        window: &mut Window,
1202        cx: &mut App,
1203    ) {
1204        Self::global(cx).update(cx, |this, cx| {
1205            this.register_active_thread_impl(workspace, thread, window, cx);
1206        });
1207    }
1208
1209    fn register_active_thread_impl(
1210        &mut self,
1211        workspace: &WeakEntity<Workspace>,
1212        thread: Entity<AcpThread>,
1213        window: &mut Window,
1214        cx: &mut Context<Self>,
1215    ) {
1216        let action_log = thread.read(cx).action_log().clone();
1217
1218        let action_log_subscription = cx.observe_in(&action_log, window, {
1219            let workspace = workspace.clone();
1220            move |this, _action_log, window, cx| {
1221                this.update_reviewing_editors(&workspace, window, cx);
1222            }
1223        });
1224
1225        let thread_subscription = cx.subscribe_in(&thread, window, {
1226            let workspace = workspace.clone();
1227            move |this, thread, event, window, cx| {
1228                this.handle_acp_thread_event(&workspace, thread, event, window, cx)
1229            }
1230        });
1231
1232        if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) {
1233            // replace thread and action log subscription, but keep editors
1234            workspace_thread.thread = thread.downgrade();
1235            workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
1236            self.update_reviewing_editors(workspace, window, cx);
1237            return;
1238        }
1239
1240        let settings_subscription = cx.observe_global_in::<SettingsStore>(window, {
1241            let workspace = workspace.clone();
1242            let mut was_active = AgentSettings::get_global(cx).single_file_review;
1243            move |this, window, cx| {
1244                let is_active = AgentSettings::get_global(cx).single_file_review;
1245                if was_active != is_active {
1246                    was_active = is_active;
1247                    this.update_reviewing_editors(&workspace, window, cx);
1248                }
1249            }
1250        });
1251
1252        let workspace_subscription = workspace
1253            .upgrade()
1254            .map(|workspace| cx.subscribe_in(&workspace, window, Self::handle_workspace_event));
1255
1256        self.workspace_threads.insert(
1257            workspace.clone(),
1258            WorkspaceThread {
1259                thread: thread.downgrade(),
1260                _thread_subscriptions: (action_log_subscription, thread_subscription),
1261                singleton_editors: HashMap::default(),
1262                _settings_subscription: settings_subscription,
1263                _workspace_subscription: workspace_subscription,
1264            },
1265        );
1266
1267        let workspace = workspace.clone();
1268        cx.defer_in(window, move |this, window, cx| {
1269            if let Some(workspace) = workspace.upgrade() {
1270                this.register_workspace(workspace, window, cx);
1271            }
1272        });
1273    }
1274
1275    fn register_workspace(
1276        &mut self,
1277        workspace: Entity<Workspace>,
1278        window: &mut Window,
1279        cx: &mut Context<Self>,
1280    ) {
1281        let agent_diff = cx.entity();
1282
1283        let editors = workspace.update(cx, |workspace, cx| {
1284            let agent_diff = agent_diff.clone();
1285
1286            Self::register_review_action::<Keep>(workspace, Self::keep, &agent_diff);
1287            Self::register_review_action::<Reject>(workspace, Self::reject, &agent_diff);
1288            Self::register_review_action::<KeepAll>(workspace, Self::keep_all, &agent_diff);
1289            Self::register_review_action::<RejectAll>(workspace, Self::reject_all, &agent_diff);
1290
1291            workspace.items_of_type(cx).collect::<Vec<_>>()
1292        });
1293
1294        let weak_workspace = workspace.downgrade();
1295
1296        for editor in editors {
1297            if let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) {
1298                self.register_editor(weak_workspace.clone(), buffer, editor, window, cx);
1299            };
1300        }
1301
1302        self.update_reviewing_editors(&weak_workspace, window, cx);
1303    }
1304
1305    fn register_review_action<T: Action>(
1306        workspace: &mut Workspace,
1307        review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState
1308        + 'static,
1309        this: &Entity<AgentDiff>,
1310    ) {
1311        let this = this.clone();
1312        workspace.register_action(move |workspace, _: &T, window, cx| {
1313            let review = &review;
1314            let task = this.update(cx, |this, cx| {
1315                this.review_in_active_editor(workspace, review, window, cx)
1316            });
1317
1318            if let Some(task) = task {
1319                task.detach_and_log_err(cx);
1320            } else {
1321                cx.propagate();
1322            }
1323        });
1324    }
1325
1326    fn handle_acp_thread_event(
1327        &mut self,
1328        workspace: &WeakEntity<Workspace>,
1329        thread: &Entity<AcpThread>,
1330        event: &AcpThreadEvent,
1331        window: &mut Window,
1332        cx: &mut Context<Self>,
1333    ) {
1334        match event {
1335            AcpThreadEvent::NewEntry => {
1336                if thread
1337                    .read(cx)
1338                    .entries()
1339                    .last()
1340                    .is_some_and(|entry| entry.diffs().next().is_some())
1341                {
1342                    self.update_reviewing_editors(workspace, window, cx);
1343                }
1344            }
1345            AcpThreadEvent::EntryUpdated(ix) => {
1346                if thread
1347                    .read(cx)
1348                    .entries()
1349                    .get(*ix)
1350                    .is_some_and(|entry| entry.diffs().next().is_some())
1351                {
1352                    self.update_reviewing_editors(workspace, window, cx);
1353                }
1354            }
1355            AcpThreadEvent::Stopped
1356            | AcpThreadEvent::Error
1357            | AcpThreadEvent::LoadError(_)
1358            | AcpThreadEvent::Refusal => {
1359                self.update_reviewing_editors(workspace, window, cx);
1360            }
1361            AcpThreadEvent::TitleUpdated
1362            | AcpThreadEvent::TokenUsageUpdated
1363            | AcpThreadEvent::EntriesRemoved(_)
1364            | AcpThreadEvent::ToolAuthorizationRequired
1365            | AcpThreadEvent::PromptCapabilitiesUpdated
1366            | AcpThreadEvent::AvailableCommandsUpdated(_)
1367            | AcpThreadEvent::Retry(_)
1368            | AcpThreadEvent::ModeUpdated(_)
1369            | AcpThreadEvent::ConfigOptionsUpdated(_) => {}
1370        }
1371    }
1372
1373    fn handle_workspace_event(
1374        &mut self,
1375        workspace: &Entity<Workspace>,
1376        event: &workspace::Event,
1377        window: &mut Window,
1378        cx: &mut Context<Self>,
1379    ) {
1380        if let workspace::Event::ItemAdded { item } = event
1381            && let Some(editor) = item.downcast::<Editor>()
1382            && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx)
1383        {
1384            self.register_editor(workspace.downgrade(), buffer, editor, window, cx);
1385        }
1386    }
1387
1388    fn full_editor_buffer(editor: &Editor, cx: &App) -> Option<WeakEntity<Buffer>> {
1389        if editor.mode().is_full() {
1390            editor
1391                .buffer()
1392                .read(cx)
1393                .as_singleton()
1394                .map(|buffer| buffer.downgrade())
1395        } else {
1396            None
1397        }
1398    }
1399
1400    fn register_editor(
1401        &mut self,
1402        workspace: WeakEntity<Workspace>,
1403        buffer: WeakEntity<Buffer>,
1404        editor: Entity<Editor>,
1405        window: &mut Window,
1406        cx: &mut Context<Self>,
1407    ) {
1408        let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) else {
1409            return;
1410        };
1411
1412        let weak_editor = editor.downgrade();
1413
1414        workspace_thread
1415            .singleton_editors
1416            .entry(buffer.clone())
1417            .or_default()
1418            .entry(weak_editor.clone())
1419            .or_insert_with(|| {
1420                let workspace = workspace.clone();
1421                cx.observe_release(&editor, move |this, _, _cx| {
1422                    let Some(active_thread) = this.workspace_threads.get_mut(&workspace) else {
1423                        return;
1424                    };
1425
1426                    if let Entry::Occupied(mut entry) =
1427                        active_thread.singleton_editors.entry(buffer)
1428                    {
1429                        let set = entry.get_mut();
1430                        set.remove(&weak_editor);
1431
1432                        if set.is_empty() {
1433                            entry.remove();
1434                        }
1435                    }
1436                })
1437            });
1438
1439        self.update_reviewing_editors(&workspace, window, cx);
1440    }
1441
1442    fn update_reviewing_editors(
1443        &mut self,
1444        workspace: &WeakEntity<Workspace>,
1445        window: &mut Window,
1446        cx: &mut Context<Self>,
1447    ) {
1448        if !AgentSettings::get_global(cx).single_file_review {
1449            for (editor, _) in self.reviewing_editors.drain() {
1450                editor
1451                    .update(cx, |editor, cx| {
1452                        editor.end_temporary_diff_override(cx);
1453                        editor.unregister_addon::<EditorAgentDiffAddon>();
1454                    })
1455                    .ok();
1456            }
1457            return;
1458        }
1459
1460        let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) else {
1461            return;
1462        };
1463
1464        let Some(thread) = workspace_thread.thread.upgrade() else {
1465            return;
1466        };
1467
1468        let action_log = thread.read(cx).action_log();
1469        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1470
1471        let mut unaffected = self.reviewing_editors.clone();
1472
1473        for (buffer, diff_handle) in changed_buffers {
1474            if buffer.read(cx).file().is_none() {
1475                continue;
1476            }
1477
1478            let Some(buffer_editors) = workspace_thread.singleton_editors.get(&buffer.downgrade())
1479            else {
1480                continue;
1481            };
1482
1483            for weak_editor in buffer_editors.keys() {
1484                let Some(editor) = weak_editor.upgrade() else {
1485                    continue;
1486                };
1487
1488                let multibuffer = editor.read(cx).buffer().clone();
1489                multibuffer.update(cx, |multibuffer, cx| {
1490                    multibuffer.add_diff(diff_handle.clone(), cx);
1491                });
1492
1493                let reviewing_state = EditorState::Reviewing;
1494
1495                let previous_state = self
1496                    .reviewing_editors
1497                    .insert(weak_editor.clone(), reviewing_state.clone());
1498
1499                if previous_state.is_none() {
1500                    editor.update(cx, |editor, cx| {
1501                        editor.start_temporary_diff_override();
1502                        editor.set_render_diff_hunk_controls(diff_hunk_controls(&thread), cx);
1503                        editor.set_expand_all_diff_hunks(cx);
1504                        editor.register_addon(EditorAgentDiffAddon);
1505                    });
1506                } else {
1507                    unaffected.remove(weak_editor);
1508                }
1509
1510                if reviewing_state == EditorState::Reviewing
1511                    && previous_state != Some(reviewing_state)
1512                {
1513                    // Jump to first hunk when we enter review mode
1514                    editor.update(cx, |editor, cx| {
1515                        let snapshot = multibuffer.read(cx).snapshot(cx);
1516                        if let Some(first_hunk) = snapshot.diff_hunks().next() {
1517                            let first_hunk_start = first_hunk.multi_buffer_range().start;
1518
1519                            editor.change_selections(
1520                                SelectionEffects::scroll(Autoscroll::center()),
1521                                window,
1522                                cx,
1523                                |selections| {
1524                                    selections.select_ranges([first_hunk_start..first_hunk_start])
1525                                },
1526                            );
1527                        }
1528                    });
1529                }
1530            }
1531        }
1532
1533        // Remove editors from this workspace that are no longer under review
1534        for (editor, _) in unaffected {
1535            // Note: We could avoid this check by storing `reviewing_editors` by Workspace,
1536            // but that would add another lookup in `AgentDiff::editor_state`
1537            // which gets called much more frequently.
1538            let in_workspace = editor
1539                .read_with(cx, |editor, _cx| editor.workspace())
1540                .ok()
1541                .flatten()
1542                .is_some_and(|editor_workspace| {
1543                    editor_workspace.entity_id() == workspace.entity_id()
1544                });
1545
1546            if in_workspace {
1547                editor
1548                    .update(cx, |editor, cx| {
1549                        editor.end_temporary_diff_override(cx);
1550                        editor.unregister_addon::<EditorAgentDiffAddon>();
1551                    })
1552                    .ok();
1553                self.reviewing_editors.remove(&editor);
1554            }
1555        }
1556
1557        cx.notify();
1558    }
1559
1560    fn editor_state(&self, editor: &WeakEntity<Editor>) -> EditorState {
1561        self.reviewing_editors
1562            .get(editor)
1563            .cloned()
1564            .unwrap_or(EditorState::Idle)
1565    }
1566
1567    fn deploy_pane_from_editor(&self, editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
1568        let Some(workspace) = editor.read(cx).workspace() else {
1569            return;
1570        };
1571
1572        let Some(WorkspaceThread { thread, .. }) =
1573            self.workspace_threads.get(&workspace.downgrade())
1574        else {
1575            return;
1576        };
1577
1578        let Some(thread) = thread.upgrade() else {
1579            return;
1580        };
1581
1582        AgentDiffPane::deploy(thread, workspace.downgrade(), window, cx).log_err();
1583    }
1584
1585    fn keep_all(
1586        editor: &Entity<Editor>,
1587        thread: &Entity<AcpThread>,
1588        window: &mut Window,
1589        cx: &mut App,
1590    ) -> PostReviewState {
1591        editor.update(cx, |editor, cx| {
1592            let snapshot = editor.buffer().read(cx).snapshot(cx);
1593            keep_edits_in_ranges(
1594                editor,
1595                &snapshot,
1596                thread,
1597                vec![editor::Anchor::min()..editor::Anchor::max()],
1598                window,
1599                cx,
1600            );
1601        });
1602        PostReviewState::AllReviewed
1603    }
1604
1605    fn reject_all(
1606        editor: &Entity<Editor>,
1607        thread: &Entity<AcpThread>,
1608        window: &mut Window,
1609        cx: &mut App,
1610    ) -> PostReviewState {
1611        editor.update(cx, |editor, cx| {
1612            let snapshot = editor.buffer().read(cx).snapshot(cx);
1613            reject_edits_in_ranges(
1614                editor,
1615                &snapshot,
1616                thread,
1617                vec![editor::Anchor::min()..editor::Anchor::max()],
1618                window,
1619                cx,
1620            );
1621        });
1622        PostReviewState::AllReviewed
1623    }
1624
1625    fn keep(
1626        editor: &Entity<Editor>,
1627        thread: &Entity<AcpThread>,
1628        window: &mut Window,
1629        cx: &mut App,
1630    ) -> PostReviewState {
1631        editor.update(cx, |editor, cx| {
1632            let snapshot = editor.buffer().read(cx).snapshot(cx);
1633            keep_edits_in_selection(editor, &snapshot, thread, window, cx);
1634            Self::post_review_state(&snapshot)
1635        })
1636    }
1637
1638    fn reject(
1639        editor: &Entity<Editor>,
1640        thread: &Entity<AcpThread>,
1641        window: &mut Window,
1642        cx: &mut App,
1643    ) -> PostReviewState {
1644        editor.update(cx, |editor, cx| {
1645            let snapshot = editor.buffer().read(cx).snapshot(cx);
1646            reject_edits_in_selection(editor, &snapshot, thread, window, cx);
1647            Self::post_review_state(&snapshot)
1648        })
1649    }
1650
1651    fn post_review_state(snapshot: &MultiBufferSnapshot) -> PostReviewState {
1652        for (i, _) in snapshot.diff_hunks().enumerate() {
1653            if i > 0 {
1654                return PostReviewState::Pending;
1655            }
1656        }
1657        PostReviewState::AllReviewed
1658    }
1659
1660    fn review_in_active_editor(
1661        &mut self,
1662        workspace: &mut Workspace,
1663        review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState,
1664        window: &mut Window,
1665        cx: &mut Context<Self>,
1666    ) -> Option<Task<Result<()>>> {
1667        let active_item = workspace.active_item(cx)?;
1668        let editor = active_item.act_as::<Editor>(cx)?;
1669
1670        if !matches!(
1671            self.editor_state(&editor.downgrade()),
1672            EditorState::Reviewing
1673        ) {
1674            return None;
1675        }
1676
1677        let WorkspaceThread { thread, .. } =
1678            self.workspace_threads.get(&workspace.weak_handle())?;
1679
1680        let thread = thread.upgrade()?;
1681
1682        if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx)
1683            && let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton()
1684        {
1685            let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
1686
1687            let mut keys = changed_buffers.keys();
1688            keys.find(|k| *k == &curr_buffer);
1689            let next_project_path = keys
1690                .next()
1691                .filter(|k| *k != &curr_buffer)
1692                .and_then(|after| after.read(cx).project_path(cx));
1693
1694            if let Some(path) = next_project_path {
1695                let task = workspace.open_path(path, None, true, window, cx);
1696                let task = cx.spawn(async move |_, _cx| task.await.map(|_| ()));
1697                return Some(task);
1698            }
1699        }
1700
1701        Some(Task::ready(Ok(())))
1702    }
1703}
1704
1705enum PostReviewState {
1706    AllReviewed,
1707    Pending,
1708}
1709
1710pub struct EditorAgentDiffAddon;
1711
1712impl editor::Addon for EditorAgentDiffAddon {
1713    fn to_any(&self) -> &dyn std::any::Any {
1714        self
1715    }
1716
1717    fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
1718        key_context.add("agent_diff");
1719        key_context.add("editor_agent_diff");
1720    }
1721}
1722
1723#[cfg(test)]
1724mod tests {
1725    use super::*;
1726    use crate::Keep;
1727    use acp_thread::AgentConnection as _;
1728    use editor::EditorSettings;
1729    use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
1730    use project::{FakeFs, Project};
1731    use serde_json::json;
1732    use settings::SettingsStore;
1733    use std::{path::Path, rc::Rc};
1734    use util::path;
1735
1736    #[gpui::test]
1737    async fn test_multibuffer_agent_diff(cx: &mut TestAppContext) {
1738        cx.update(|cx| {
1739            let settings_store = SettingsStore::test(cx);
1740            cx.set_global(settings_store);
1741            prompt_store::init(cx);
1742            theme::init(theme::LoadThemes::JustBase, cx);
1743            language_model::init_settings(cx);
1744        });
1745
1746        let fs = FakeFs::new(cx.executor());
1747        fs.insert_tree(
1748            path!("/test"),
1749            json!({"file1": "abc\ndef\nghi\njkl\nmno\npqr\nstu\nvwx\nyz"}),
1750        )
1751        .await;
1752        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
1753        let buffer_path = project
1754            .read_with(cx, |project, cx| {
1755                project.find_project_path("test/file1", cx)
1756            })
1757            .unwrap();
1758
1759        let connection = Rc::new(acp_thread::StubAgentConnection::new());
1760        let thread = cx
1761            .update(|cx| {
1762                connection
1763                    .clone()
1764                    .new_thread(project.clone(), Path::new(path!("/test")), cx)
1765            })
1766            .await
1767            .unwrap();
1768
1769        let action_log = cx.read(|cx| thread.read(cx).action_log().clone());
1770
1771        let (workspace, cx) =
1772            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1773        let agent_diff = cx.new_window_entity(|window, cx| {
1774            AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx)
1775        });
1776        let editor = agent_diff.read_with(cx, |diff, _cx| diff.editor.clone());
1777
1778        let buffer = project
1779            .update(cx, |project, cx| project.open_buffer(buffer_path, cx))
1780            .await
1781            .unwrap();
1782        cx.update(|_, cx| {
1783            action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
1784            buffer.update(cx, |buffer, cx| {
1785                buffer
1786                    .edit(
1787                        [
1788                            (Point::new(1, 1)..Point::new(1, 2), "E"),
1789                            (Point::new(3, 2)..Point::new(3, 3), "L"),
1790                            (Point::new(5, 0)..Point::new(5, 1), "P"),
1791                            (Point::new(7, 1)..Point::new(7, 2), "W"),
1792                        ],
1793                        None,
1794                        cx,
1795                    )
1796                    .unwrap()
1797            });
1798            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1799        });
1800        cx.run_until_parked();
1801
1802        // When opening the assistant diff, the cursor is positioned on the first hunk.
1803        assert_eq!(
1804            editor.read_with(cx, |editor, cx| editor.text(cx)),
1805            "abc\ndef\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
1806        );
1807        assert_eq!(
1808            editor
1809                .update(cx, |editor, cx| editor
1810                    .selections
1811                    .newest::<Point>(&editor.display_snapshot(cx)))
1812                .range(),
1813            Point::new(1, 0)..Point::new(1, 0)
1814        );
1815
1816        // After keeping a hunk, the cursor should be positioned on the second hunk.
1817        agent_diff.update_in(cx, |diff, window, cx| diff.keep(&Keep, window, cx));
1818        cx.run_until_parked();
1819        assert_eq!(
1820            editor.read_with(cx, |editor, cx| editor.text(cx)),
1821            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
1822        );
1823        assert_eq!(
1824            editor
1825                .update(cx, |editor, cx| editor
1826                    .selections
1827                    .newest::<Point>(&editor.display_snapshot(cx)))
1828                .range(),
1829            Point::new(3, 0)..Point::new(3, 0)
1830        );
1831
1832        // Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
1833        editor.update_in(cx, |editor, window, cx| {
1834            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1835                selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
1836            });
1837        });
1838        agent_diff.update_in(cx, |diff, window, cx| {
1839            diff.reject(&crate::Reject, window, cx)
1840        });
1841        cx.run_until_parked();
1842        assert_eq!(
1843            editor.read_with(cx, |editor, cx| editor.text(cx)),
1844            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nyz"
1845        );
1846        assert_eq!(
1847            editor
1848                .update(cx, |editor, cx| editor
1849                    .selections
1850                    .newest::<Point>(&editor.display_snapshot(cx)))
1851                .range(),
1852            Point::new(3, 0)..Point::new(3, 0)
1853        );
1854
1855        // Keeping a range that doesn't intersect the current selection doesn't move it.
1856        agent_diff.update_in(cx, |_diff, window, cx| {
1857            let position = editor
1858                .read(cx)
1859                .buffer()
1860                .read(cx)
1861                .read(cx)
1862                .anchor_before(Point::new(7, 0));
1863            editor.update(cx, |editor, cx| {
1864                let snapshot = editor.buffer().read(cx).snapshot(cx);
1865                keep_edits_in_ranges(
1866                    editor,
1867                    &snapshot,
1868                    &thread,
1869                    vec![position..position],
1870                    window,
1871                    cx,
1872                )
1873            });
1874        });
1875        cx.run_until_parked();
1876        assert_eq!(
1877            editor.read_with(cx, |editor, cx| editor.text(cx)),
1878            "abc\ndEf\nghi\njkl\njkL\nmno\nPqr\nstu\nvwx\nyz"
1879        );
1880        assert_eq!(
1881            editor
1882                .update(cx, |editor, cx| editor
1883                    .selections
1884                    .newest::<Point>(&editor.display_snapshot(cx)))
1885                .range(),
1886            Point::new(3, 0)..Point::new(3, 0)
1887        );
1888    }
1889
1890    #[gpui::test]
1891    async fn test_singleton_agent_diff(cx: &mut TestAppContext) {
1892        cx.update(|cx| {
1893            let settings_store = SettingsStore::test(cx);
1894            cx.set_global(settings_store);
1895            prompt_store::init(cx);
1896            theme::init(theme::LoadThemes::JustBase, cx);
1897            language_model::init_settings(cx);
1898            workspace::register_project_item::<Editor>(cx);
1899        });
1900
1901        let fs = FakeFs::new(cx.executor());
1902        fs.insert_tree(
1903            path!("/test"),
1904            json!({"file1": "abc\ndef\nghi\njkl\nmno\npqr\nstu\nvwx\nyz"}),
1905        )
1906        .await;
1907        fs.insert_tree(path!("/test"), json!({"file2": "abc\ndef\nghi"}))
1908            .await;
1909
1910        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
1911        let buffer_path1 = project
1912            .read_with(cx, |project, cx| {
1913                project.find_project_path("test/file1", cx)
1914            })
1915            .unwrap();
1916        let buffer_path2 = project
1917            .read_with(cx, |project, cx| {
1918                project.find_project_path("test/file2", cx)
1919            })
1920            .unwrap();
1921
1922        let (workspace, cx) =
1923            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1924
1925        // Add the diff toolbar to the active pane
1926        let diff_toolbar = cx.new_window_entity(|_, cx| AgentDiffToolbar::new(cx));
1927
1928        workspace.update_in(cx, {
1929            let diff_toolbar = diff_toolbar.clone();
1930
1931            move |workspace, window, cx| {
1932                workspace.active_pane().update(cx, |pane, cx| {
1933                    pane.toolbar().update(cx, |toolbar, cx| {
1934                        toolbar.add_item(diff_toolbar, window, cx);
1935                    });
1936                })
1937            }
1938        });
1939
1940        let connection = Rc::new(acp_thread::StubAgentConnection::new());
1941        let thread = cx
1942            .update(|_, cx| {
1943                connection
1944                    .clone()
1945                    .new_thread(project.clone(), Path::new(path!("/test")), cx)
1946            })
1947            .await
1948            .unwrap();
1949        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
1950
1951        // Set the active thread
1952        cx.update(|window, cx| {
1953            AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
1954        });
1955
1956        let buffer1 = project
1957            .update(cx, |project, cx| {
1958                project.open_buffer(buffer_path1.clone(), cx)
1959            })
1960            .await
1961            .unwrap();
1962        let buffer2 = project
1963            .update(cx, |project, cx| {
1964                project.open_buffer(buffer_path2.clone(), cx)
1965            })
1966            .await
1967            .unwrap();
1968
1969        // Open an editor for buffer1
1970        let editor1 = cx.new_window_entity(|window, cx| {
1971            Editor::for_buffer(buffer1.clone(), Some(project.clone()), window, cx)
1972        });
1973
1974        workspace.update_in(cx, |workspace, window, cx| {
1975            workspace.add_item_to_active_pane(Box::new(editor1.clone()), None, true, window, cx);
1976        });
1977        cx.run_until_parked();
1978
1979        // Toolbar knows about the current editor, but it's hidden since there are no changes yet
1980        assert!(diff_toolbar.read_with(cx, |toolbar, _cx| matches!(
1981            toolbar.active_item,
1982            Some(AgentDiffToolbarItem::Editor {
1983                state: EditorState::Idle,
1984                ..
1985            })
1986        )));
1987        assert_eq!(
1988            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
1989            ToolbarItemLocation::Hidden
1990        );
1991
1992        // Make changes
1993        cx.update(|_, cx| {
1994            action_log.update(cx, |log, cx| log.buffer_read(buffer1.clone(), cx));
1995            buffer1.update(cx, |buffer, cx| {
1996                buffer
1997                    .edit(
1998                        [
1999                            (Point::new(1, 1)..Point::new(1, 2), "E"),
2000                            (Point::new(3, 2)..Point::new(3, 3), "L"),
2001                            (Point::new(5, 0)..Point::new(5, 1), "P"),
2002                            (Point::new(7, 1)..Point::new(7, 2), "W"),
2003                        ],
2004                        None,
2005                        cx,
2006                    )
2007                    .unwrap()
2008            });
2009            action_log.update(cx, |log, cx| log.buffer_edited(buffer1.clone(), cx));
2010
2011            action_log.update(cx, |log, cx| log.buffer_read(buffer2.clone(), cx));
2012            buffer2.update(cx, |buffer, cx| {
2013                buffer
2014                    .edit(
2015                        [
2016                            (Point::new(0, 0)..Point::new(0, 1), "A"),
2017                            (Point::new(2, 1)..Point::new(2, 2), "H"),
2018                        ],
2019                        None,
2020                        cx,
2021                    )
2022                    .unwrap();
2023            });
2024            action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
2025        });
2026        cx.run_until_parked();
2027
2028        // The already opened editor displays the diff and the cursor is at the first hunk
2029        assert_eq!(
2030            editor1.read_with(cx, |editor, cx| editor.text(cx)),
2031            "abc\ndef\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
2032        );
2033        assert_eq!(
2034            editor1
2035                .update(cx, |editor, cx| editor
2036                    .selections
2037                    .newest::<Point>(&editor.display_snapshot(cx)))
2038                .range(),
2039            Point::new(1, 0)..Point::new(1, 0)
2040        );
2041
2042        // The toolbar is displayed in the right state
2043        assert_eq!(
2044            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
2045            ToolbarItemLocation::PrimaryRight
2046        );
2047        assert!(diff_toolbar.read_with(cx, |toolbar, _cx| matches!(
2048            toolbar.active_item,
2049            Some(AgentDiffToolbarItem::Editor {
2050                state: EditorState::Reviewing,
2051                ..
2052            })
2053        )));
2054
2055        // The toolbar respects its setting
2056        override_toolbar_agent_review_setting(false, cx);
2057        assert_eq!(
2058            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
2059            ToolbarItemLocation::Hidden
2060        );
2061        override_toolbar_agent_review_setting(true, cx);
2062        assert_eq!(
2063            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
2064            ToolbarItemLocation::PrimaryRight
2065        );
2066
2067        // After keeping a hunk, the cursor should be positioned on the second hunk.
2068        workspace.update(cx, |_, cx| {
2069            cx.dispatch_action(&Keep);
2070        });
2071        cx.run_until_parked();
2072        assert_eq!(
2073            editor1.read_with(cx, |editor, cx| editor.text(cx)),
2074            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nvWx\nyz"
2075        );
2076        assert_eq!(
2077            editor1
2078                .update(cx, |editor, cx| editor
2079                    .selections
2080                    .newest::<Point>(&editor.display_snapshot(cx)))
2081                .range(),
2082            Point::new(3, 0)..Point::new(3, 0)
2083        );
2084
2085        // Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
2086        editor1.update_in(cx, |editor, window, cx| {
2087            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
2088                selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
2089            });
2090        });
2091        workspace.update(cx, |_, cx| {
2092            cx.dispatch_action(&Reject);
2093        });
2094        cx.run_until_parked();
2095        assert_eq!(
2096            editor1.read_with(cx, |editor, cx| editor.text(cx)),
2097            "abc\ndEf\nghi\njkl\njkL\nmno\npqr\nPqr\nstu\nvwx\nyz"
2098        );
2099        assert_eq!(
2100            editor1
2101                .update(cx, |editor, cx| editor
2102                    .selections
2103                    .newest::<Point>(&editor.display_snapshot(cx)))
2104                .range(),
2105            Point::new(3, 0)..Point::new(3, 0)
2106        );
2107
2108        // Keeping a range that doesn't intersect the current selection doesn't move it.
2109        editor1.update_in(cx, |editor, window, cx| {
2110            let buffer = editor.buffer().read(cx);
2111            let position = buffer.read(cx).anchor_before(Point::new(7, 0));
2112            let snapshot = buffer.snapshot(cx);
2113            keep_edits_in_ranges(
2114                editor,
2115                &snapshot,
2116                &thread,
2117                vec![position..position],
2118                window,
2119                cx,
2120            )
2121        });
2122        cx.run_until_parked();
2123        assert_eq!(
2124            editor1.read_with(cx, |editor, cx| editor.text(cx)),
2125            "abc\ndEf\nghi\njkl\njkL\nmno\nPqr\nstu\nvwx\nyz"
2126        );
2127        assert_eq!(
2128            editor1
2129                .update(cx, |editor, cx| editor
2130                    .selections
2131                    .newest::<Point>(&editor.display_snapshot(cx)))
2132                .range(),
2133            Point::new(3, 0)..Point::new(3, 0)
2134        );
2135
2136        // Reviewing the last change opens the next changed buffer
2137        workspace
2138            .update_in(cx, |workspace, window, cx| {
2139                AgentDiff::global(cx).update(cx, |agent_diff, cx| {
2140                    agent_diff.review_in_active_editor(workspace, AgentDiff::keep, window, cx)
2141                })
2142            })
2143            .unwrap()
2144            .await
2145            .unwrap();
2146
2147        cx.run_until_parked();
2148
2149        let editor2 = workspace.update(cx, |workspace, cx| {
2150            workspace.active_item_as::<Editor>(cx).unwrap()
2151        });
2152
2153        let editor2_path = editor2
2154            .read_with(cx, |editor, cx| editor.project_path(cx))
2155            .unwrap();
2156        assert_eq!(editor2_path, buffer_path2);
2157
2158        assert_eq!(
2159            editor2.read_with(cx, |editor, cx| editor.text(cx)),
2160            "abc\nAbc\ndef\nghi\ngHi"
2161        );
2162        assert_eq!(
2163            editor2
2164                .update(cx, |editor, cx| editor
2165                    .selections
2166                    .newest::<Point>(&editor.display_snapshot(cx)))
2167                .range(),
2168            Point::new(0, 0)..Point::new(0, 0)
2169        );
2170
2171        // Editor 1 toolbar is hidden since all changes have been reviewed
2172        workspace.update_in(cx, |workspace, window, cx| {
2173            workspace.activate_item(&editor1, true, true, window, cx)
2174        });
2175
2176        assert!(diff_toolbar.read_with(cx, |toolbar, _cx| matches!(
2177            toolbar.active_item,
2178            Some(AgentDiffToolbarItem::Editor {
2179                state: EditorState::Idle,
2180                ..
2181            })
2182        )));
2183        assert_eq!(
2184            diff_toolbar.read_with(cx, |toolbar, cx| toolbar.location(cx)),
2185            ToolbarItemLocation::Hidden
2186        );
2187    }
2188
2189    fn override_toolbar_agent_review_setting(active: bool, cx: &mut VisualTestContext) {
2190        cx.update(|_window, cx| {
2191            SettingsStore::update_global(cx, |store, _cx| {
2192                let mut editor_settings = store.get::<EditorSettings>(None).clone();
2193                editor_settings.toolbar.agent_review = active;
2194                store.override_global(editor_settings);
2195            })
2196        });
2197        cx.run_until_parked();
2198    }
2199}