picker_prompt.rs

  1use futures::channel::oneshot;
  2use fuzzy::{StringMatch, StringMatchCandidate};
  3
  4use core::cmp;
  5use gpui::{
  6    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
  7    IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity,
  8    Window, rems,
  9};
 10use picker::{Picker, PickerDelegate};
 11use std::sync::Arc;
 12use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
 13use util::ResultExt;
 14use workspace::{ModalView, Workspace};
 15
 16pub struct PickerPrompt {
 17    pub picker: Entity<Picker<PickerPromptDelegate>>,
 18    rem_width: f32,
 19    _subscription: Subscription,
 20}
 21
 22pub fn prompt(
 23    prompt: &str,
 24    options: Vec<SharedString>,
 25    workspace: WeakEntity<Workspace>,
 26    window: &mut Window,
 27    cx: &mut App,
 28) -> Task<Option<usize>> {
 29    if options.is_empty() {
 30        return Task::ready(None);
 31    }
 32    let prompt = prompt.to_string().into();
 33
 34    window.spawn(cx, async move |cx| {
 35        // Modal branch picker has a longer trailoff than a popover one.
 36        let (tx, rx) = oneshot::channel();
 37        let delegate = PickerPromptDelegate::new(prompt, options, tx, 70);
 38
 39        workspace
 40            .update_in(cx, |workspace, window, cx| {
 41                workspace.toggle_modal(window, cx, |window, cx| {
 42                    PickerPrompt::new(delegate, 34., window, cx)
 43                })
 44            })
 45            .ok();
 46
 47        (rx.await).ok()
 48    })
 49}
 50
 51impl PickerPrompt {
 52    fn new(
 53        delegate: PickerPromptDelegate,
 54        rem_width: f32,
 55        window: &mut Window,
 56        cx: &mut Context<Self>,
 57    ) -> Self {
 58        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 59        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
 60        Self {
 61            picker,
 62            rem_width,
 63            _subscription,
 64        }
 65    }
 66}
 67impl ModalView for PickerPrompt {}
 68impl EventEmitter<DismissEvent> for PickerPrompt {}
 69
 70impl Focusable for PickerPrompt {
 71    fn focus_handle(&self, cx: &App) -> FocusHandle {
 72        self.picker.focus_handle(cx)
 73    }
 74}
 75
 76impl Render for PickerPrompt {
 77    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 78        v_flex()
 79            .w(rems(self.rem_width))
 80            .child(self.picker.clone())
 81            .on_mouse_down_out(cx.listener(|this, _, window, cx| {
 82                this.picker.update(cx, |this, cx| {
 83                    this.cancel(&Default::default(), window, cx);
 84                })
 85            }))
 86    }
 87}
 88
 89pub struct PickerPromptDelegate {
 90    prompt: Arc<str>,
 91    matches: Vec<StringMatch>,
 92    all_options: Vec<SharedString>,
 93    selected_index: usize,
 94    max_match_length: usize,
 95    tx: Option<oneshot::Sender<usize>>,
 96}
 97
 98impl PickerPromptDelegate {
 99    pub fn new(
100        prompt: Arc<str>,
101        options: Vec<SharedString>,
102        tx: oneshot::Sender<usize>,
103        max_chars: usize,
104    ) -> Self {
105        Self {
106            prompt,
107            all_options: options,
108            matches: vec![],
109            selected_index: 0,
110            max_match_length: max_chars,
111            tx: Some(tx),
112        }
113    }
114}
115
116impl PickerDelegate for PickerPromptDelegate {
117    type ListItem = ListItem;
118
119    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
120        self.prompt.clone()
121    }
122
123    fn match_count(&self) -> usize {
124        self.matches.len()
125    }
126
127    fn selected_index(&self) -> usize {
128        self.selected_index
129    }
130
131    fn set_selected_index(
132        &mut self,
133        ix: usize,
134        _window: &mut Window,
135        _: &mut Context<Picker<Self>>,
136    ) {
137        self.selected_index = ix;
138    }
139
140    fn update_matches(
141        &mut self,
142        query: String,
143        window: &mut Window,
144        cx: &mut Context<Picker<Self>>,
145    ) -> Task<()> {
146        cx.spawn_in(window, async move |picker, cx| {
147            let candidates = picker.read_with(cx, |picker, _| {
148                picker
149                    .delegate
150                    .all_options
151                    .iter()
152                    .enumerate()
153                    .map(|(ix, option)| StringMatchCandidate::new(ix, &option))
154                    .collect::<Vec<StringMatchCandidate>>()
155            });
156            let Some(candidates) = candidates.log_err() else {
157                return;
158            };
159            let matches: Vec<StringMatch> = if query.is_empty() {
160                candidates
161                    .into_iter()
162                    .enumerate()
163                    .map(|(index, candidate)| StringMatch {
164                        candidate_id: index,
165                        string: candidate.string,
166                        positions: Vec::new(),
167                        score: 0.0,
168                    })
169                    .collect()
170            } else {
171                fuzzy::match_strings(
172                    &candidates,
173                    &query,
174                    true,
175                    10000,
176                    &Default::default(),
177                    cx.background_executor().clone(),
178                )
179                .await
180            };
181            picker
182                .update(cx, |picker, _| {
183                    let delegate = &mut picker.delegate;
184                    delegate.matches = matches;
185                    if delegate.matches.is_empty() {
186                        delegate.selected_index = 0;
187                    } else {
188                        delegate.selected_index =
189                            cmp::min(delegate.selected_index, delegate.matches.len() - 1);
190                    }
191                })
192                .log_err();
193        })
194    }
195
196    fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
197        let Some(option) = self.matches.get(self.selected_index()) else {
198            return;
199        };
200
201        self.tx.take().map(|tx| tx.send(option.candidate_id));
202        cx.emit(DismissEvent);
203    }
204
205    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
206        cx.emit(DismissEvent);
207    }
208
209    fn render_match(
210        &self,
211        ix: usize,
212        selected: bool,
213        _window: &mut Window,
214        _cx: &mut Context<Picker<Self>>,
215    ) -> Option<Self::ListItem> {
216        let hit = &self.matches[ix];
217        let shortened_option = util::truncate_and_trailoff(&hit.string, self.max_match_length);
218
219        Some(
220            ListItem::new(SharedString::from(format!("picker-prompt-menu-{ix}")))
221                .inset(true)
222                .spacing(ListItemSpacing::Sparse)
223                .toggle_state(selected)
224                .map(|el| {
225                    let highlights: Vec<_> = hit
226                        .positions
227                        .iter()
228                        .filter(|index| index < &&self.max_match_length)
229                        .copied()
230                        .collect();
231
232                    el.child(HighlightedLabel::new(shortened_option, highlights))
233                }),
234        )
235    }
236}