repository_selector.rs

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