context_history.rs

  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}