1use std::sync::Arc;
2
3use gpui::{
4 AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, View, WeakView,
5};
6use picker::{Picker, PickerDelegate};
7use project::Project;
8use ui::utils::{format_distance_from_now, DateTimeType};
9use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
10use workspace::{Item, Workspace};
11
12use crate::{
13 AssistantPanelDelegate, ContextStore, RemoteContextMetadata, SavedContextMetadata,
14 DEFAULT_TAB_TITLE,
15};
16
17#[derive(Clone)]
18pub enum ContextMetadata {
19 Remote(RemoteContextMetadata),
20 Saved(SavedContextMetadata),
21}
22
23enum SavedContextPickerEvent {
24 Confirmed(ContextMetadata),
25}
26
27pub struct ContextHistory {
28 picker: View<Picker<SavedContextPickerDelegate>>,
29 _subscriptions: Vec<Subscription>,
30 workspace: WeakView<Workspace>,
31}
32
33impl ContextHistory {
34 pub fn new(
35 project: Model<Project>,
36 context_store: Model<ContextStore>,
37 workspace: WeakView<Workspace>,
38 cx: &mut ViewContext<Self>,
39 ) -> Self {
40 let picker = cx.new_view(|cx| {
41 Picker::uniform_list(
42 SavedContextPickerDelegate::new(project, context_store.clone()),
43 cx,
44 )
45 .modal(false)
46 .max_height(None)
47 });
48
49 let subscriptions = vec![
50 cx.observe(&context_store, |this, _, cx| {
51 this.picker.update(cx, |picker, cx| picker.refresh(cx));
52 }),
53 cx.subscribe(&picker, Self::handle_picker_event),
54 ];
55
56 Self {
57 picker,
58 _subscriptions: subscriptions,
59 workspace,
60 }
61 }
62
63 fn handle_picker_event(
64 &mut self,
65 _: View<Picker<SavedContextPickerDelegate>>,
66 event: &SavedContextPickerEvent,
67 cx: &mut ViewContext<Self>,
68 ) {
69 let SavedContextPickerEvent::Confirmed(context) = event;
70
71 let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
72 return;
73 };
74
75 self.workspace
76 .update(cx, |workspace, cx| match context {
77 ContextMetadata::Remote(metadata) => {
78 assistant_panel_delegate
79 .open_remote_context(workspace, metadata.id.clone(), cx)
80 .detach_and_log_err(cx);
81 }
82 ContextMetadata::Saved(metadata) => {
83 assistant_panel_delegate
84 .open_saved_context(workspace, metadata.path.clone(), cx)
85 .detach_and_log_err(cx);
86 }
87 })
88 .ok();
89 }
90}
91
92impl Render for ContextHistory {
93 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
94 div().size_full().child(self.picker.clone())
95 }
96}
97
98impl FocusableView for ContextHistory {
99 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
100 self.picker.focus_handle(cx)
101 }
102}
103
104impl EventEmitter<()> for ContextHistory {}
105
106impl Item for ContextHistory {
107 type Event = ();
108
109 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
110 Some("History".into())
111 }
112}
113
114struct SavedContextPickerDelegate {
115 store: Model<ContextStore>,
116 project: Model<Project>,
117 matches: Vec<ContextMetadata>,
118 selected_index: usize,
119}
120
121impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
122
123impl SavedContextPickerDelegate {
124 fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
125 Self {
126 project,
127 store,
128 matches: Vec::new(),
129 selected_index: 0,
130 }
131 }
132}
133
134impl PickerDelegate for SavedContextPickerDelegate {
135 type ListItem = ListItem;
136
137 fn match_count(&self) -> usize {
138 self.matches.len()
139 }
140
141 fn selected_index(&self) -> usize {
142 self.selected_index
143 }
144
145 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
146 self.selected_index = ix;
147 }
148
149 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
150 "Search...".into()
151 }
152
153 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
154 let search = self.store.read(cx).search(query, cx);
155 cx.spawn(|this, mut cx| async move {
156 let matches = search.await;
157 this.update(&mut cx, |this, cx| {
158 let host_contexts = this.delegate.store.read(cx).host_contexts();
159 this.delegate.matches = host_contexts
160 .iter()
161 .cloned()
162 .map(ContextMetadata::Remote)
163 .chain(matches.into_iter().map(ContextMetadata::Saved))
164 .collect();
165 this.delegate.selected_index = 0;
166 cx.notify();
167 })
168 .ok();
169 })
170 }
171
172 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
173 if let Some(metadata) = self.matches.get(self.selected_index) {
174 cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
175 }
176 }
177
178 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
179
180 fn render_match(
181 &self,
182 ix: usize,
183 selected: bool,
184 cx: &mut ViewContext<Picker<Self>>,
185 ) -> Option<Self::ListItem> {
186 let context = self.matches.get(ix)?;
187 let item = match context {
188 ContextMetadata::Remote(context) => {
189 let host_user = self.project.read(cx).host().and_then(|collaborator| {
190 self.project
191 .read(cx)
192 .user_store()
193 .read(cx)
194 .get_cached_user(collaborator.user_id)
195 });
196 div()
197 .flex()
198 .w_full()
199 .justify_between()
200 .gap_2()
201 .child(
202 h_flex().flex_1().overflow_x_hidden().child(
203 Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
204 .size(LabelSize::Small),
205 ),
206 )
207 .child(
208 h_flex()
209 .gap_2()
210 .children(if let Some(host_user) = host_user {
211 vec![
212 Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
213 Label::new(format!("Shared by @{}", host_user.github_login))
214 .color(Color::Muted)
215 .size(LabelSize::Small)
216 .into_any_element(),
217 ]
218 } else {
219 vec![Label::new("Shared by host")
220 .color(Color::Muted)
221 .size(LabelSize::Small)
222 .into_any_element()]
223 }),
224 )
225 }
226 ContextMetadata::Saved(context) => div()
227 .flex()
228 .w_full()
229 .justify_between()
230 .gap_2()
231 .child(
232 h_flex()
233 .flex_1()
234 .child(Label::new(context.title.clone()).size(LabelSize::Small))
235 .overflow_x_hidden(),
236 )
237 .child(
238 Label::new(format_distance_from_now(
239 DateTimeType::Local(context.mtime),
240 false,
241 true,
242 true,
243 ))
244 .color(Color::Muted)
245 .size(LabelSize::Small),
246 ),
247 };
248 Some(
249 ListItem::new(ix)
250 .inset(true)
251 .spacing(ListItemSpacing::Sparse)
252 .toggle_state(selected)
253 .child(item),
254 )
255 }
256}