mention_crease.rs

  1use std::{ops::RangeInclusive, path::PathBuf, time::Duration};
  2
  3use acp_thread::MentionUri;
  4use agent_client_protocol as acp;
  5use editor::{Editor, SelectionEffects, scroll::Autoscroll};
  6use gpui::{
  7    Animation, AnimationExt, AnyView, Context, IntoElement, TaskExt, WeakEntity, Window,
  8    pulsating_between,
  9};
 10use prompt_store::PromptId;
 11use rope::Point;
 12use settings::Settings;
 13use theme_settings::ThemeSettings;
 14use ui::{ButtonLike, TintColor, Tooltip, prelude::*};
 15use workspace::{OpenOptions, Workspace};
 16
 17use crate::Agent;
 18
 19#[derive(IntoElement)]
 20pub struct MentionCrease {
 21    id: ElementId,
 22    icon: SharedString,
 23    label: SharedString,
 24    mention_uri: Option<MentionUri>,
 25    workspace: Option<WeakEntity<Workspace>>,
 26    is_toggled: bool,
 27    is_loading: bool,
 28    tooltip: Option<SharedString>,
 29    image_preview: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
 30}
 31
 32impl MentionCrease {
 33    pub fn new(
 34        id: impl Into<ElementId>,
 35        icon: impl Into<SharedString>,
 36        label: impl Into<SharedString>,
 37    ) -> Self {
 38        Self {
 39            id: id.into(),
 40            icon: icon.into(),
 41            label: label.into(),
 42            mention_uri: None,
 43            workspace: None,
 44            is_toggled: false,
 45            is_loading: false,
 46            tooltip: None,
 47            image_preview: None,
 48        }
 49    }
 50
 51    pub fn mention_uri(mut self, mention_uri: Option<MentionUri>) -> Self {
 52        self.mention_uri = mention_uri;
 53        self
 54    }
 55
 56    pub fn workspace(mut self, workspace: Option<WeakEntity<Workspace>>) -> Self {
 57        self.workspace = workspace;
 58        self
 59    }
 60
 61    pub fn is_toggled(mut self, is_toggled: bool) -> Self {
 62        self.is_toggled = is_toggled;
 63        self
 64    }
 65
 66    pub fn is_loading(mut self, is_loading: bool) -> Self {
 67        self.is_loading = is_loading;
 68        self
 69    }
 70
 71    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
 72        self.tooltip = Some(tooltip.into());
 73        self
 74    }
 75
 76    pub fn image_preview(
 77        mut self,
 78        builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
 79    ) -> Self {
 80        self.image_preview = Some(Box::new(builder));
 81        self
 82    }
 83}
 84
 85impl RenderOnce for MentionCrease {
 86    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 87        let settings = ThemeSettings::get_global(cx);
 88        let font_size = settings.agent_buffer_font_size(cx);
 89        let buffer_font = settings.buffer_font.clone();
 90        let is_loading = self.is_loading;
 91        let tooltip = self.tooltip;
 92        let image_preview = self.image_preview;
 93
 94        let button_height = DefiniteLength::Absolute(AbsoluteLength::Pixels(
 95            px(window.line_height().into()) - px(1.),
 96        ));
 97
 98        ButtonLike::new(self.id)
 99            .style(ButtonStyle::Outlined)
100            .size(ButtonSize::Compact)
101            .height(button_height)
102            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
103            .toggle_state(self.is_toggled)
104            .when_some(
105                self.mention_uri.clone().zip(self.workspace.clone()),
106                |this, (mention_uri, workspace)| {
107                    this.on_click(move |_event, window, cx| {
108                        open_mention_uri(mention_uri.clone(), &workspace, window, cx);
109                    })
110                },
111            )
112            .child(
113                h_flex()
114                    .pb_px()
115                    .gap_1()
116                    .font(buffer_font)
117                    .text_size(font_size)
118                    .child(
119                        Icon::from_path(self.icon.clone())
120                            .size(IconSize::XSmall)
121                            .color(Color::Muted),
122                    )
123                    .child(self.label.clone())
124                    .map(|this| {
125                        if is_loading {
126                            this.with_animation(
127                                "loading-context-crease",
128                                Animation::new(Duration::from_secs(2))
129                                    .repeat()
130                                    .with_easing(pulsating_between(0.4, 0.8)),
131                                |label, delta| label.opacity(delta),
132                            )
133                            .into_any()
134                        } else {
135                            this.into_any()
136                        }
137                    }),
138            )
139            .map(|button| {
140                if let Some(image_preview) = image_preview {
141                    button.hoverable_tooltip(image_preview)
142                } else {
143                    button.when_some(tooltip, |this, tooltip_text| {
144                        this.tooltip(Tooltip::text(tooltip_text))
145                    })
146                }
147            })
148    }
149}
150
151fn open_mention_uri(
152    mention_uri: MentionUri,
153    workspace: &WeakEntity<Workspace>,
154    window: &mut Window,
155    cx: &mut App,
156) {
157    let Some(workspace) = workspace.upgrade() else {
158        return;
159    };
160
161    workspace.update(cx, |workspace, cx| match mention_uri {
162        MentionUri::File { abs_path } => {
163            open_file(workspace, abs_path, None, window, cx);
164        }
165        MentionUri::Symbol {
166            abs_path,
167            line_range,
168            ..
169        }
170        | MentionUri::Selection {
171            abs_path: Some(abs_path),
172            line_range,
173        } => {
174            open_file(workspace, abs_path, Some(line_range), window, cx);
175        }
176        MentionUri::Directory { abs_path } => {
177            reveal_in_project_panel(workspace, abs_path, cx);
178        }
179        MentionUri::Thread { id, name } => {
180            open_thread(workspace, id, name, window, cx);
181        }
182        MentionUri::Rule { id, .. } => {
183            open_rule(workspace, id, window, cx);
184        }
185        MentionUri::Fetch { url } => {
186            cx.open_url(url.as_str());
187        }
188        MentionUri::PastedImage { .. }
189        | MentionUri::Selection { abs_path: None, .. }
190        | MentionUri::Diagnostics { .. }
191        | MentionUri::TerminalSelection { .. }
192        | MentionUri::GitDiff { .. }
193        | MentionUri::MergeConflict { .. } => {}
194    });
195}
196
197fn open_file(
198    workspace: &mut Workspace,
199    abs_path: PathBuf,
200    line_range: Option<RangeInclusive<u32>>,
201    window: &mut Window,
202    cx: &mut Context<Workspace>,
203) {
204    let project = workspace.project();
205
206    if let Some(project_path) =
207        project.update(cx, |project, cx| project.find_project_path(&abs_path, cx))
208    {
209        let item = workspace.open_path(project_path, None, true, window, cx);
210        if let Some(line_range) = line_range {
211            window
212                .spawn(cx, async move |cx| {
213                    let Some(editor) = item.await?.downcast::<Editor>() else {
214                        return Ok(());
215                    };
216                    editor
217                        .update_in(cx, |editor, window, cx| {
218                            let range = Point::new(*line_range.start(), 0)
219                                ..Point::new(*line_range.start(), 0);
220                            editor.change_selections(
221                                SelectionEffects::scroll(Autoscroll::center()),
222                                window,
223                                cx,
224                                |selections| selections.select_ranges(vec![range]),
225                            );
226                        })
227                        .ok();
228                    anyhow::Ok(())
229                })
230                .detach_and_log_err(cx);
231        } else {
232            item.detach_and_log_err(cx);
233        }
234    } else if abs_path.exists() {
235        workspace
236            .open_abs_path(
237                abs_path,
238                OpenOptions {
239                    focus: Some(true),
240                    ..Default::default()
241                },
242                window,
243                cx,
244            )
245            .detach_and_log_err(cx);
246    }
247}
248
249fn reveal_in_project_panel(
250    workspace: &mut Workspace,
251    abs_path: PathBuf,
252    cx: &mut Context<Workspace>,
253) {
254    let project = workspace.project();
255    let Some(entry_id) = project.update(cx, |project, cx| {
256        let path = project.find_project_path(&abs_path, cx)?;
257        project.entry_for_path(&path, cx).map(|entry| entry.id)
258    }) else {
259        return;
260    };
261
262    project.update(cx, |_, cx| {
263        cx.emit(project::Event::RevealInProjectPanel(entry_id));
264    });
265}
266
267fn open_thread(
268    workspace: &mut Workspace,
269    id: acp::SessionId,
270    name: String,
271    window: &mut Window,
272    cx: &mut Context<Workspace>,
273) {
274    use crate::AgentPanel;
275
276    let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
277        return;
278    };
279
280    // Right now we only support loading threads in the native agent
281    panel.update(cx, |panel, cx| {
282        panel.load_agent_thread(
283            Agent::NativeAgent,
284            id,
285            None,
286            Some(name.into()),
287            true,
288            window,
289            cx,
290        )
291    });
292}
293
294fn open_rule(
295    _workspace: &mut Workspace,
296    id: PromptId,
297    window: &mut Window,
298    cx: &mut Context<Workspace>,
299) {
300    use zed_actions::assistant::OpenRulesLibrary;
301
302    let PromptId::User { uuid } = id else {
303        return;
304    };
305
306    window.dispatch_action(
307        Box::new(OpenRulesLibrary {
308            prompt_to_select: Some(uuid.0),
309        }),
310        cx,
311    );
312}