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}