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