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