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}