context_pill.rs

  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}