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}