diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index d592231726..441ca9cf18 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -178,6 +178,56 @@ impl MessageEditor { return; }; + if let MentionUri::File { abs_path, .. } = &mention_uri { + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(abs_path, cx) + else { + return; + }; + let image = cx + .spawn(async move |_, cx| { + let image = project + .update(cx, |project, cx| project.open_image(project_path, cx)) + .map_err(|e| e.to_string())? + .await + .map_err(|e| e.to_string())?; + image + .read_with(cx, |image, _cx| image.image.clone()) + .map_err(|e| e.to_string()) + }) + .shared(); + let Some(crease_id) = insert_crease_for_image( + *excerpt_id, + start, + content_len, + Some(abs_path.as_path().into()), + image.clone(), + self.editor.clone(), + window, + cx, + ) else { + return; + }; + self.confirm_mention_for_image( + crease_id, + anchor, + Some(abs_path.clone()), + image, + window, + cx, + ); + return; + } + } + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( *excerpt_id, start, @@ -195,71 +245,21 @@ impl MessageEditor { MentionUri::Fetch { url } => { self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); } - MentionUri::File { - abs_path, - is_directory, - } => { - self.confirm_mention_for_file( - crease_id, - anchor, - abs_path, - is_directory, - window, - cx, - ); - } MentionUri::Thread { id, name } => { self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); } MentionUri::TextThread { path, name } => { self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx); } - MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => { + MentionUri::File { .. } + | MentionUri::Symbol { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => { self.mention_set.insert_uri(crease_id, mention_uri.clone()); } } } - fn confirm_mention_for_file( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - abs_path: PathBuf, - is_directory: bool, - window: &mut Window, - cx: &mut Context, - ) { - let extension = abs_path - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - if Img::extensions().contains(&extension) && !extension.contains("svg") { - let project = self.project.clone(); - let Some(project_path) = project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) - else { - return; - }; - let image = cx.spawn(async move |_, cx| { - let image = project - .update(cx, |project, cx| project.open_image(project_path, cx))? - .await?; - image.read_with(cx, |image, _cx| image.image.clone()) - }); - self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx); - } else { - self.mention_set.insert_uri( - crease_id, - MentionUri::File { - abs_path, - is_directory, - }, - ); - } - } - fn confirm_mention_for_fetch( &mut self, crease_id: CreaseId, @@ -498,25 +498,20 @@ impl MessageEditor { let Some(anchor) = multibuffer_anchor else { return; }; + let task = Task::ready(Ok(Arc::new(image))).shared(); let Some(crease_id) = insert_crease_for_image( excerpt_id, text_anchor, content_len, None.clone(), + task.clone(), self.editor.clone(), window, cx, ) else { return; }; - self.confirm_mention_for_image( - crease_id, - anchor, - None, - Task::ready(Ok(Arc::new(image))), - window, - cx, - ); + self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx); } } @@ -584,7 +579,7 @@ impl MessageEditor { crease_id: CreaseId, anchor: Anchor, abs_path: Option, - image: Task>>, + image: Shared, String>>>, window: &mut Window, cx: &mut Context, ) { @@ -937,6 +932,7 @@ pub(crate) fn insert_crease_for_image( anchor: text::Anchor, content_len: usize, abs_path: Option>, + image: Shared, String>>>, editor: Entity, window: &mut Window, cx: &mut App, @@ -956,7 +952,7 @@ pub(crate) fn insert_crease_for_image( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_image_fold_icon_button(crease_label, cx.weak_entity()), + render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), merge_adjacent: false, ..Default::default() }; @@ -978,9 +974,11 @@ pub(crate) fn insert_crease_for_image( fn render_image_fold_icon_button( label: SharedString, + image_task: Shared, String>>>, editor: WeakEntity, ) -> Arc, &mut App) -> AnyElement> { Arc::new({ + let image_task = image_task.clone(); move |fold_id, fold_range, cx| { let is_in_text_selection = editor .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) @@ -1005,11 +1003,47 @@ fn render_image_fold_icon_button( .single_line(), ), ) + .hoverable_tooltip({ + let image_task = image_task.clone(); + move |_, cx| { + let image = image_task.peek().cloned().transpose().ok().flatten(); + let image_task = image_task.clone(); + cx.new::(|cx| ImageHover { + image, + _task: cx.spawn(async move |this, cx| { + if let Ok(image) = image_task.clone().await { + this.update(cx, |this, cx| { + if this.image.replace(image).is_none() { + cx.notify(); + } + }) + .ok(); + } + }), + }) + .into() + } + }) .into_any_element() } }) } +struct ImageHover { + image: Option>, + _task: Task<()>, +} + +impl Render for ImageHover { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + if let Some(image) = self.image.clone() { + gpui::img(image).max_w_96().max_h_96().into_any_element() + } else { + gpui::Empty.into_any_element() + } + } +} + #[derive(Debug, Eq, PartialEq)] pub enum Mention { Text { uri: MentionUri, content: String }, diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 0b30007e44..31bf76e843 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -400,6 +400,7 @@ pub struct ButtonLike { size: ButtonSize, rounding: Option, tooltip: Option AnyView>>, + hoverable_tooltip: Option AnyView>>, cursor_style: CursorStyle, on_click: Option>, on_right_click: Option>, @@ -420,6 +421,7 @@ impl ButtonLike { size: ButtonSize::Default, rounding: Some(ButtonLikeRounding::All), tooltip: None, + hoverable_tooltip: None, children: SmallVec::new(), cursor_style: CursorStyle::PointingHand, on_click: None, @@ -463,6 +465,14 @@ impl ButtonLike { self.on_right_click = Some(Box::new(handler)); self } + + pub fn hoverable_tooltip( + mut self, + tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> Self { + self.hoverable_tooltip = Some(Box::new(tooltip)); + self + } } impl Disableable for ButtonLike { @@ -654,6 +664,9 @@ impl RenderOnce for ButtonLike { .when_some(self.tooltip, |this, tooltip| { this.tooltip(move |window, cx| tooltip(window, cx)) }) + .when_some(self.hoverable_tooltip, |this, tooltip| { + this.hoverable_tooltip(move |window, cx| tooltip(window, cx)) + }) .children(self.children) } }