repository_selector.rs

  1use gpui::{
  2    AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
  3    Subscription, Task, View, WeakModel, WeakView,
  4};
  5use picker::{Picker, PickerDelegate};
  6use project::{
  7    git::{GitState, RepositoryHandle},
  8    Project,
  9};
 10use std::sync::Arc;
 11use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
 12
 13pub struct RepositorySelector {
 14    picker: View<Picker<RepositorySelectorDelegate>>,
 15    /// The task used to update the picker's matches when there is a change to
 16    /// the repository list.
 17    update_matches_task: Option<Task<()>>,
 18    _subscriptions: Vec<Subscription>,
 19}
 20
 21impl RepositorySelector {
 22    pub fn new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
 23        let git_state = project.read(cx).git_state().cloned();
 24        let all_repositories = git_state
 25            .as_ref()
 26            .map_or(vec![], |git_state| git_state.read(cx).all_repositories());
 27        let filtered_repositories = all_repositories.clone();
 28        let delegate = RepositorySelectorDelegate {
 29            project: project.downgrade(),
 30            repository_selector: cx.view().downgrade(),
 31            repository_entries: all_repositories,
 32            filtered_repositories,
 33            selected_index: 0,
 34        };
 35
 36        let picker =
 37            cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
 38
 39        let _subscriptions = if let Some(git_state) = git_state {
 40            vec![cx.subscribe(&git_state, Self::handle_project_git_event)]
 41        } else {
 42            Vec::new()
 43        };
 44
 45        RepositorySelector {
 46            picker,
 47            update_matches_task: None,
 48            _subscriptions,
 49        }
 50    }
 51
 52    fn handle_project_git_event(
 53        &mut self,
 54        git_state: Model<GitState>,
 55        _event: &project::git::Event,
 56        cx: &mut ViewContext<Self>,
 57    ) {
 58        // TODO handle events individually
 59        let task = self.picker.update(cx, |this, cx| {
 60            let query = this.query(cx);
 61            this.delegate.repository_entries = git_state.read(cx).all_repositories();
 62            this.delegate.update_matches(query, cx)
 63        });
 64        self.update_matches_task = Some(task);
 65    }
 66}
 67
 68impl EventEmitter<DismissEvent> for RepositorySelector {}
 69
 70impl FocusableView for RepositorySelector {
 71    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 72        self.picker.focus_handle(cx)
 73    }
 74}
 75
 76impl Render for RepositorySelector {
 77    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
 78        self.picker.clone()
 79    }
 80}
 81
 82#[derive(IntoElement)]
 83pub struct RepositorySelectorPopoverMenu<T>
 84where
 85    T: PopoverTrigger,
 86{
 87    repository_selector: View<RepositorySelector>,
 88    trigger: T,
 89    handle: Option<PopoverMenuHandle<RepositorySelector>>,
 90}
 91
 92impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
 93    pub fn new(repository_selector: View<RepositorySelector>, trigger: T) -> Self {
 94        Self {
 95            repository_selector,
 96            trigger,
 97            handle: None,
 98        }
 99    }
100
101    pub fn with_handle(mut self, handle: PopoverMenuHandle<RepositorySelector>) -> Self {
102        self.handle = Some(handle);
103        self
104    }
105}
106
107impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
108    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
109        let repository_selector = self.repository_selector.clone();
110
111        PopoverMenu::new("repository-switcher")
112            .menu(move |_cx| Some(repository_selector.clone()))
113            .trigger(self.trigger)
114            .attach(gpui::Corner::BottomLeft)
115            .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
116    }
117}
118
119pub struct RepositorySelectorDelegate {
120    project: WeakModel<Project>,
121    repository_selector: WeakView<RepositorySelector>,
122    repository_entries: Vec<RepositoryHandle>,
123    filtered_repositories: Vec<RepositoryHandle>,
124    selected_index: usize,
125}
126
127impl RepositorySelectorDelegate {
128    pub fn update_repository_entries(&mut self, all_repositories: Vec<RepositoryHandle>) {
129        self.repository_entries = all_repositories.clone();
130        self.filtered_repositories = all_repositories;
131        self.selected_index = 0;
132    }
133}
134
135impl PickerDelegate for RepositorySelectorDelegate {
136    type ListItem = ListItem;
137
138    fn match_count(&self) -> usize {
139        self.filtered_repositories.len()
140    }
141
142    fn selected_index(&self) -> usize {
143        self.selected_index
144    }
145
146    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
147        self.selected_index = ix.min(self.filtered_repositories.len().saturating_sub(1));
148        cx.notify();
149    }
150
151    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
152        "Select a repository...".into()
153    }
154
155    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
156        let all_repositories = self.repository_entries.clone();
157
158        cx.spawn(|this, mut cx| async move {
159            let filtered_repositories = cx
160                .background_executor()
161                .spawn(async move {
162                    if query.is_empty() {
163                        all_repositories
164                    } else {
165                        all_repositories
166                            .into_iter()
167                            .filter(|_repo_info| {
168                                // TODO: Implement repository filtering logic
169                                true
170                            })
171                            .collect()
172                    }
173                })
174                .await;
175
176            this.update(&mut cx, |this, cx| {
177                this.delegate.filtered_repositories = filtered_repositories;
178                this.delegate.set_selected_index(0, cx);
179                cx.notify();
180            })
181            .ok();
182        })
183    }
184
185    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
186        let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
187            return;
188        };
189        selected_repo.activate(cx);
190        self.dismissed(cx);
191    }
192
193    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
194        self.repository_selector
195            .update(cx, |_this, cx| cx.emit(DismissEvent))
196            .ok();
197    }
198
199    fn render_header(&self, _cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
200        // TODO: Implement header rendering if needed
201        None
202    }
203
204    fn render_match(
205        &self,
206        ix: usize,
207        selected: bool,
208        cx: &mut ViewContext<Picker<Self>>,
209    ) -> Option<Self::ListItem> {
210        let project = self.project.upgrade()?;
211        let repo_info = self.filtered_repositories.get(ix)?;
212        let display_name = repo_info.display_name(project.read(cx), cx);
213        // TODO: Implement repository item rendering
214        Some(
215            ListItem::new(ix)
216                .inset(true)
217                .spacing(ListItemSpacing::Sparse)
218                .toggle_state(selected)
219                .child(Label::new(display_name)),
220        )
221    }
222
223    fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
224        // TODO: Implement footer rendering if needed
225        Some(
226            div()
227                .text_ui_sm(cx)
228                .child("Temporary location for repo selector")
229                .into_any_element(),
230        )
231    }
232}