Files
oak-gpui/crates/git_ui/src/repository_selector.rs
T
6924720b35 Move repository state RPC handlers to the GitStore (#27391)
This is another in the series of PRs to make the GitStore own all
repository state and enable better concurrency control for git
repository scans.

After this PR, the `RepositoryEntry`s stored in worktree snapshots are
used only as a staging ground for local GitStores to pull from after
git-related events; non-local worktrees don't store them at all,
although this is not reflected in the types. GitTraversal and other
places that need information about repositories get it from the
GitStore. The GitStore also takes over handling of the new
UpdateRepository and RemoveRepository messages. However, repositories
are still discovered and scanned on a per-worktree basis, and we're
still identifying them by the (worktree-specific) project entry ID of
their working directory.

- [x] Remove WorkDirectory from RepositoryEntry
- [x] Remove worktree IDs from repository-related RPC messages
- [x] Handle UpdateRepository and RemoveRepository RPCs from the
GitStore

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-26 18:23:44 -04:00

237 lines
7.1 KiB
Rust

use gpui::{
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
};
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{git_store::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<Workspace>,
) {
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<Picker<RepositorySelectorDelegate>>,
}
impl RepositorySelector {
pub fn new(
project_handle: Entity<Project>,
width: Rems,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let git_store = project_handle.read(cx).git_store().clone();
let repository_entries = git_store.update(cx, |git_store, _cx| {
git_store
.repositories()
.values()
.cloned()
.collect::<Vec<_>>()
});
let filtered_repositories = repository_entries.clone();
let widest_item_ix = repository_entries.iter().position_max_by(|a, b| {
a.read(cx)
.display_name()
.len()
.cmp(&b.read(cx).display_name().len())
});
let delegate = RepositorySelectorDelegate {
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<Entity<Repository>> {
// 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::<Vec<&Entity<Repository>>>();
//
// 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<DismissEvent> 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<Self>) -> impl IntoElement {
div().w(self.width).child(self.picker.clone())
}
}
impl ModalView for RepositorySelector {}
pub struct RepositorySelectorDelegate {
repository_selector: WeakEntity<RepositorySelector>,
repository_entries: Vec<Entity<Repository>>,
filtered_repositories: Vec<Entity<Repository>>,
selected_index: usize,
}
impl RepositorySelectorDelegate {
pub fn update_repository_entries(&mut self, all_repositories: Vec<Entity<Repository>>) {
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<Picker<Self>>,
) {
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<str> {
"Select a repository...".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> 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<Picker<Self>>) {
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<Picker<Self>>) {
self.repository_selector
.update(cx, |_this, cx| cx.emit(DismissEvent))
.ok();
}
fn render_header(
&self,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
// TODO: Implement header rendering if needed
None
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let repo_info = self.filtered_repositories.get(ix)?;
let display_name = repo_info.read(cx).display_name();
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(Label::new(display_name)),
)
}
}