scan local repo

This commit is contained in:
2026-08-31 07:54:41 +07:00
parent 767638eda2
commit 6fcc945dae
8 changed files with 366 additions and 34 deletions
+113 -20
View File
@@ -1,4 +1,5 @@
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use assets::CustomIconName;
@@ -18,7 +19,7 @@ use gpui_component::button::{Button, ButtonVariants};
use gpui_component::input::InputState;
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use signed_core::Announcement;
use signed_state::{Backend, BackendEvent, Profile, ProfileStore, RepoListStore};
use signed_state::{Backend, BackendEvent, LocalReposStore, Profile, ProfileStore, RepoListStore};
use super::{RepoDetailView, RepoListView};
use crate::image_cache::{MAX_IMAGES, image_cache};
@@ -37,20 +38,23 @@ pub struct SidebarPanel {
focus_handle: FocusHandle,
dock_area: WeakEntity<DockArea>,
explore: Option<WeakEntity<RepoListView>>,
logged_in: bool,
/// Repositories announced by the current user, listed under
/// "All Repositories". Recreated when the signer changes.
my_repos: Option<Entity<RepoListStore>>,
/// Observes the current user's repo store so the list re-renders.
my_repos_subscription: Option<Subscription>,
logged_in: bool,
/// Banner artwork shown behind the sign-in screen,
/// picked at random from the bundled `backgrounds/` assets.
banner: SharedString,
/// Observes the local-repository scan so new discoveries re-render.
_local_repos_subscription: Subscription,
_subscription: Subscription,
}
impl SidebarPanel {
pub fn new(dock_area: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self {
let local_repos_store = LocalReposStore::global(cx);
let backend = Backend::global(cx);
let logged_in = backend.read(cx).current_user().is_some();
@@ -71,14 +75,19 @@ impl SidebarPanel {
cx.notify();
});
let local_repos_subscription = cx.observe(&local_repos_store, |_, _, cx| {
cx.notify();
});
let mut panel = Self {
focus_handle: cx.focus_handle(),
dock_area,
logged_in,
explore: None,
my_repos: None,
my_repos_subscription: None,
logged_in,
banner: pick_banner(),
_local_repos_subscription: local_repos_subscription,
_subscription: subscription,
};
@@ -168,9 +177,13 @@ impl SidebarPanel {
/// The "All Repositories" section: header with the create button and
/// the current user's repositories below it, lazily rendered through a
/// [`uniform_list`].
/// [`uniform_list`], followed by the local git repositories discovered
/// by the startup scan.
fn render_my_repos(&self, cx: &mut Context<Self>) -> impl IntoElement {
let store = self.my_repos.as_ref();
let local = LocalReposStore::global(cx);
let local_repos = local.read(cx).repos.clone();
let scanning = local.read(cx).scanning;
v_flex()
.px_2()
@@ -193,19 +206,37 @@ impl SidebarPanel {
.child(div().text_xs().font_semibold().child("All Repositories")),
)
.child(
Button::new("add")
.icon(IconName::Plus)
.small()
.ghost()
.on_click(cx.listener(|this, _ev, window, cx| {
this.open_create_repo(window, cx);
})),
h_flex()
.gap_1()
.child(
Button::new("rescan")
.icon(IconName::Redo)
.small()
.ghost()
.tooltip("Rescan for local repositories")
.on_click(cx.listener(|_this, _ev, _window, cx| {
LocalReposStore::global(cx)
.update(cx, |store, cx| store.rescan(cx));
})),
)
.child(
Button::new("add")
.icon(IconName::Plus)
.small()
.ghost()
.on_click(cx.listener(|this, _ev, window, cx| {
this.open_create_repo(window, cx);
})),
),
),
)
.when_some(store, |builder, store| {
let announcements = store.read(cx).announcements.clone();
// One merged list: the user's NIP-34 repositories first,
// then the local repositories discovered by the scan.
let total = announcements.len() + local_repos.len();
if announcements.is_empty() {
if total == 0 {
builder.child(
div()
.flex_1()
@@ -213,18 +244,27 @@ impl SidebarPanel {
.py_1()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("No repositories yet"),
.child(if scanning {
"Scanning for local repositories…"
} else {
"No repositories yet"
}),
)
} else {
builder.child(
uniform_list(
"my-repos-list",
announcements.len(),
total,
cx.processor(move |this, range: Range<usize>, _window, cx| {
range
.map(|ix| {
this.render_repo_row(&announcements[ix], cx)
.into_any_element()
this.render_repo_row_at(
&announcements,
&local_repos,
ix,
cx,
)
.into_any_element()
})
.collect()
}),
@@ -236,8 +276,27 @@ impl SidebarPanel {
})
}
/// One repository row in the sidebar, styled like the nav items: a
/// deterministic pixel avatar and the repo name.
/// One row of the merged sidebar list: a NIP-34 repository or a local
/// repository.
fn render_repo_row_at(
&self,
announcements: &[Announcement],
local_repos: &[PathBuf],
ix: usize,
cx: &mut Context<Self>,
) -> AnyElement {
if ix < announcements.len() {
return self
.render_repo_row(&announcements[ix], cx)
.into_any_element();
}
let local_ix = ix - announcements.len();
let path = &local_repos[local_ix];
self.render_local_row(path, cx).into_any_element()
}
fn render_repo_row(
&self,
announcement: &Announcement,
@@ -255,6 +314,28 @@ impl SidebarPanel {
)
}
/// One local repository row: a deterministic pixel avatar seeded from
/// the path, the directory name, and a warning suffix marking it as
/// not yet set up for NIP-34. The row has no click handler — announcing
/// local repositories is future work.
fn render_local_row(&self, path: &Path, cx: &mut Context<Self>) -> impl IntoElement {
let name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string());
NavItem::new(
format!("local-repo:{}", path.display()),
name,
PixelAvatar::new(path.to_string_lossy()),
)
.suffix(
Icon::new(IconName::TriangleAlert)
.small()
.text_color(cx.theme().warning),
)
}
/// Show the Import Identity dialog.
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
import_dialog::open(window, cx);
@@ -491,8 +572,8 @@ impl Render for SidebarPanel {
}
/// A single navigation entry in the sidebar: an arbitrary leading element
/// (an icon, avatar, ...) and a text label with a hover highlight and an
/// optional click handler.
/// (an icon, avatar, ...) and a text label with a hover highlight,
/// an optional trailing suffix (e.g. a status icon) and an optional click handler.
#[allow(clippy::type_complexity)]
#[derive(IntoElement)]
struct NavItem {
@@ -500,6 +581,8 @@ struct NavItem {
style: StyleRefinement,
icon: AnyElement,
label: SharedString,
/// Trailing element rendered at the right edge of the row, after the (ellipsized) label.
suffix: Option<AnyElement>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
@@ -515,10 +598,17 @@ impl NavItem {
icon: icon.into_any_element(),
label: label.into(),
style: StyleRefinement::default(),
suffix: None,
on_click: None,
}
}
/// A trailing element rendered at the right edge of the row
fn suffix(mut self, suffix: impl IntoElement) -> Self {
self.suffix = Some(suffix.into_any_element());
self
}
fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Box::new(listener));
self
@@ -545,6 +635,9 @@ impl RenderOnce for NavItem {
.text_ellipsis()
.child(self.label),
)
.when_some(self.suffix, |this, suffix| {
this.child(div().flex_shrink_0().child(suffix))
})
.hover(|this| this.bg(cx.theme().list_hover))
.when_some(self.on_click, |this, listener| this.on_click(listener))
}