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, TaskExt, WeakEntity, Window,
8 pulsating_between,
9};
10use prompt_store::PromptId;
11use rope::Point;
12use settings::Settings;
13use theme_settings::ThemeSettings;
14use ui::{ButtonLike, TintColor, Tooltip, prelude::*};
15use workspace::{OpenOptions, Workspace};
16
17use crate::Agent;
18
19#[derive(IntoElement)]
20pub struct MentionCrease {
21 id: ElementId,
22 icon: SharedString,
23 label: SharedString,
24 mention_uri: Option<MentionUri>,
25 workspace: Option<WeakEntity<Workspace>>,
26 is_toggled: bool,
27 is_loading: bool,
28 tooltip: Option<SharedString>,
29 image_preview: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
30}
31
32impl MentionCrease {
33 pub fn new(
34 id: impl Into<ElementId>,
35 icon: impl Into<SharedString>,
36 label: impl Into<SharedString>,
37 ) -> Self {
38 Self {
39 id: id.into(),
40 icon: icon.into(),
41 label: label.into(),
42 mention_uri: None,
43 workspace: None,
44 is_toggled: false,
45 is_loading: false,
46 tooltip: None,
47 image_preview: None,
48 }
49 }
50
51 pub fn mention_uri(mut self, mention_uri: Option<MentionUri>) -> Self {
52 self.mention_uri = mention_uri;
53 self
54 }
55
56 pub fn workspace(mut self, workspace: Option<WeakEntity<Workspace>>) -> Self {
57 self.workspace = workspace;
58 self
59 }
60
61 pub fn is_toggled(mut self, is_toggled: bool) -> Self {
62 self.is_toggled = is_toggled;
63 self
64 }
65
66 pub fn is_loading(mut self, is_loading: bool) -> Self {
67 self.is_loading = is_loading;
68 self
69 }
70
71 pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
72 self.tooltip = Some(tooltip.into());
73 self
74 }
75
76 pub fn image_preview(
77 mut self,
78 builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
79 ) -> Self {
80 self.image_preview = Some(Box::new(builder));
81 self
82 }
83}
84
85impl RenderOnce for MentionCrease {
86 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
87 let settings = ThemeSettings::get_global(cx);
88 let font_size = settings.agent_buffer_font_size(cx);
89 let buffer_font = settings.buffer_font.clone();
90 let is_loading = self.is_loading;
91 let tooltip = self.tooltip;
92 let image_preview = self.image_preview;
93
94 let button_height = DefiniteLength::Absolute(AbsoluteLength::Pixels(
95 px(window.line_height().into()) - px(1.),
96 ));
97
98 ButtonLike::new(self.id)
99 .style(ButtonStyle::Outlined)
100 .size(ButtonSize::Compact)
101 .height(button_height)
102 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
103 .toggle_state(self.is_toggled)
104 .when_some(
105 self.mention_uri.clone().zip(self.workspace.clone()),
106 |this, (mention_uri, workspace)| {
107 this.on_click(move |_event, window, cx| {
108 open_mention_uri(mention_uri.clone(), &workspace, window, cx);
109 })
110 },
111 )
112 .child(
113 h_flex()
114 .pb_px()
115 .gap_1()
116 .font(buffer_font)
117 .text_size(font_size)
118 .child(
119 Icon::from_path(self.icon.clone())
120 .size(IconSize::XSmall)
121 .color(Color::Muted),
122 )
123 .child(self.label.clone())
124 .map(|this| {
125 if is_loading {
126 this.with_animation(
127 "loading-context-crease",
128 Animation::new(Duration::from_secs(2))
129 .repeat()
130 .with_easing(pulsating_between(0.4, 0.8)),
131 |label, delta| label.opacity(delta),
132 )
133 .into_any()
134 } else {
135 this.into_any()
136 }
137 }),
138 )
139 .map(|button| {
140 if let Some(image_preview) = image_preview {
141 button.hoverable_tooltip(image_preview)
142 } else {
143 button.when_some(tooltip, |this, tooltip_text| {
144 this.tooltip(Tooltip::text(tooltip_text))
145 })
146 }
147 })
148 }
149}
150
151fn open_mention_uri(
152 mention_uri: MentionUri,
153 workspace: &WeakEntity<Workspace>,
154 window: &mut Window,
155 cx: &mut App,
156) {
157 let Some(workspace) = workspace.upgrade() else {
158 return;
159 };
160
161 workspace.update(cx, |workspace, cx| match mention_uri {
162 MentionUri::File { abs_path } => {
163 open_file(workspace, abs_path, None, window, cx);
164 }
165 MentionUri::Symbol {
166 abs_path,
167 line_range,
168 ..
169 }
170 | MentionUri::Selection {
171 abs_path: Some(abs_path),
172 line_range,
173 } => {
174 open_file(workspace, abs_path, Some(line_range), window, cx);
175 }
176 MentionUri::Directory { abs_path } => {
177 reveal_in_project_panel(workspace, abs_path, cx);
178 }
179 MentionUri::Thread { id, name } => {
180 open_thread(workspace, id, name, window, cx);
181 }
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}