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