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