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}