1use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
2use itertools::Itertools;
3use picker::{Picker, PickerDelegate, PickerEditorPosition};
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 let mut repos: Vec<_> = git_store.repositories().values().cloned().collect();
40
41 repos.sort_by_key(|a| a.read(_cx).display_name());
42
43 repos
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::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()
113 .key_context("GitRepositorySelector")
114 .w(self.width)
115 .child(self.picker.clone())
116 }
117}
118
119impl ModalView for RepositorySelector {}
120
121pub struct RepositorySelectorDelegate {
122 repository_selector: WeakEntity<RepositorySelector>,
123 repository_entries: Vec<Entity<Repository>>,
124 filtered_repositories: Vec<Entity<Repository>>,
125 selected_index: usize,
126}
127
128impl RepositorySelectorDelegate {
129 pub fn update_repository_entries(&mut self, all_repositories: Vec<Entity<Repository>>) {
130 self.repository_entries = all_repositories.clone();
131 self.filtered_repositories = all_repositories;
132 self.selected_index = 0;
133 }
134}
135
136impl PickerDelegate for RepositorySelectorDelegate {
137 type ListItem = ListItem;
138
139 fn match_count(&self) -> usize {
140 self.filtered_repositories.len()
141 }
142
143 fn selected_index(&self) -> usize {
144 self.selected_index
145 }
146
147 fn set_selected_index(
148 &mut self,
149 ix: usize,
150 _window: &mut Window,
151 cx: &mut Context<Picker<Self>>,
152 ) {
153 self.selected_index = ix.min(self.filtered_repositories.len().saturating_sub(1));
154 cx.notify();
155 }
156
157 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
158 "Select a repository...".into()
159 }
160
161 fn editor_position(&self) -> PickerEditorPosition {
162 PickerEditorPosition::End
163 }
164
165 fn update_matches(
166 &mut self,
167 query: String,
168 window: &mut Window,
169 cx: &mut Context<Picker<Self>>,
170 ) -> Task<()> {
171 let all_repositories = self.repository_entries.clone();
172
173 let repo_names: Vec<(Entity<Repository>, String)> = all_repositories
174 .iter()
175 .map(|repo| (repo.clone(), repo.read(cx).display_name().to_lowercase()))
176 .collect();
177
178 cx.spawn_in(window, async move |this, cx| {
179 let filtered_repositories = cx
180 .background_spawn(async move {
181 if query.is_empty() {
182 all_repositories
183 } else {
184 let query_lower = query.to_lowercase();
185 repo_names
186 .into_iter()
187 .filter(|(_, display_name)| display_name.contains(&query_lower))
188 .map(|(repo, _)| repo)
189 .collect()
190 }
191 })
192 .await;
193
194 this.update_in(cx, |this, window, cx| {
195 let mut sorted_repositories = filtered_repositories;
196 sorted_repositories.sort_by_key(|a| a.read(cx).display_name());
197 this.delegate.filtered_repositories = sorted_repositories;
198 this.delegate.set_selected_index(0, window, cx);
199 cx.notify();
200 })
201 .ok();
202 })
203 }
204
205 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
206 let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
207 return;
208 };
209 selected_repo.update(cx, |selected_repo, cx| {
210 selected_repo.set_as_active_repository(cx)
211 });
212 self.dismissed(window, cx);
213 }
214
215 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
216 self.repository_selector
217 .update(cx, |_this, cx| cx.emit(DismissEvent))
218 .ok();
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 repo_info = self.filtered_repositories.get(ix)?;
229 let display_name = repo_info.read(cx).display_name();
230 Some(
231 ListItem::new(ix)
232 .inset(true)
233 .spacing(ListItemSpacing::Sparse)
234 .toggle_state(selected)
235 .child(Label::new(display_name)),
236 )
237 }
238}