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