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