1use gpui::{
2 AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
3 Subscription, Task, View, WeakModel, WeakView,
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: View<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: Model<Project>, cx: &mut ViewContext<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.view().downgrade(),
31 repository_entries: all_repositories,
32 filtered_repositories,
33 selected_index: 0,
34 };
35
36 let picker =
37 cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
38
39 let _subscriptions = if let Some(git_state) = git_state {
40 vec![cx.subscribe(&git_state, Self::handle_project_git_event)]
41 } else {
42 Vec::new()
43 };
44
45 RepositorySelector {
46 picker,
47 update_matches_task: None,
48 _subscriptions,
49 }
50 }
51
52 fn handle_project_git_event(
53 &mut self,
54 git_state: Model<GitState>,
55 _event: &project::git::Event,
56 cx: &mut ViewContext<Self>,
57 ) {
58 // TODO handle events individually
59 let task = self.picker.update(cx, |this, cx| {
60 let query = this.query(cx);
61 this.delegate.repository_entries = git_state.read(cx).all_repositories();
62 this.delegate.update_matches(query, cx)
63 });
64 self.update_matches_task = Some(task);
65 }
66}
67
68impl EventEmitter<DismissEvent> for RepositorySelector {}
69
70impl FocusableView for RepositorySelector {
71 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
72 self.picker.focus_handle(cx)
73 }
74}
75
76impl Render for RepositorySelector {
77 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
78 self.picker.clone()
79 }
80}
81
82#[derive(IntoElement)]
83pub struct RepositorySelectorPopoverMenu<T>
84where
85 T: PopoverTrigger,
86{
87 repository_selector: View<RepositorySelector>,
88 trigger: T,
89 handle: Option<PopoverMenuHandle<RepositorySelector>>,
90}
91
92impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
93 pub fn new(repository_selector: View<RepositorySelector>, trigger: T) -> Self {
94 Self {
95 repository_selector,
96 trigger,
97 handle: None,
98 }
99 }
100
101 pub fn with_handle(mut self, handle: PopoverMenuHandle<RepositorySelector>) -> Self {
102 self.handle = Some(handle);
103 self
104 }
105}
106
107impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
108 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
109 let repository_selector = self.repository_selector.clone();
110
111 PopoverMenu::new("repository-switcher")
112 .menu(move |_cx| Some(repository_selector.clone()))
113 .trigger(self.trigger)
114 .attach(gpui::Corner::BottomLeft)
115 .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
116 }
117}
118
119pub struct RepositorySelectorDelegate {
120 project: WeakModel<Project>,
121 repository_selector: WeakView<RepositorySelector>,
122 repository_entries: Vec<RepositoryHandle>,
123 filtered_repositories: Vec<RepositoryHandle>,
124 selected_index: usize,
125}
126
127impl RepositorySelectorDelegate {
128 pub fn update_repository_entries(&mut self, all_repositories: Vec<RepositoryHandle>) {
129 self.repository_entries = all_repositories.clone();
130 self.filtered_repositories = all_repositories;
131 self.selected_index = 0;
132 }
133}
134
135impl PickerDelegate for RepositorySelectorDelegate {
136 type ListItem = ListItem;
137
138 fn match_count(&self) -> usize {
139 self.filtered_repositories.len()
140 }
141
142 fn selected_index(&self) -> usize {
143 self.selected_index
144 }
145
146 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
147 self.selected_index = ix.min(self.filtered_repositories.len().saturating_sub(1));
148 cx.notify();
149 }
150
151 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
152 "Select a repository...".into()
153 }
154
155 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
156 let all_repositories = self.repository_entries.clone();
157
158 cx.spawn(|this, mut cx| async move {
159 let filtered_repositories = cx
160 .background_executor()
161 .spawn(async move {
162 if query.is_empty() {
163 all_repositories
164 } else {
165 all_repositories
166 .into_iter()
167 .filter(|_repo_info| {
168 // TODO: Implement repository filtering logic
169 true
170 })
171 .collect()
172 }
173 })
174 .await;
175
176 this.update(&mut cx, |this, cx| {
177 this.delegate.filtered_repositories = filtered_repositories;
178 this.delegate.set_selected_index(0, cx);
179 cx.notify();
180 })
181 .ok();
182 })
183 }
184
185 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
186 let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
187 return;
188 };
189 selected_repo.activate(cx);
190 self.dismissed(cx);
191 }
192
193 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
194 self.repository_selector
195 .update(cx, |_this, cx| cx.emit(DismissEvent))
196 .ok();
197 }
198
199 fn render_header(&self, _cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
200 // TODO: Implement header rendering if needed
201 None
202 }
203
204 fn render_match(
205 &self,
206 ix: usize,
207 selected: bool,
208 cx: &mut ViewContext<Picker<Self>>,
209 ) -> Option<Self::ListItem> {
210 let project = self.project.upgrade()?;
211 let repo_info = self.filtered_repositories.get(ix)?;
212 let display_name = repo_info.display_name(project.read(cx), cx);
213 // TODO: Implement repository item rendering
214 Some(
215 ListItem::new(ix)
216 .inset(true)
217 .spacing(ListItemSpacing::Sparse)
218 .toggle_state(selected)
219 .child(Label::new(display_name)),
220 )
221 }
222
223 fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
224 // TODO: Implement footer rendering if needed
225 Some(
226 div()
227 .text_ui_sm(cx)
228 .child("Temporary location for repo selector")
229 .into_any_element(),
230 )
231 }
232}