1use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
2use itertools::Itertools;
3use picker::{Picker, PickerDelegate};
4use project::{Project, git_store::Repository};
5use std::sync::Arc;
6use ui::{ListItem, ListItemSpacing, prelude::*};
7use workspace::{ModalView, Workspace};
8
9pub fn register(workspace: &mut Workspace) {
10 workspace.register_action(open);
11}
12
13pub fn open(
14 workspace: &mut Workspace,
15 _: &zed_actions::git::SelectRepo,
16 window: &mut Window,
17 cx: &mut Context<Workspace>,
18) {
19 let project = workspace.project().clone();
20 workspace.toggle_modal(window, cx, |window, cx| {
21 RepositorySelector::new(project, rems(34.), window, cx)
22 })
23}
24
25pub struct RepositorySelector {
26 width: Rems,
27 picker: Entity<Picker<RepositorySelectorDelegate>>,
28}
29
30impl RepositorySelector {
31 pub fn new(
32 project_handle: Entity<Project>,
33 width: Rems,
34 window: &mut Window,
35 cx: &mut Context<Self>,
36 ) -> Self {
37 let git_store = project_handle.read(cx).git_store().clone();
38 let repository_entries = git_store.update(cx, |git_store, _cx| {
39 git_store
40 .repositories()
41 .values()
42 .cloned()
43 .collect::<Vec<_>>()
44 });
45 let filtered_repositories = repository_entries.clone();
46
47 let widest_item_ix = repository_entries.iter().position_max_by(|a, b| {
48 a.read(cx)
49 .display_name()
50 .len()
51 .cmp(&b.read(cx).display_name().len())
52 });
53
54 let delegate = RepositorySelectorDelegate {
55 repository_selector: cx.entity().downgrade(),
56 repository_entries,
57 filtered_repositories,
58 selected_index: 0,
59 };
60
61 let picker = cx.new(|cx| {
62 Picker::nonsearchable_uniform_list(delegate, window, cx)
63 .widest_item(widest_item_ix)
64 .max_height(Some(rems(20.).into()))
65 });
66
67 RepositorySelector { picker, width }
68 }
69}
70
71//pub(crate) fn filtered_repository_entries(
72// git_store: &GitStore,
73// cx: &App,
74//) -> Vec<Entity<Repository>> {
75// let repositories = git_store
76// .repositories()
77// .values()
78// .sorted_by_key(|repo| {
79// let repo = repo.read(cx);
80// (
81// repo.dot_git_abs_path.clone(),
82// repo.worktree_abs_path.clone(),
83// )
84// })
85// .collect::<Vec<&Entity<Repository>>>();
86//
87// repositories
88// .chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path)
89// .flat_map(|chunk| {
90// let has_non_single_file_worktree = chunk
91// .iter()
92// .any(|repo| !repo.read(cx).is_from_single_file_worktree);
93// chunk.iter().filter(move |repo| {
94// // Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree.
95// !repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree
96// })
97// })
98// .map(|&repo| repo.clone())
99// .collect()
100//}
101
102impl EventEmitter<DismissEvent> for RepositorySelector {}
103
104impl Focusable for RepositorySelector {
105 fn focus_handle(&self, cx: &App) -> FocusHandle {
106 self.picker.focus_handle(cx)
107 }
108}
109
110impl Render for RepositorySelector {
111 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
112 div().w(self.width).child(self.picker.clone())
113 }
114}
115
116impl ModalView for RepositorySelector {}
117
118pub struct RepositorySelectorDelegate {
119 repository_selector: WeakEntity<RepositorySelector>,
120 repository_entries: Vec<Entity<Repository>>,
121 filtered_repositories: Vec<Entity<Repository>>,
122 selected_index: usize,
123}
124
125impl RepositorySelectorDelegate {
126 pub fn update_repository_entries(&mut self, all_repositories: Vec<Entity<Repository>>) {
127 self.repository_entries = all_repositories.clone();
128 self.filtered_repositories = all_repositories;
129 self.selected_index = 0;
130 }
131}
132
133impl PickerDelegate for RepositorySelectorDelegate {
134 type ListItem = ListItem;
135
136 fn match_count(&self) -> usize {
137 self.filtered_repositories.len()
138 }
139
140 fn selected_index(&self) -> usize {
141 self.selected_index
142 }
143
144 fn set_selected_index(
145 &mut self,
146 ix: usize,
147 _window: &mut Window,
148 cx: &mut Context<Picker<Self>>,
149 ) {
150 self.selected_index = ix.min(self.filtered_repositories.len().saturating_sub(1));
151 cx.notify();
152 }
153
154 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
155 "Select a repository...".into()
156 }
157
158 fn update_matches(
159 &mut self,
160 query: String,
161 window: &mut Window,
162 cx: &mut Context<Picker<Self>>,
163 ) -> Task<()> {
164 let all_repositories = self.repository_entries.clone();
165
166 cx.spawn_in(window, async move |this, cx| {
167 let filtered_repositories = cx
168 .background_spawn(async move {
169 if query.is_empty() {
170 all_repositories
171 } else {
172 all_repositories
173 .into_iter()
174 .filter(|_repo_info| {
175 // TODO: Implement repository filtering logic
176 true
177 })
178 .collect()
179 }
180 })
181 .await;
182
183 this.update_in(cx, |this, window, cx| {
184 this.delegate.filtered_repositories = filtered_repositories;
185 this.delegate.set_selected_index(0, window, cx);
186 cx.notify();
187 })
188 .ok();
189 })
190 }
191
192 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
193 let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
194 return;
195 };
196 selected_repo.update(cx, |selected_repo, cx| {
197 selected_repo.set_as_active_repository(cx)
198 });
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_match(
209 &self,
210 ix: usize,
211 selected: bool,
212 _window: &mut Window,
213 cx: &mut Context<Picker<Self>>,
214 ) -> Option<Self::ListItem> {
215 let repo_info = self.filtered_repositories.get(ix)?;
216 let display_name = repo_info.read(cx).display_name();
217 Some(
218 ListItem::new(ix)
219 .inset(true)
220 .spacing(ListItemSpacing::Sparse)
221 .toggle_state(selected)
222 .child(Label::new(display_name)),
223 )
224 }
225}