context_picker.rs

  1use std::sync::Arc;
  2
  3use gpui::{DismissEvent, SharedString, Task, WeakView};
  4use picker::{Picker, PickerDelegate, PickerEditorPosition};
  5use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
  6
  7use crate::message_editor::MessageEditor;
  8
  9#[derive(IntoElement)]
 10pub(super) struct ContextPicker<T: PopoverTrigger> {
 11    message_editor: WeakView<MessageEditor>,
 12    trigger: T,
 13}
 14
 15#[derive(Clone)]
 16struct ContextPickerEntry {
 17    name: SharedString,
 18    description: SharedString,
 19    icon: IconName,
 20}
 21
 22pub(crate) struct ContextPickerDelegate {
 23    all_entries: Vec<ContextPickerEntry>,
 24    filtered_entries: Vec<ContextPickerEntry>,
 25    message_editor: WeakView<MessageEditor>,
 26    selected_ix: usize,
 27}
 28
 29impl<T: PopoverTrigger> ContextPicker<T> {
 30    pub(crate) fn new(message_editor: WeakView<MessageEditor>, trigger: T) -> Self {
 31        ContextPicker {
 32            message_editor,
 33            trigger,
 34        }
 35    }
 36}
 37
 38impl PickerDelegate for ContextPickerDelegate {
 39    type ListItem = ListItem;
 40
 41    fn match_count(&self) -> usize {
 42        self.filtered_entries.len()
 43    }
 44
 45    fn selected_index(&self) -> usize {
 46        self.selected_ix
 47    }
 48
 49    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
 50        self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
 51        cx.notify();
 52    }
 53
 54    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
 55        "Select a context source…".into()
 56    }
 57
 58    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
 59        let all_commands = self.all_entries.clone();
 60        cx.spawn(|this, mut cx| async move {
 61            let filtered_commands = cx
 62                .background_executor()
 63                .spawn(async move {
 64                    if query.is_empty() {
 65                        all_commands
 66                    } else {
 67                        all_commands
 68                            .into_iter()
 69                            .filter(|model_info| {
 70                                model_info
 71                                    .name
 72                                    .to_lowercase()
 73                                    .contains(&query.to_lowercase())
 74                            })
 75                            .collect()
 76                    }
 77                })
 78                .await;
 79
 80            this.update(&mut cx, |this, cx| {
 81                this.delegate.filtered_entries = filtered_commands;
 82                this.delegate.set_selected_index(0, cx);
 83                cx.notify();
 84            })
 85            .ok();
 86        })
 87    }
 88
 89    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
 90        if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
 91            self.message_editor
 92                .update(cx, |_message_editor, _cx| {
 93                    println!("Insert context from {}", entry.name);
 94                })
 95                .ok();
 96            cx.emit(DismissEvent);
 97        }
 98    }
 99
100    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
101
102    fn editor_position(&self) -> PickerEditorPosition {
103        PickerEditorPosition::End
104    }
105
106    fn render_match(
107        &self,
108        ix: usize,
109        selected: bool,
110        _cx: &mut ViewContext<Picker<Self>>,
111    ) -> Option<Self::ListItem> {
112        let entry = self.filtered_entries.get(ix)?;
113
114        Some(
115            ListItem::new(ix)
116                .inset(true)
117                .spacing(ListItemSpacing::Dense)
118                .selected(selected)
119                .tooltip({
120                    let description = entry.description.clone();
121                    move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
122                })
123                .child(
124                    v_flex()
125                        .group(format!("context-entry-label-{ix}"))
126                        .w_full()
127                        .py_0p5()
128                        .min_w(px(250.))
129                        .max_w(px(400.))
130                        .child(
131                            h_flex()
132                                .gap_1p5()
133                                .child(Icon::new(entry.icon).size(IconSize::XSmall))
134                                .child(
135                                    Label::new(entry.name.clone())
136                                        .single_line()
137                                        .size(LabelSize::Small),
138                                ),
139                        )
140                        .child(
141                            div().overflow_hidden().text_ellipsis().child(
142                                Label::new(entry.description.clone())
143                                    .size(LabelSize::Small)
144                                    .color(Color::Muted),
145                            ),
146                        ),
147                ),
148        )
149    }
150}
151
152impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
153    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
154        let entries = vec![
155            ContextPickerEntry {
156                name: "directory".into(),
157                description: "Insert any directory".into(),
158                icon: IconName::Folder,
159            },
160            ContextPickerEntry {
161                name: "file".into(),
162                description: "Insert any file".into(),
163                icon: IconName::File,
164            },
165            ContextPickerEntry {
166                name: "web".into(),
167                description: "Fetch content from URL".into(),
168                icon: IconName::Globe,
169            },
170        ];
171
172        let delegate = ContextPickerDelegate {
173            all_entries: entries.clone(),
174            message_editor: self.message_editor.clone(),
175            filtered_entries: entries,
176            selected_ix: 0,
177        };
178
179        let picker =
180            cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
181
182        let handle = self
183            .message_editor
184            .update(cx, |this, _| this.context_picker_handle.clone())
185            .ok();
186        PopoverMenu::new("context-picker")
187            .menu(move |_cx| Some(picker.clone()))
188            .trigger(self.trigger)
189            .attach(gpui::AnchorCorner::TopLeft)
190            .anchor(gpui::AnchorCorner::BottomLeft)
191            .offset(gpui::Point {
192                x: px(0.0),
193                y: px(-16.0),
194            })
195            .when_some(handle, |this, handle| this.with_handle(handle))
196    }
197}