context_pill.rs

  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}