1use std::{rc::Rc, time::Duration};
2
3use file_icons::FileIcons;
4use gpui::ClickEvent;
5use gpui::{Animation, AnimationExt as _, pulsating_between};
6use ui::{IconButtonShape, Tooltip, prelude::*};
7
8use crate::context::{AssistantContext, ContextId, ContextKind};
9
10#[derive(IntoElement)]
11pub enum ContextPill {
12 Added {
13 context: AddedContext,
14 dupe_name: bool,
15 focused: bool,
16 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
17 on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
18 },
19 Suggested {
20 name: SharedString,
21 icon_path: Option<SharedString>,
22 kind: ContextKind,
23 focused: bool,
24 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
25 },
26}
27
28impl ContextPill {
29 pub fn added(
30 context: AddedContext,
31 dupe_name: bool,
32 focused: bool,
33 on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
34 ) -> Self {
35 Self::Added {
36 context,
37 dupe_name,
38 on_remove,
39 focused,
40 on_click: None,
41 }
42 }
43
44 pub fn suggested(
45 name: SharedString,
46 icon_path: Option<SharedString>,
47 kind: ContextKind,
48 focused: bool,
49 ) -> Self {
50 Self::Suggested {
51 name,
52 icon_path,
53 kind,
54 focused,
55 on_click: None,
56 }
57 }
58
59 pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
60 match &mut self {
61 ContextPill::Added { on_click, .. } => {
62 *on_click = Some(listener);
63 }
64 ContextPill::Suggested { on_click, .. } => {
65 *on_click = Some(listener);
66 }
67 }
68 self
69 }
70
71 pub fn id(&self) -> ElementId {
72 match self {
73 Self::Added { context, .. } => {
74 ElementId::NamedInteger("context-pill".into(), context.id.0)
75 }
76 Self::Suggested { .. } => "suggested-context-pill".into(),
77 }
78 }
79
80 pub fn icon(&self) -> Icon {
81 match self {
82 Self::Suggested {
83 icon_path: Some(icon_path),
84 ..
85 }
86 | Self::Added {
87 context:
88 AddedContext {
89 icon_path: Some(icon_path),
90 ..
91 },
92 ..
93 } => Icon::from_path(icon_path),
94 Self::Suggested { kind, .. }
95 | Self::Added {
96 context: AddedContext { kind, .. },
97 ..
98 } => Icon::new(kind.icon()),
99 }
100 }
101}
102
103impl RenderOnce for ContextPill {
104 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
105 let color = cx.theme().colors();
106
107 let base_pill = h_flex()
108 .id(self.id())
109 .pl_1()
110 .pb(px(1.))
111 .border_1()
112 .rounded_sm()
113 .gap_1()
114 .child(self.icon().size(IconSize::XSmall).color(Color::Muted));
115
116 match &self {
117 ContextPill::Added {
118 context,
119 dupe_name,
120 on_remove,
121 focused,
122 on_click,
123 } => base_pill
124 .bg(color.element_background)
125 .border_color(if *focused {
126 color.border_focused
127 } else {
128 color.border.opacity(0.5)
129 })
130 .pr(if on_remove.is_some() { px(2.) } else { px(4.) })
131 .child(
132 h_flex()
133 .id("context-data")
134 .gap_1()
135 .child(
136 div().max_w_64().child(
137 Label::new(context.name.clone())
138 .size(LabelSize::Small)
139 .truncate(),
140 ),
141 )
142 .when_some(context.parent.as_ref(), |element, parent_name| {
143 if *dupe_name {
144 element.child(
145 Label::new(parent_name.clone())
146 .size(LabelSize::XSmall)
147 .color(Color::Muted),
148 )
149 } else {
150 element
151 }
152 })
153 .when_some(context.tooltip.as_ref(), |element, tooltip| {
154 element.tooltip(Tooltip::text(tooltip.clone()))
155 }),
156 )
157 .when_some(on_remove.as_ref(), |element, on_remove| {
158 element.child(
159 IconButton::new(("remove", context.id.0), IconName::Close)
160 .shape(IconButtonShape::Square)
161 .icon_size(IconSize::XSmall)
162 .tooltip(Tooltip::text("Remove Context"))
163 .on_click({
164 let on_remove = on_remove.clone();
165 move |event, window, cx| on_remove(event, window, cx)
166 }),
167 )
168 })
169 .when_some(on_click.as_ref(), |element, on_click| {
170 let on_click = on_click.clone();
171 element
172 .cursor_pointer()
173 .on_click(move |event, window, cx| on_click(event, window, cx))
174 })
175 .map(|element| {
176 if context.summarizing {
177 element
178 .tooltip(ui::Tooltip::text("Summarizing..."))
179 .with_animation(
180 "pulsating-ctx-pill",
181 Animation::new(Duration::from_secs(2))
182 .repeat()
183 .with_easing(pulsating_between(0.4, 0.8)),
184 |label, delta| label.opacity(delta),
185 )
186 .into_any_element()
187 } else {
188 element.into_any()
189 }
190 }),
191 ContextPill::Suggested {
192 name,
193 icon_path: _,
194 kind: _,
195 focused,
196 on_click,
197 } => base_pill
198 .cursor_pointer()
199 .pr_1()
200 .border_dashed()
201 .border_color(if *focused {
202 color.border_focused
203 } else {
204 color.border
205 })
206 .hover(|style| style.bg(color.element_hover.opacity(0.5)))
207 .when(*focused, |this| {
208 this.bg(color.element_background.opacity(0.5))
209 })
210 .child(
211 div().max_w_64().child(
212 Label::new(name.clone())
213 .size(LabelSize::Small)
214 .color(Color::Muted)
215 .truncate(),
216 ),
217 )
218 .tooltip(|window, cx| {
219 Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
220 })
221 .when_some(on_click.as_ref(), |element, on_click| {
222 let on_click = on_click.clone();
223 element.on_click(move |event, window, cx| on_click(event, window, cx))
224 })
225 .into_any(),
226 }
227 }
228}
229
230pub struct AddedContext {
231 pub id: ContextId,
232 pub kind: ContextKind,
233 pub name: SharedString,
234 pub parent: Option<SharedString>,
235 pub tooltip: Option<SharedString>,
236 pub icon_path: Option<SharedString>,
237 pub summarizing: bool,
238}
239
240impl AddedContext {
241 pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
242 match context {
243 AssistantContext::File(file_context) => {
244 let full_path = file_context.context_buffer.file.full_path(cx);
245 let full_path_string: SharedString =
246 full_path.to_string_lossy().into_owned().into();
247 let name = full_path
248 .file_name()
249 .map(|n| n.to_string_lossy().into_owned().into())
250 .unwrap_or_else(|| full_path_string.clone());
251 let parent = full_path
252 .parent()
253 .and_then(|p| p.file_name())
254 .map(|n| n.to_string_lossy().into_owned().into());
255 AddedContext {
256 id: file_context.id,
257 kind: ContextKind::File,
258 name,
259 parent,
260 tooltip: Some(full_path_string),
261 icon_path: FileIcons::get_icon(&full_path, cx),
262 summarizing: false,
263 }
264 }
265
266 AssistantContext::Directory(directory_context) => {
267 let full_path = directory_context
268 .worktree
269 .read(cx)
270 .full_path(&directory_context.path);
271 let full_path_string: SharedString =
272 full_path.to_string_lossy().into_owned().into();
273 let name = full_path
274 .file_name()
275 .map(|n| n.to_string_lossy().into_owned().into())
276 .unwrap_or_else(|| full_path_string.clone());
277 let parent = full_path
278 .parent()
279 .and_then(|p| p.file_name())
280 .map(|n| n.to_string_lossy().into_owned().into());
281 AddedContext {
282 id: directory_context.id,
283 kind: ContextKind::Directory,
284 name,
285 parent,
286 tooltip: Some(full_path_string),
287 icon_path: None,
288 summarizing: false,
289 }
290 }
291
292 AssistantContext::Symbol(symbol_context) => AddedContext {
293 id: symbol_context.id,
294 kind: ContextKind::Symbol,
295 name: symbol_context.context_symbol.id.name.clone(),
296 parent: None,
297 tooltip: None,
298 icon_path: None,
299 summarizing: false,
300 },
301
302 AssistantContext::Excerpt(excerpt_context) => {
303 let full_path = excerpt_context.context_buffer.file.full_path(cx);
304 let mut full_path_string = full_path.to_string_lossy().into_owned();
305 let mut name = full_path
306 .file_name()
307 .map(|n| n.to_string_lossy().into_owned())
308 .unwrap_or_else(|| full_path_string.clone());
309
310 let line_range_text = format!(
311 " ({}-{})",
312 excerpt_context.line_range.start.row + 1,
313 excerpt_context.line_range.end.row + 1
314 );
315
316 full_path_string.push_str(&line_range_text);
317 name.push_str(&line_range_text);
318
319 let parent = full_path
320 .parent()
321 .and_then(|p| p.file_name())
322 .map(|n| n.to_string_lossy().into_owned().into());
323
324 AddedContext {
325 id: excerpt_context.id,
326 kind: ContextKind::File, // Use File icon for excerpts
327 name: name.into(),
328 parent,
329 tooltip: Some(full_path_string.into()),
330 icon_path: FileIcons::get_icon(&full_path, cx),
331 summarizing: false,
332 }
333 }
334
335 AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
336 id: fetched_url_context.id,
337 kind: ContextKind::FetchedUrl,
338 name: fetched_url_context.url.clone(),
339 parent: None,
340 tooltip: None,
341 icon_path: None,
342 summarizing: false,
343 },
344
345 AssistantContext::Thread(thread_context) => AddedContext {
346 id: thread_context.id,
347 kind: ContextKind::Thread,
348 name: thread_context.summary(cx),
349 parent: None,
350 tooltip: None,
351 icon_path: None,
352 summarizing: thread_context
353 .thread
354 .read(cx)
355 .is_generating_detailed_summary(),
356 },
357 }
358 }
359}