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 .when(*focused, |this| {
201 this.bg(color.element_background.opacity(0.5))
202 })
203 .border_dashed()
204 .border_color(if *focused {
205 color.border_focused
206 } else {
207 color.border
208 })
209 .hover(|style| style.bg(color.element_hover.opacity(0.5)))
210 .child(
211 div().px_0p5().max_w_64().child(
212 Label::new(name.clone())
213 .size(LabelSize::Small)
214 .color(Color::Muted)
215 .truncate(),
216 ),
217 )
218 .child(
219 Label::new(match kind {
220 ContextKind::File => "Active Tab",
221 ContextKind::Thread
222 | ContextKind::Directory
223 | ContextKind::FetchedUrl
224 | ContextKind::Symbol => "Active",
225 })
226 .size(LabelSize::XSmall)
227 .color(Color::Muted),
228 )
229 .child(
230 Icon::new(IconName::Plus)
231 .size(IconSize::XSmall)
232 .into_any_element(),
233 )
234 .tooltip(|window, cx| {
235 Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
236 })
237 .when_some(on_click.as_ref(), |element, on_click| {
238 let on_click = on_click.clone();
239 element.on_click(move |event, window, cx| on_click(event, window, cx))
240 })
241 .into_any(),
242 }
243 }
244}
245
246pub struct AddedContext {
247 pub id: ContextId,
248 pub kind: ContextKind,
249 pub name: SharedString,
250 pub parent: Option<SharedString>,
251 pub tooltip: Option<SharedString>,
252 pub icon_path: Option<SharedString>,
253 pub summarizing: bool,
254}
255
256impl AddedContext {
257 pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
258 match context {
259 AssistantContext::File(file_context) => {
260 let full_path = file_context.context_buffer.file.full_path(cx);
261 let full_path_string: SharedString =
262 full_path.to_string_lossy().into_owned().into();
263 let name = full_path
264 .file_name()
265 .map(|n| n.to_string_lossy().into_owned().into())
266 .unwrap_or_else(|| full_path_string.clone());
267 let parent = full_path
268 .parent()
269 .and_then(|p| p.file_name())
270 .map(|n| n.to_string_lossy().into_owned().into());
271 AddedContext {
272 id: file_context.id,
273 kind: ContextKind::File,
274 name,
275 parent,
276 tooltip: Some(full_path_string),
277 icon_path: FileIcons::get_icon(&full_path, cx),
278 summarizing: false,
279 }
280 }
281
282 AssistantContext::Directory(directory_context) => {
283 let full_path = directory_context
284 .worktree
285 .read(cx)
286 .full_path(&directory_context.path);
287 let full_path_string: SharedString =
288 full_path.to_string_lossy().into_owned().into();
289 let name = full_path
290 .file_name()
291 .map(|n| n.to_string_lossy().into_owned().into())
292 .unwrap_or_else(|| full_path_string.clone());
293 let parent = full_path
294 .parent()
295 .and_then(|p| p.file_name())
296 .map(|n| n.to_string_lossy().into_owned().into());
297 AddedContext {
298 id: directory_context.id,
299 kind: ContextKind::Directory,
300 name,
301 parent,
302 tooltip: Some(full_path_string),
303 icon_path: None,
304 summarizing: false,
305 }
306 }
307
308 AssistantContext::Symbol(symbol_context) => AddedContext {
309 id: symbol_context.id,
310 kind: ContextKind::Symbol,
311 name: symbol_context.context_symbol.id.name.clone(),
312 parent: None,
313 tooltip: None,
314 icon_path: None,
315 summarizing: false,
316 },
317
318 AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
319 id: fetched_url_context.id,
320 kind: ContextKind::FetchedUrl,
321 name: fetched_url_context.url.clone(),
322 parent: None,
323 tooltip: None,
324 icon_path: None,
325 summarizing: false,
326 },
327
328 AssistantContext::Thread(thread_context) => AddedContext {
329 id: thread_context.id,
330 kind: ContextKind::Thread,
331 name: thread_context.summary(cx),
332 parent: None,
333 tooltip: None,
334 icon_path: None,
335 summarizing: thread_context
336 .thread
337 .read(cx)
338 .is_generating_detailed_summary(),
339 },
340 }
341 }
342}