profile_picker.rs

  1use std::sync::Arc;
  2
  3use assistant_settings::AssistantSettings;
  4use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  5use gpui::{
  6    App, Context, DismissEvent, Entity, EventEmitter, Focusable, SharedString, Task, WeakEntity,
  7    Window,
  8};
  9use picker::{Picker, PickerDelegate};
 10use settings::Settings;
 11use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
 12use util::ResultExt as _;
 13
 14pub struct ProfilePicker {
 15    picker: Entity<Picker<ProfilePickerDelegate>>,
 16}
 17
 18impl ProfilePicker {
 19    pub fn new(
 20        delegate: ProfilePickerDelegate,
 21        window: &mut Window,
 22        cx: &mut Context<Self>,
 23    ) -> Self {
 24        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
 25        Self { picker }
 26    }
 27}
 28
 29impl EventEmitter<DismissEvent> for ProfilePicker {}
 30
 31impl Focusable for ProfilePicker {
 32    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 33        self.picker.focus_handle(cx)
 34    }
 35}
 36
 37impl Render for ProfilePicker {
 38    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 39        v_flex().w(rems(34.)).child(self.picker.clone())
 40    }
 41}
 42
 43#[derive(Debug)]
 44pub struct ProfileEntry {
 45    pub id: Arc<str>,
 46    pub name: SharedString,
 47}
 48
 49pub struct ProfilePickerDelegate {
 50    profile_picker: WeakEntity<ProfilePicker>,
 51    profiles: Vec<ProfileEntry>,
 52    matches: Vec<StringMatch>,
 53    selected_index: usize,
 54    on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>,
 55}
 56
 57impl ProfilePickerDelegate {
 58    pub fn new(
 59        on_confirm: impl Fn(&Arc<str>, &mut Window, &mut App) + 'static,
 60        cx: &mut Context<ProfilePicker>,
 61    ) -> Self {
 62        let settings = AssistantSettings::get_global(cx);
 63
 64        let profiles = settings
 65            .profiles
 66            .iter()
 67            .map(|(id, profile)| ProfileEntry {
 68                id: id.clone(),
 69                name: profile.name.clone(),
 70            })
 71            .collect::<Vec<_>>();
 72
 73        Self {
 74            profile_picker: cx.entity().downgrade(),
 75            profiles,
 76            matches: Vec::new(),
 77            selected_index: 0,
 78            on_confirm: Arc::new(on_confirm),
 79        }
 80    }
 81}
 82
 83impl PickerDelegate for ProfilePickerDelegate {
 84    type ListItem = ListItem;
 85
 86    fn match_count(&self) -> usize {
 87        self.matches.len()
 88    }
 89
 90    fn selected_index(&self) -> usize {
 91        self.selected_index
 92    }
 93
 94    fn set_selected_index(
 95        &mut self,
 96        ix: usize,
 97        _window: &mut Window,
 98        _cx: &mut Context<Picker<Self>>,
 99    ) {
100        self.selected_index = ix;
101    }
102
103    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
104        "Search profiles…".into()
105    }
106
107    fn update_matches(
108        &mut self,
109        query: String,
110        window: &mut Window,
111        cx: &mut Context<Picker<Self>>,
112    ) -> Task<()> {
113        let background = cx.background_executor().clone();
114        let candidates = self
115            .profiles
116            .iter()
117            .enumerate()
118            .map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
119            .collect::<Vec<_>>();
120
121        cx.spawn_in(window, async move |this, cx| {
122            let matches = if query.is_empty() {
123                candidates
124                    .into_iter()
125                    .enumerate()
126                    .map(|(index, candidate)| StringMatch {
127                        candidate_id: index,
128                        string: candidate.string,
129                        positions: Vec::new(),
130                        score: 0.,
131                    })
132                    .collect()
133            } else {
134                match_strings(
135                    &candidates,
136                    &query,
137                    false,
138                    100,
139                    &Default::default(),
140                    background,
141                )
142                .await
143            };
144
145            this.update(cx, |this, _cx| {
146                this.delegate.matches = matches;
147                this.delegate.selected_index = this
148                    .delegate
149                    .selected_index
150                    .min(this.delegate.matches.len().saturating_sub(1));
151            })
152            .log_err();
153        })
154    }
155
156    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
157        if self.matches.is_empty() {
158            self.dismissed(window, cx);
159            return;
160        }
161
162        let candidate_id = self.matches[self.selected_index].candidate_id;
163        let profile = &self.profiles[candidate_id];
164
165        (self.on_confirm)(&profile.id, window, cx);
166    }
167
168    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
169        self.profile_picker
170            .update(cx, |_this, cx| cx.emit(DismissEvent))
171            .log_err();
172    }
173
174    fn render_match(
175        &self,
176        ix: usize,
177        selected: bool,
178        _window: &mut Window,
179        _cx: &mut Context<Picker<Self>>,
180    ) -> Option<Self::ListItem> {
181        let profile_match = &self.matches[ix];
182
183        Some(
184            ListItem::new(ix)
185                .inset(true)
186                .spacing(ListItemSpacing::Sparse)
187                .toggle_state(selected)
188                .child(HighlightedLabel::new(
189                    profile_match.string.clone(),
190                    profile_match.positions.clone(),
191                )),
192        )
193    }
194}