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