thread_context_picker.rs

  1use std::sync::Arc;
  2use std::sync::atomic::AtomicBool;
  3
  4use fuzzy::StringMatchCandidate;
  5use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
  6use picker::{Picker, PickerDelegate};
  7use ui::{ListItem, prelude::*};
  8
  9use crate::context_picker::ContextPicker;
 10use crate::context_store::{self, ContextStore};
 11use crate::thread::ThreadId;
 12use crate::thread_store::ThreadStore;
 13
 14pub struct ThreadContextPicker {
 15    picker: Entity<Picker<ThreadContextPickerDelegate>>,
 16}
 17
 18impl ThreadContextPicker {
 19    pub fn new(
 20        thread_store: WeakEntity<ThreadStore>,
 21        context_picker: WeakEntity<ContextPicker>,
 22        context_store: WeakEntity<context_store::ContextStore>,
 23        window: &mut Window,
 24        cx: &mut Context<Self>,
 25    ) -> Self {
 26        let delegate =
 27            ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
 28        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 29
 30        ThreadContextPicker { picker }
 31    }
 32}
 33
 34impl Focusable for ThreadContextPicker {
 35    fn focus_handle(&self, cx: &App) -> FocusHandle {
 36        self.picker.focus_handle(cx)
 37    }
 38}
 39
 40impl Render for ThreadContextPicker {
 41    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 42        self.picker.clone()
 43    }
 44}
 45
 46#[derive(Debug, Clone)]
 47pub struct ThreadContextEntry {
 48    pub id: ThreadId,
 49    pub summary: SharedString,
 50}
 51
 52pub struct ThreadContextPickerDelegate {
 53    thread_store: WeakEntity<ThreadStore>,
 54    context_picker: WeakEntity<ContextPicker>,
 55    context_store: WeakEntity<context_store::ContextStore>,
 56    matches: Vec<ThreadContextEntry>,
 57    selected_index: usize,
 58}
 59
 60impl ThreadContextPickerDelegate {
 61    pub fn new(
 62        thread_store: WeakEntity<ThreadStore>,
 63        context_picker: WeakEntity<ContextPicker>,
 64        context_store: WeakEntity<context_store::ContextStore>,
 65    ) -> Self {
 66        ThreadContextPickerDelegate {
 67            thread_store,
 68            context_picker,
 69            context_store,
 70            matches: Vec::new(),
 71            selected_index: 0,
 72        }
 73    }
 74}
 75
 76impl PickerDelegate for ThreadContextPickerDelegate {
 77    type ListItem = ListItem;
 78
 79    fn match_count(&self) -> usize {
 80        self.matches.len()
 81    }
 82
 83    fn selected_index(&self) -> usize {
 84        self.selected_index
 85    }
 86
 87    fn set_selected_index(
 88        &mut self,
 89        ix: usize,
 90        _window: &mut Window,
 91        _cx: &mut Context<Picker<Self>>,
 92    ) {
 93        self.selected_index = ix;
 94    }
 95
 96    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 97        "Search threads…".into()
 98    }
 99
100    fn update_matches(
101        &mut self,
102        query: String,
103        window: &mut Window,
104        cx: &mut Context<Picker<Self>>,
105    ) -> Task<()> {
106        let Some(thread_store) = self.thread_store.upgrade() else {
107            return Task::ready(());
108        };
109
110        let search_task = search_threads(query, Arc::new(AtomicBool::default()), thread_store, cx);
111        cx.spawn_in(window, async move |this, cx| {
112            let matches = search_task.await;
113            this.update(cx, |this, cx| {
114                this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
115                this.delegate.selected_index = 0;
116                cx.notify();
117            })
118            .ok();
119        })
120    }
121
122    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
123        let Some(entry) = self.matches.get(self.selected_index) else {
124            return;
125        };
126
127        let Some(thread_store) = self.thread_store.upgrade() else {
128            return;
129        };
130
131        let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx));
132
133        cx.spawn(async move |this, cx| {
134            let thread = open_thread_task.await?;
135            this.update(cx, |this, cx| {
136                this.delegate
137                    .context_store
138                    .update(cx, |context_store, cx| {
139                        context_store.add_thread(thread, true, cx)
140                    })
141                    .ok();
142            })
143        })
144        .detach_and_log_err(cx);
145    }
146
147    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
148        self.context_picker
149            .update(cx, |_, cx| {
150                cx.emit(DismissEvent);
151            })
152            .ok();
153    }
154
155    fn render_match(
156        &self,
157        ix: usize,
158        selected: bool,
159        _window: &mut Window,
160        cx: &mut Context<Picker<Self>>,
161    ) -> Option<Self::ListItem> {
162        let thread = &self.matches[ix];
163
164        Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
165            render_thread_context_entry(thread, self.context_store.clone(), cx),
166        ))
167    }
168}
169
170pub fn render_thread_context_entry(
171    thread: &ThreadContextEntry,
172    context_store: WeakEntity<ContextStore>,
173    cx: &mut App,
174) -> Div {
175    let added = context_store.upgrade().map_or(false, |ctx_store| {
176        ctx_store.read(cx).includes_thread(&thread.id)
177    });
178
179    h_flex()
180        .gap_1p5()
181        .w_full()
182        .justify_between()
183        .child(
184            h_flex()
185                .gap_1p5()
186                .max_w_72()
187                .child(
188                    Icon::new(IconName::MessageBubbles)
189                        .size(IconSize::XSmall)
190                        .color(Color::Muted),
191                )
192                .child(Label::new(thread.summary.clone()).truncate()),
193        )
194        .when(added, |el| {
195            el.child(
196                h_flex()
197                    .gap_1()
198                    .child(
199                        Icon::new(IconName::Check)
200                            .size(IconSize::Small)
201                            .color(Color::Success),
202                    )
203                    .child(Label::new("Added").size(LabelSize::Small)),
204            )
205        })
206}
207
208#[derive(Clone)]
209pub struct ThreadMatch {
210    pub thread: ThreadContextEntry,
211    pub is_recent: bool,
212}
213
214pub(crate) fn search_threads(
215    query: String,
216    cancellation_flag: Arc<AtomicBool>,
217    thread_store: Entity<ThreadStore>,
218    cx: &mut App,
219) -> Task<Vec<ThreadMatch>> {
220    let threads = thread_store
221        .read(cx)
222        .reverse_chronological_threads()
223        .into_iter()
224        .map(|thread| ThreadContextEntry {
225            id: thread.id,
226            summary: thread.summary,
227        })
228        .collect::<Vec<_>>();
229
230    let executor = cx.background_executor().clone();
231    cx.background_spawn(async move {
232        if query.is_empty() {
233            threads
234                .into_iter()
235                .map(|thread| ThreadMatch {
236                    thread,
237                    is_recent: false,
238                })
239                .collect()
240        } else {
241            let candidates = threads
242                .iter()
243                .enumerate()
244                .map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
245                .collect::<Vec<_>>();
246            let matches = fuzzy::match_strings(
247                &candidates,
248                &query,
249                false,
250                100,
251                &cancellation_flag,
252                executor,
253            )
254            .await;
255
256            matches
257                .into_iter()
258                .map(|mat| ThreadMatch {
259                    thread: threads[mat.candidate_id].clone(),
260                    is_recent: false,
261                })
262                .collect()
263        }
264    })
265}