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