1use std::path::Path;
2use std::sync::Arc;
3use std::sync::atomic::AtomicBool;
4
5use chrono::{DateTime, Utc};
6use fuzzy::StringMatchCandidate;
7use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
8use picker::{Picker, PickerDelegate};
9use ui::{ListItem, prelude::*};
10
11use crate::context_picker::ContextPicker;
12use crate::context_store::{self, ContextStore};
13use crate::thread::ThreadId;
14use crate::thread_store::{TextThreadStore, ThreadStore};
15
16pub struct ThreadContextPicker {
17 picker: Entity<Picker<ThreadContextPickerDelegate>>,
18}
19
20impl ThreadContextPicker {
21 pub fn new(
22 thread_store: WeakEntity<ThreadStore>,
23 text_thread_context_store: WeakEntity<TextThreadStore>,
24 context_picker: WeakEntity<ContextPicker>,
25 context_store: WeakEntity<context_store::ContextStore>,
26 window: &mut Window,
27 cx: &mut Context<Self>,
28 ) -> Self {
29 let delegate = ThreadContextPickerDelegate::new(
30 thread_store,
31 text_thread_context_store,
32 context_picker,
33 context_store,
34 );
35 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
36
37 ThreadContextPicker { picker }
38 }
39}
40
41impl Focusable for ThreadContextPicker {
42 fn focus_handle(&self, cx: &App) -> FocusHandle {
43 self.picker.focus_handle(cx)
44 }
45}
46
47impl Render for ThreadContextPicker {
48 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
49 self.picker.clone()
50 }
51}
52
53#[derive(Debug, Clone)]
54pub enum ThreadContextEntry {
55 Thread {
56 id: ThreadId,
57 title: SharedString,
58 },
59 Context {
60 path: Arc<Path>,
61 title: SharedString,
62 },
63}
64
65impl ThreadContextEntry {
66 pub fn title(&self) -> &SharedString {
67 match self {
68 Self::Thread { title, .. } => title,
69 Self::Context { title, .. } => title,
70 }
71 }
72}
73
74pub struct ThreadContextPickerDelegate {
75 thread_store: WeakEntity<ThreadStore>,
76 text_thread_store: WeakEntity<TextThreadStore>,
77 context_picker: WeakEntity<ContextPicker>,
78 context_store: WeakEntity<context_store::ContextStore>,
79 matches: Vec<ThreadContextEntry>,
80 selected_index: usize,
81}
82
83impl ThreadContextPickerDelegate {
84 pub fn new(
85 thread_store: WeakEntity<ThreadStore>,
86 text_thread_store: WeakEntity<TextThreadStore>,
87 context_picker: WeakEntity<ContextPicker>,
88 context_store: WeakEntity<context_store::ContextStore>,
89 ) -> Self {
90 ThreadContextPickerDelegate {
91 thread_store,
92 context_picker,
93 context_store,
94 text_thread_store,
95 matches: Vec::new(),
96 selected_index: 0,
97 }
98 }
99}
100
101impl PickerDelegate for ThreadContextPickerDelegate {
102 type ListItem = ListItem;
103
104 fn match_count(&self) -> usize {
105 self.matches.len()
106 }
107
108 fn selected_index(&self) -> usize {
109 self.selected_index
110 }
111
112 fn set_selected_index(
113 &mut self,
114 ix: usize,
115 _window: &mut Window,
116 _cx: &mut Context<Picker<Self>>,
117 ) {
118 self.selected_index = ix;
119 }
120
121 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
122 "Search threads…".into()
123 }
124
125 fn update_matches(
126 &mut self,
127 query: String,
128 window: &mut Window,
129 cx: &mut Context<Picker<Self>>,
130 ) -> Task<()> {
131 let Some((thread_store, text_thread_context_store)) = self
132 .thread_store
133 .upgrade()
134 .zip(self.text_thread_store.upgrade())
135 else {
136 return Task::ready(());
137 };
138
139 let search_task = search_threads(
140 query,
141 Arc::new(AtomicBool::default()),
142 thread_store,
143 text_thread_context_store,
144 cx,
145 );
146 cx.spawn_in(window, async move |this, cx| {
147 let matches = search_task.await;
148 this.update(cx, |this, cx| {
149 this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
150 this.delegate.selected_index = 0;
151 cx.notify();
152 })
153 .ok();
154 })
155 }
156
157 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
158 let Some(entry) = self.matches.get(self.selected_index) else {
159 return;
160 };
161
162 match entry {
163 ThreadContextEntry::Thread { id, .. } => {
164 let Some(thread_store) = self.thread_store.upgrade() else {
165 return;
166 };
167 let open_thread_task =
168 thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
169
170 cx.spawn(async move |this, cx| {
171 let thread = open_thread_task.await?;
172 this.update(cx, |this, cx| {
173 this.delegate
174 .context_store
175 .update(cx, |context_store, cx| {
176 context_store.add_thread(thread, true, cx)
177 })
178 .ok();
179 })
180 })
181 .detach_and_log_err(cx);
182 }
183 ThreadContextEntry::Context { path, .. } => {
184 let Some(text_thread_store) = self.text_thread_store.upgrade() else {
185 return;
186 };
187 let task = text_thread_store
188 .update(cx, |this, cx| this.open_local_context(path.clone(), cx));
189
190 cx.spawn(async move |this, cx| {
191 let thread = task.await?;
192 this.update(cx, |this, cx| {
193 this.delegate
194 .context_store
195 .update(cx, |context_store, cx| {
196 context_store.add_text_thread(thread, true, cx)
197 })
198 .ok();
199 })
200 })
201 .detach_and_log_err(cx);
202 }
203 }
204 }
205
206 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
207 self.context_picker
208 .update(cx, |_, cx| {
209 cx.emit(DismissEvent);
210 })
211 .ok();
212 }
213
214 fn render_match(
215 &self,
216 ix: usize,
217 selected: bool,
218 _window: &mut Window,
219 cx: &mut Context<Picker<Self>>,
220 ) -> Option<Self::ListItem> {
221 let thread = &self.matches[ix];
222
223 Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
224 render_thread_context_entry(thread, self.context_store.clone(), cx),
225 ))
226 }
227}
228
229pub fn render_thread_context_entry(
230 entry: &ThreadContextEntry,
231 context_store: WeakEntity<ContextStore>,
232 cx: &mut App,
233) -> Div {
234 let is_added = match entry {
235 ThreadContextEntry::Thread { id, .. } => context_store
236 .upgrade()
237 .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(&id)),
238 ThreadContextEntry::Context { path, .. } => {
239 context_store.upgrade().map_or(false, |ctx_store| {
240 ctx_store.read(cx).includes_text_thread(path)
241 })
242 }
243 };
244
245 h_flex()
246 .gap_1p5()
247 .w_full()
248 .justify_between()
249 .child(
250 h_flex()
251 .gap_1p5()
252 .max_w_72()
253 .child(
254 Icon::new(IconName::MessageBubbles)
255 .size(IconSize::XSmall)
256 .color(Color::Muted),
257 )
258 .child(Label::new(entry.title().clone()).truncate()),
259 )
260 .when(is_added, |el| {
261 el.child(
262 h_flex()
263 .gap_1()
264 .child(
265 Icon::new(IconName::Check)
266 .size(IconSize::Small)
267 .color(Color::Success),
268 )
269 .child(Label::new("Added").size(LabelSize::Small)),
270 )
271 })
272}
273
274#[derive(Clone)]
275pub struct ThreadMatch {
276 pub thread: ThreadContextEntry,
277 pub is_recent: bool,
278}
279
280pub fn unordered_thread_entries(
281 thread_store: Entity<ThreadStore>,
282 text_thread_store: Entity<TextThreadStore>,
283 cx: &App,
284) -> impl Iterator<Item = (DateTime<Utc>, ThreadContextEntry)> {
285 let threads = thread_store.read(cx).unordered_threads().map(|thread| {
286 (
287 thread.updated_at,
288 ThreadContextEntry::Thread {
289 id: thread.id.clone(),
290 title: thread.summary.clone(),
291 },
292 )
293 });
294
295 let text_threads = text_thread_store
296 .read(cx)
297 .unordered_contexts()
298 .map(|context| {
299 (
300 context.mtime.to_utc(),
301 ThreadContextEntry::Context {
302 path: context.path.clone(),
303 title: context.title.clone().into(),
304 },
305 )
306 });
307
308 threads.chain(text_threads)
309}
310
311pub(crate) fn search_threads(
312 query: String,
313 cancellation_flag: Arc<AtomicBool>,
314 thread_store: Entity<ThreadStore>,
315 text_thread_store: Entity<TextThreadStore>,
316 cx: &mut App,
317) -> Task<Vec<ThreadMatch>> {
318 let mut threads =
319 unordered_thread_entries(thread_store, text_thread_store, cx).collect::<Vec<_>>();
320 threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at));
321
322 let executor = cx.background_executor().clone();
323 cx.background_spawn(async move {
324 if query.is_empty() {
325 threads
326 .into_iter()
327 .map(|(_, thread)| ThreadMatch {
328 thread,
329 is_recent: false,
330 })
331 .collect()
332 } else {
333 let candidates = threads
334 .iter()
335 .enumerate()
336 .map(|(id, (_, thread))| StringMatchCandidate::new(id, &thread.title()))
337 .collect::<Vec<_>>();
338 let matches = fuzzy::match_strings(
339 &candidates,
340 &query,
341 false,
342 100,
343 &cancellation_flag,
344 executor,
345 )
346 .await;
347
348 matches
349 .into_iter()
350 .map(|mat| ThreadMatch {
351 thread: threads[mat.candidate_id].1.clone(),
352 is_recent: false,
353 })
354 .collect()
355 }
356 })
357}