Scroll to first hunk when clicking on a file to review in Agent Panel (#28075)

Antonio Scandurra created

Release Notes:

- Added the ability to scroll to a file when clicking on it in the Agent
Panel review section.

Change summary

crates/agent/src/agent_diff.rs     | 36 +++++++++++++++++++++++++------
crates/agent/src/message_editor.rs | 32 +++++++++++++++++++++++++++
2 files changed, 60 insertions(+), 8 deletions(-)

Detailed changes

crates/agent/src/agent_diff.rs 🔗

@@ -44,7 +44,7 @@ impl AgentDiff {
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Result<()> {
+    ) -> Result<Entity<Self>> {
         let existing_diff = workspace.update(cx, |workspace, cx| {
             workspace
                 .items_of_type::<AgentDiff>(cx)
@@ -53,13 +53,15 @@ impl AgentDiff {
         if let Some(existing_diff) = existing_diff {
             workspace.update(cx, |workspace, cx| {
                 workspace.activate_item(&existing_diff, true, true, window, cx);
-            })
+            })?;
+            Ok(existing_diff)
         } else {
             let agent_diff =
                 cx.new(|cx| AgentDiff::new(thread.clone(), workspace.clone(), window, cx));
             workspace.update(cx, |workspace, cx| {
-                workspace.add_item_to_center(Box::new(agent_diff), window, cx);
-            })
+                workspace.add_item_to_center(Box::new(agent_diff.clone()), window, cx);
+            })?;
+            Ok(agent_diff)
         }
     }
 
@@ -134,11 +136,11 @@ impl AgentDiff {
         let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
 
         for (buffer, diff_handle) in changed_buffers {
-            let Some(file) = buffer.read(cx).file().cloned() else {
+            if buffer.read(cx).file().is_none() {
                 continue;
-            };
+            }
 
-            let path_key = PathKey::namespaced(0, file.full_path(cx).into());
+            let path_key = PathKey::for_buffer(&buffer, cx);
             paths_to_delete.remove(&path_key);
 
             let snapshot = buffer.read(cx).snapshot();
@@ -241,6 +243,26 @@ impl AgentDiff {
         }
     }
 
+    pub fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
+        if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
+            self.editor.update(cx, |editor, cx| {
+                let first_hunk = editor
+                    .diff_hunks_in_ranges(
+                        &[position..editor::Anchor::max()],
+                        &self.multibuffer.read(cx).read(cx),
+                    )
+                    .next();
+
+                if let Some(first_hunk) = first_hunk {
+                    let first_hunk_start = first_hunk.multi_buffer_range().start;
+                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+                        selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
+                    })
+                }
+            });
+        }
+    }
+
     fn keep(&mut self, _: &crate::Keep, window: &mut Window, cx: &mut Context<Self>) {
         let ranges = self
             .editor

crates/agent/src/message_editor.rs 🔗

@@ -9,8 +9,10 @@ use gpui::{
     Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
     WeakEntity, linear_color_stop, linear_gradient, point,
 };
+use language::Buffer;
 use language_model::LanguageModelRegistry;
 use language_model_selector::ToggleModelSelector;
+use multi_buffer;
 use project::Project;
 use settings::Settings;
 use std::time::Duration;
@@ -320,6 +322,19 @@ impl MessageEditor {
     fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
         AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
     }
+
+    fn handle_file_click(
+        &self,
+        buffer: Entity<Buffer>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Ok(diff) = AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
+        {
+            let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
+            diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
+        }
+    }
 }
 
 impl Focusable for MessageEditor {
@@ -487,11 +502,16 @@ impl Render for MessageEditor {
                         }])
                         .child(
                             h_flex()
+                                .id("edits-container")
                                 .p_1p5()
                                 .justify_between()
                                 .when(self.edits_expanded, |this| {
                                     this.border_b_1().border_color(border_color)
                                 })
+                                .cursor_pointer()
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.handle_review_click(window, cx)
+                                }))
                                 .child(
                                     h_flex()
                                         .gap_1()
@@ -605,11 +625,21 @@ impl Render for MessageEditor {
                                                         .justify_between()
                                                         .child(
                                                             h_flex()
-                                                                .id("file-container")
+                                                                .id(("file-container", index))
                                                                 .pr_8()
                                                                 .gap_1p5()
                                                                 .max_w_full()
                                                                 .overflow_x_scroll()
+                                                                .cursor_pointer()
+                                                                .on_click({
+                                                                    let buffer = buffer.clone();
+                                                                    cx.listener(move |this, _, window, cx| {
+                                                                        this.handle_file_click(buffer.clone(), window, cx);
+                                                                    })
+                                                                })
+                                                                .tooltip(
+                                                                    Tooltip::text(format!("Review {}", path.display()))
+                                                                )
                                                                 .child(file_icon)
                                                                 .child(
                                                                     h_flex()