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