use gpui::{ AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, }; use itertools::Itertools; use picker::{Picker, PickerDelegate}; use project::{ git_store::{GitStore, Repository}, Project, }; use std::sync::Arc; use ui::{prelude::*, ListItem, ListItemSpacing}; use workspace::{ModalView, Workspace}; pub fn register(workspace: &mut Workspace) { workspace.register_action(open); } pub fn open( workspace: &mut Workspace, _: &zed_actions::git::SelectRepo, window: &mut Window, cx: &mut Context, ) { let project = workspace.project().clone(); workspace.toggle_modal(window, cx, |window, cx| { RepositorySelector::new(project, rems(34.), window, cx) }) } pub struct RepositorySelector { width: Rems, picker: Entity>, } impl RepositorySelector { pub fn new( project_handle: Entity, width: Rems, window: &mut Window, cx: &mut Context, ) -> Self { let git_store = project_handle.read(cx).git_store().clone(); let repository_entries = git_store.update(cx, |git_store, cx| { filtered_repository_entries(git_store, cx) }); let project = project_handle.read(cx); let filtered_repositories = repository_entries.clone(); let widest_item_ix = repository_entries.iter().position_max_by(|a, b| { a.read(cx) .display_name(project, cx) .len() .cmp(&b.read(cx).display_name(project, cx).len()) }); let delegate = RepositorySelectorDelegate { project: project_handle.downgrade(), repository_selector: cx.entity().downgrade(), repository_entries, filtered_repositories, selected_index: 0, }; let picker = cx.new(|cx| { Picker::nonsearchable_uniform_list(delegate, window, cx) .widest_item(widest_item_ix) .max_height(Some(rems(20.).into())) }); RepositorySelector { picker, width } } } pub(crate) fn filtered_repository_entries( git_store: &GitStore, cx: &App, ) -> Vec> { let repositories = git_store .repositories() .values() .sorted_by_key(|repo| { let repo = repo.read(cx); ( repo.dot_git_abs_path.clone(), repo.worktree_abs_path.clone(), ) }) .collect::>>(); repositories .chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path) .flat_map(|chunk| { let has_non_single_file_worktree = chunk .iter() .any(|repo| !repo.read(cx).is_from_single_file_worktree); chunk.iter().filter(move |repo| { // Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree. !repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree }) }) .map(|&repo| repo.clone()) .collect() } impl EventEmitter for RepositorySelector {} impl Focusable for RepositorySelector { fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl Render for RepositorySelector { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().w(self.width).child(self.picker.clone()) } } impl ModalView for RepositorySelector {} pub struct RepositorySelectorDelegate { project: WeakEntity, repository_selector: WeakEntity, repository_entries: Vec>, filtered_repositories: Vec>, selected_index: usize, } impl RepositorySelectorDelegate { pub fn update_repository_entries(&mut self, all_repositories: Vec>) { self.repository_entries = all_repositories.clone(); self.filtered_repositories = all_repositories; self.selected_index = 0; } } impl PickerDelegate for RepositorySelectorDelegate { type ListItem = ListItem; fn match_count(&self) -> usize { self.filtered_repositories.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index( &mut self, ix: usize, _window: &mut Window, cx: &mut Context>, ) { self.selected_index = ix.min(self.filtered_repositories.len().saturating_sub(1)); cx.notify(); } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Select a repository...".into() } fn update_matches( &mut self, query: String, window: &mut Window, cx: &mut Context>, ) -> Task<()> { let all_repositories = self.repository_entries.clone(); cx.spawn_in(window, async move |this, cx| { let filtered_repositories = cx .background_spawn(async move { if query.is_empty() { all_repositories } else { all_repositories .into_iter() .filter(|_repo_info| { // TODO: Implement repository filtering logic true }) .collect() } }) .await; this.update_in(cx, |this, window, cx| { this.delegate.filtered_repositories = filtered_repositories; this.delegate.set_selected_index(0, window, cx); cx.notify(); }) .ok(); }) } fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else { return; }; selected_repo.update(cx, |selected_repo, cx| { selected_repo.set_as_active_repository(cx) }); self.dismissed(window, cx); } fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { self.repository_selector .update(cx, |_this, cx| cx.emit(DismissEvent)) .ok(); } fn render_header( &self, _window: &mut Window, _cx: &mut Context>, ) -> Option { // TODO: Implement header rendering if needed None } fn render_match( &self, ix: usize, selected: bool, _window: &mut Window, cx: &mut Context>, ) -> Option { let project = self.project.upgrade()?; let repo_info = self.filtered_repositories.get(ix)?; let display_name = repo_info.read(cx).display_name(project.read(cx), cx); Some( ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .child(Label::new(display_name)), ) } }