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