use std::sync::Arc; use gpui::{ AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, View, WeakView, }; use picker::{Picker, PickerDelegate}; use project::Project; use ui::utils::{format_distance_from_now, DateTimeType}; use ui::{prelude::*, Avatar, ListItem, ListItemSpacing}; use workspace::Item; use crate::context_editor::DEFAULT_TAB_TITLE; use crate::{AssistantPanel, ContextStore, RemoteContextMetadata, SavedContextMetadata}; #[derive(Clone)] pub enum ContextMetadata { Remote(RemoteContextMetadata), Saved(SavedContextMetadata), } enum SavedContextPickerEvent { Confirmed(ContextMetadata), } pub struct ContextHistory { picker: View>, _subscriptions: Vec, assistant_panel: WeakView, } impl ContextHistory { pub fn new( project: Model, context_store: Model, assistant_panel: WeakView, cx: &mut ViewContext, ) -> Self { let picker = cx.new_view(|cx| { Picker::uniform_list( SavedContextPickerDelegate::new(project, context_store.clone()), cx, ) .modal(false) .max_height(None) }); let _subscriptions = vec![ cx.observe(&context_store, |this, _, cx| { this.picker.update(cx, |picker, cx| picker.refresh(cx)); }), cx.subscribe(&picker, Self::handle_picker_event), ]; Self { picker, _subscriptions, assistant_panel, } } fn handle_picker_event( &mut self, _: View>, event: &SavedContextPickerEvent, cx: &mut ViewContext, ) { let SavedContextPickerEvent::Confirmed(context) = event; self.assistant_panel .update(cx, |assistant_panel, cx| match context { ContextMetadata::Remote(metadata) => { assistant_panel .open_remote_context(metadata.id.clone(), cx) .detach_and_log_err(cx); } ContextMetadata::Saved(metadata) => { assistant_panel .open_saved_context(metadata.path.clone(), cx) .detach_and_log_err(cx); } }) .ok(); } } impl Render for ContextHistory { fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { div().size_full().child(self.picker.clone()) } } impl FocusableView for ContextHistory { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.picker.focus_handle(cx) } } impl EventEmitter<()> for ContextHistory {} impl Item for ContextHistory { type Event = (); fn tab_content_text(&self, _cx: &WindowContext) -> Option { Some("History".into()) } } struct SavedContextPickerDelegate { store: Model, project: Model, matches: Vec, selected_index: usize, } impl EventEmitter for Picker {} impl SavedContextPickerDelegate { fn new(project: Model, store: Model) -> Self { Self { project, store, matches: Vec::new(), selected_index: 0, } } } impl PickerDelegate for SavedContextPickerDelegate { type ListItem = ListItem; fn match_count(&self) -> usize { self.matches.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { self.selected_index = ix; } fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Search...".into() } fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { let search = self.store.read(cx).search(query, cx); cx.spawn(|this, mut cx| async move { let matches = search.await; this.update(&mut cx, |this, cx| { let host_contexts = this.delegate.store.read(cx).host_contexts(); this.delegate.matches = host_contexts .iter() .cloned() .map(ContextMetadata::Remote) .chain(matches.into_iter().map(ContextMetadata::Saved)) .collect(); this.delegate.selected_index = 0; cx.notify(); }) .ok(); }) } fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { if let Some(metadata) = self.matches.get(self.selected_index) { cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone())); } } fn dismissed(&mut self, _cx: &mut ViewContext>) {} fn render_match( &self, ix: usize, selected: bool, cx: &mut ViewContext>, ) -> Option { let context = self.matches.get(ix)?; let item = match context { ContextMetadata::Remote(context) => { let host_user = self.project.read(cx).host().and_then(|collaborator| { self.project .read(cx) .user_store() .read(cx) .get_cached_user(collaborator.user_id) }); div() .flex() .w_full() .justify_between() .gap_2() .child( h_flex().flex_1().overflow_x_hidden().child( Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into())) .size(LabelSize::Small), ), ) .child( h_flex() .gap_2() .children(if let Some(host_user) = host_user { vec![ Avatar::new(host_user.avatar_uri.clone()).into_any_element(), Label::new(format!("Shared by @{}", host_user.github_login)) .color(Color::Muted) .size(LabelSize::Small) .into_any_element(), ] } else { vec![Label::new("Shared by host") .color(Color::Muted) .size(LabelSize::Small) .into_any_element()] }), ) } ContextMetadata::Saved(context) => div() .flex() .w_full() .justify_between() .gap_2() .child( h_flex() .flex_1() .child(Label::new(context.title.clone()).size(LabelSize::Small)) .overflow_x_hidden(), ) .child( Label::new(format_distance_from_now( DateTimeType::Local(context.mtime), false, true, true, )) .color(Color::Muted) .size(LabelSize::Small), ), }; Some( ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .child(item), ) } }