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