repository_selector.rs

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