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}