repository_selector.rs

  1use gpui::{
  2    AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
  3    Task, WeakEntity,
  4};
  5use itertools::Itertools;
  6use picker::{Picker, PickerDelegate};
  7use project::{
  8    git::{GitStore, Repository},
  9    Project,
 10};
 11use std::sync::Arc;
 12use ui::{prelude::*, ListItem, ListItemSpacing};
 13
 14pub struct RepositorySelector {
 15    picker: Entity<Picker<RepositorySelectorDelegate>>,
 16    /// The task used to update the picker's matches when there is a change to
 17    /// the repository list.
 18    update_matches_task: Option<Task<()>>,
 19    _subscriptions: Vec<Subscription>,
 20}
 21
 22impl RepositorySelector {
 23    pub fn new(
 24        project_handle: Entity<Project>,
 25        window: &mut Window,
 26        cx: &mut Context<Self>,
 27    ) -> Self {
 28        let project = project_handle.read(cx);
 29        let git_store = project.git_store().clone();
 30        let all_repositories = git_store.read(cx).all_repositories();
 31        let filtered_repositories = all_repositories.clone();
 32
 33        let widest_item_ix = all_repositories.iter().position_max_by(|a, b| {
 34            a.read(cx)
 35                .display_name(project, cx)
 36                .len()
 37                .cmp(&b.read(cx).display_name(project, cx).len())
 38        });
 39
 40        let delegate = RepositorySelectorDelegate {
 41            project: project_handle.downgrade(),
 42            repository_selector: cx.entity().downgrade(),
 43            repository_entries: all_repositories.clone(),
 44            filtered_repositories,
 45            selected_index: 0,
 46        };
 47
 48        let picker = cx.new(|cx| {
 49            Picker::nonsearchable_uniform_list(delegate, window, cx)
 50                .widest_item(widest_item_ix)
 51                .max_height(Some(rems(20.).into()))
 52        });
 53
 54        let _subscriptions =
 55            vec![cx.subscribe_in(&git_store, window, Self::handle_project_git_event)];
 56
 57        RepositorySelector {
 58            picker,
 59            update_matches_task: None,
 60            _subscriptions,
 61        }
 62    }
 63
 64    fn handle_project_git_event(
 65        &mut self,
 66        git_store: &Entity<GitStore>,
 67        _event: &project::git::GitEvent,
 68        window: &mut Window,
 69        cx: &mut Context<Self>,
 70    ) {
 71        // TODO handle events individually
 72        let task = self.picker.update(cx, |this, cx| {
 73            let query = this.query(cx);
 74            this.delegate.repository_entries = git_store.read(cx).all_repositories();
 75            this.delegate.update_matches(query, window, cx)
 76        });
 77        self.update_matches_task = Some(task);
 78    }
 79}
 80
 81impl EventEmitter<DismissEvent> for RepositorySelector {}
 82
 83impl Focusable for RepositorySelector {
 84    fn focus_handle(&self, cx: &App) -> FocusHandle {
 85        self.picker.focus_handle(cx)
 86    }
 87}
 88
 89impl Render for RepositorySelector {
 90    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 91        self.picker.clone()
 92    }
 93}
 94
 95pub struct RepositorySelectorDelegate {
 96    project: WeakEntity<Project>,
 97    repository_selector: WeakEntity<RepositorySelector>,
 98    repository_entries: Vec<Entity<Repository>>,
 99    filtered_repositories: Vec<Entity<Repository>>,
100    selected_index: usize,
101}
102
103impl RepositorySelectorDelegate {
104    pub fn update_repository_entries(&mut self, all_repositories: Vec<Entity<Repository>>) {
105        self.repository_entries = all_repositories.clone();
106        self.filtered_repositories = all_repositories;
107        self.selected_index = 0;
108    }
109}
110
111impl PickerDelegate for RepositorySelectorDelegate {
112    type ListItem = ListItem;
113
114    fn match_count(&self) -> usize {
115        self.filtered_repositories.len()
116    }
117
118    fn selected_index(&self) -> usize {
119        self.selected_index
120    }
121
122    fn set_selected_index(
123        &mut self,
124        ix: usize,
125        _window: &mut Window,
126        cx: &mut Context<Picker<Self>>,
127    ) {
128        self.selected_index = ix.min(self.filtered_repositories.len().saturating_sub(1));
129        cx.notify();
130    }
131
132    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
133        "Select a repository...".into()
134    }
135
136    fn update_matches(
137        &mut self,
138        query: String,
139        window: &mut Window,
140        cx: &mut Context<Picker<Self>>,
141    ) -> Task<()> {
142        let all_repositories = self.repository_entries.clone();
143
144        cx.spawn_in(window, |this, mut cx| async move {
145            let filtered_repositories = cx
146                .background_spawn(async move {
147                    if query.is_empty() {
148                        all_repositories
149                    } else {
150                        all_repositories
151                            .into_iter()
152                            .filter(|_repo_info| {
153                                // TODO: Implement repository filtering logic
154                                true
155                            })
156                            .collect()
157                    }
158                })
159                .await;
160
161            this.update_in(&mut cx, |this, window, cx| {
162                this.delegate.filtered_repositories = filtered_repositories;
163                this.delegate.set_selected_index(0, window, cx);
164                cx.notify();
165            })
166            .ok();
167        })
168    }
169
170    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
171        let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
172            return;
173        };
174        selected_repo.update(cx, |selected_repo, cx| selected_repo.activate(cx));
175        self.dismissed(window, cx);
176    }
177
178    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
179        self.repository_selector
180            .update(cx, |_this, cx| cx.emit(DismissEvent))
181            .ok();
182    }
183
184    fn render_header(
185        &self,
186        _window: &mut Window,
187        _cx: &mut Context<Picker<Self>>,
188    ) -> Option<AnyElement> {
189        // TODO: Implement header rendering if needed
190        None
191    }
192
193    fn render_match(
194        &self,
195        ix: usize,
196        selected: bool,
197        _window: &mut Window,
198        cx: &mut Context<Picker<Self>>,
199    ) -> Option<Self::ListItem> {
200        let project = self.project.upgrade()?;
201        let repo_info = self.filtered_repositories.get(ix)?;
202        let display_name = repo_info.read(cx).display_name(project.read(cx), cx);
203        Some(
204            ListItem::new(ix)
205                .inset(true)
206                .spacing(ListItemSpacing::Sparse)
207                .toggle_state(selected)
208                .child(Label::new(display_name)),
209        )
210    }
211}