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}