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 });
192}
193
194fn open_file(
195 workspace: &mut Workspace,
196 abs_path: PathBuf,
197 line_range: Option<RangeInclusive<u32>>,
198 window: &mut Window,
199 cx: &mut Context<Workspace>,
200) {
201 let project = workspace.project();
202
203 if let Some(project_path) =
204 project.update(cx, |project, cx| project.find_project_path(&abs_path, cx))
205 {
206 let item = workspace.open_path(project_path, None, true, window, cx);
207 if let Some(line_range) = line_range {
208 window
209 .spawn(cx, async move |cx| {
210 let Some(editor) = item.await?.downcast::<Editor>() else {
211 return Ok(());
212 };
213 editor
214 .update_in(cx, |editor, window, cx| {
215 let range = Point::new(*line_range.start(), 0)
216 ..Point::new(*line_range.start(), 0);
217 editor.change_selections(
218 SelectionEffects::scroll(Autoscroll::center()),
219 window,
220 cx,
221 |selections| selections.select_ranges(vec![range]),
222 );
223 })
224 .ok();
225 anyhow::Ok(())
226 })
227 .detach_and_log_err(cx);
228 } else {
229 item.detach_and_log_err(cx);
230 }
231 } else if abs_path.exists() {
232 workspace
233 .open_abs_path(
234 abs_path,
235 OpenOptions {
236 focus: Some(true),
237 ..Default::default()
238 },
239 window,
240 cx,
241 )
242 .detach_and_log_err(cx);
243 }
244}
245
246fn reveal_in_project_panel(
247 workspace: &mut Workspace,
248 abs_path: PathBuf,
249 cx: &mut Context<Workspace>,
250) {
251 let project = workspace.project();
252 let Some(entry_id) = project.update(cx, |project, cx| {
253 let path = project.find_project_path(&abs_path, cx)?;
254 project.entry_for_path(&path, cx).map(|entry| entry.id)
255 }) else {
256 return;
257 };
258
259 project.update(cx, |_, cx| {
260 cx.emit(project::Event::RevealInProjectPanel(entry_id));
261 });
262}
263
264fn open_thread(
265 workspace: &mut Workspace,
266 id: acp::SessionId,
267 name: String,
268 window: &mut Window,
269 cx: &mut Context<Workspace>,
270) {
271 use crate::AgentPanel;
272
273 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
274 return;
275 };
276
277 panel.update(cx, |panel, cx| {
278 panel.load_agent_thread(id, None, Some(name.into()), window, cx)
279 });
280}
281
282fn open_rule(
283 _workspace: &mut Workspace,
284 id: PromptId,
285 window: &mut Window,
286 cx: &mut Context<Workspace>,
287) {
288 use zed_actions::assistant::OpenRulesLibrary;
289
290 let PromptId::User { uuid } = id else {
291 return;
292 };
293
294 window.dispatch_action(
295 Box::new(OpenRulesLibrary {
296 prompt_to_select: Some(uuid.0),
297 }),
298 cx,
299 );
300}