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::TextThread { .. } => {}
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}