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()
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 update_matches(
162 &mut self,
163 query: String,
164 window: &mut Window,
165 cx: &mut Context<Picker<Self>>,
166 ) -> Task<()> {
167 let all_repositories = self.repository_entries.clone();
168
169 cx.spawn_in(window, async move |this, cx| {
170 let filtered_repositories = cx
171 .background_spawn(async move {
172 if query.is_empty() {
173 all_repositories
174 } else {
175 all_repositories
176 .into_iter()
177 .filter(|_repo_info| {
178 // TODO: Implement repository filtering logic
179 true
180 })
181 .collect()
182 }
183 })
184 .await;
185
186 this.update_in(cx, |this, window, cx| {
187 this.delegate.filtered_repositories = filtered_repositories;
188 this.delegate.set_selected_index(0, window, cx);
189 cx.notify();
190 })
191 .ok();
192 })
193 }
194
195 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
196 let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
197 return;
198 };
199 selected_repo.update(cx, |selected_repo, cx| {
200 selected_repo.set_as_active_repository(cx)
201 });
202 self.dismissed(window, cx);
203 }
204
205 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
206 self.repository_selector
207 .update(cx, |_this, cx| cx.emit(DismissEvent))
208 .ok();
209 }
210
211 fn render_match(
212 &self,
213 ix: usize,
214 selected: bool,
215 _window: &mut Window,
216 cx: &mut Context<Picker<Self>>,
217 ) -> Option<Self::ListItem> {
218 let repo_info = self.filtered_repositories.get(ix)?;
219 let display_name = repo_info.read(cx).display_name();
220 Some(
221 ListItem::new(ix)
222 .inset(true)
223 .spacing(ListItemSpacing::Sparse)
224 .toggle_state(selected)
225 .child(Label::new(display_name)),
226 )
227 }
228}