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                .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}