update repo list

This commit is contained in:
2026-08-30 12:41:35 +07:00
parent eda3593982
commit c61695e350
12 changed files with 118 additions and 36 deletions
@@ -134,7 +134,7 @@ impl RepoDetailView {
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child("Cloning repository"),
.child("Cloning repository..."),
)
.into_any_element()
} else if let Some(error) = error {
@@ -37,7 +37,7 @@ impl IssueDetailView {
cx: &mut Context<Self>,
) -> Self {
let comment_input =
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment"));
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment..."));
Self {
focus_handle: cx.focus_handle(),
@@ -347,7 +347,7 @@ impl IssuesView {
/// through [`RepoStore::open_issue`] when confirmed.
pub(super) fn open_new_issue_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Issue title"));
let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue"));
let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue..."));
window.open_dialog(cx, move |dialog, _window, _cx| {
let subject = subject.clone();
@@ -94,7 +94,7 @@ impl PullRequestDetailView {
) -> Self {
let tree_state = cx.new(|cx| TreeState::new(cx));
let comment_input =
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment"));
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment..."));
// Same display name as the repo detail panel's title.
let repo_name = store
@@ -442,9 +442,9 @@ pub(super) fn open_new_pull_request_dialog(
) {
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Pull request title"));
let description =
cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change"));
let patch =
cx.new(|cx| TextareaState::new(window, cx).placeholder("Paste `git format-patch` output"));
cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change..."));
let patch = cx
.new(|cx| TextareaState::new(window, cx).placeholder("Paste `git format-patch` output..."));
window.open_dialog(cx, move |dialog, _window, _cx| {
let subject = subject.clone();
+92 -25
View File
@@ -1,5 +1,6 @@
use std::rc::Rc;
use assets::CustomIconName;
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use gpui::prelude::*;
use gpui::{
@@ -8,9 +9,11 @@ use gpui::{
};
use gpui_base::Button as BaseButton;
use gpui_component::avatar::Avatar;
use gpui_component::input::{Input, InputEvent, InputState};
use gpui_component::scroll::Scrollbar;
use gpui_component::{
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
ActiveTheme, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex,
v_virtual_list,
};
use signed_core::Announcement;
use signed_state::{ProfileStore, RepoListStore, Timestamp};
@@ -38,10 +41,24 @@ enum RepoFilter {
}
impl RepoFilter {
fn visible(self, store: &RepoListStore) -> Vec<usize> {
/// Indices into the store's `announcements` included by this filter, in
/// display order, narrowed to repositories whose name (or id) contains
/// `query`; an empty query matches everything.
fn visible(self, store: &RepoListStore, query: &str) -> Vec<usize> {
let announcements = &store.announcements;
let mut indices: Vec<usize> = (0..announcements.len()).collect();
// Narrow by the search query first, so "Recent" limits the matches
// and "Popular" ranks them.
let query = query.trim().to_lowercase();
if !query.is_empty() {
indices.retain(|&ix| {
let announcement = &announcements[ix];
let name = announcement.name.as_deref().unwrap_or(&announcement.id);
name.to_lowercase().contains(&query)
});
}
match self {
Self::All => {}
Self::Recent => indices.truncate(RECENT_COUNT),
@@ -57,6 +74,14 @@ impl RepoFilter {
indices
}
fn icon_name(self) -> CustomIconName {
match self {
Self::All => CustomIconName::Grid,
Self::Recent => CustomIconName::Recent,
Self::Popular => CustomIconName::Trending,
}
}
}
/// Browse all announced repositories.
@@ -72,22 +97,35 @@ pub struct RepoListView {
/// Number of rows [`Self::item_sizes`] was built for (the filtered repo count).
repo_len: usize,
/// Indices into the store's `announcements` matching [`Self::filter`],
/// in display order; rebuilt when the store changes or the filter is switched.
/// The virtual list renders this slice.
/// in display order; rebuilt when the store changes, the filter is
/// switched, or the search text changes. The virtual list renders this
/// slice.
visible: Vec<usize>,
/// Search box filtering repositories by name.
search: Entity<InputState>,
/// Rebuilds the visible slice as the search text changes.
_search_subscription: Subscription,
_subscription: Subscription,
}
impl RepoListView {
pub fn new(
dock_area: WeakEntity<DockArea>,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let store = RepoListStore::global(cx);
// Keep the visible slice and row sizes in sync with the store, so
// newly announced repositories appear without waiting for a click.
// Live search over repository names
let search = cx.new(|cx| InputState::new(window, cx).placeholder("Search..."));
let search_subscription = cx.subscribe(&search, |this, _search, event, cx| {
if matches!(event, InputEvent::Change) {
this.rebuild_rows(cx);
}
});
// Keep the visible slice and row sizes in sync with the store,
// so newly announced repositories appear without waiting for a click.
let subscription = cx.observe(&store, |this, _store, cx| {
this.rebuild_rows(cx);
});
@@ -101,6 +139,8 @@ impl RepoListView {
item_sizes: Rc::new(Vec::new()),
repo_len: 0,
visible: Vec::new(),
search,
_search_subscription: search_subscription,
_subscription: subscription,
};
@@ -113,13 +153,15 @@ impl RepoListView {
}
/// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the current
/// store contents and [`Self::filter`]. Called when the view is created,
/// when the store changes, and when the filter is switched,
/// so the list is ready before the next render.
/// store contents, [`Self::filter`] and the search query. Called when
/// the view is created, when the store changes, when the filter is
/// switched, and on every search keystroke, so the list is ready before
/// the next render.
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
let filter = self.filter;
let query = self.search.read(cx).value();
let store = self.store.read(cx);
self.visible = filter.visible(store);
self.visible = filter.visible(store, &query);
// Each virtual list row holds `COLUMNS` repo cards.
let rows = self.visible.len().div_ceil(COLUMNS);
@@ -252,18 +294,44 @@ impl RepoListView {
.into_any_element()
}
/// Header of the explore list: title, the count of visible repositories and the sort filter buttons.
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
fn render_header(&self, count: usize, cx: &mut Context<Self>) -> AnyElement {
h_flex()
.px_4()
.py_2()
.w_full()
.gap_3()
.items_center()
.child(self.filter_button(RepoFilter::All, "All", cx))
.child(self.filter_button(RepoFilter::Popular, "Popular", cx))
.child(self.filter_button(RepoFilter::Recent, "Recent", cx))
.child(
h_flex()
.gap_1()
.text_xs()
.child(div().font_semibold().child("Repositories"))
.child(
div()
.w_10()
.min_w_0()
.truncate()
.text_ellipsis()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(format!("({count})"))),
),
)
.child(
Input::new(&self.search)
.cleanable(true)
.w(px(180.))
.text_sm()
.border_color(cx.theme().muted)
.bg(cx.theme().muted)
.prefix(Icon::new(IconName::Search).small()),
)
.child(div().flex_1())
.child(
h_flex()
.gap_1()
.child(self.filter_button(RepoFilter::All, "All", cx))
.child(self.filter_button(RepoFilter::Popular, "Popular", cx))
.child(self.filter_button(RepoFilter::Recent, "Recent", cx)),
)
.into_any_element()
}
@@ -275,22 +343,22 @@ impl RepoListView {
label: &'static str,
cx: &mut Context<Self>,
) -> AnyElement {
let active = self.filter == filter;
BaseButton::new(label)
.flex()
.items_center()
.h_7()
.px_2()
.gap_1()
.child(Icon::new(filter.icon_name()))
.child(div().text_sm().child(label))
.text_color(cx.theme().button_foreground)
.rounded(cx.theme().radius)
.hover(|this| this.bg(cx.theme().button_hover))
.active(|this| this.bg(cx.theme().primary_active))
.selected(self.filter == filter)
.when(self.filter == filter, |this| {
this.bg(cx.theme().primary)
.text_color(cx.theme().primary_foreground)
})
.active(|this| this.bg(cx.theme().button_active))
.selected(active)
.when(active, |this| this.bg(cx.theme().button_active))
.on_click(cx.listener(move |this, _event, _window, cx| {
this.filter = filter;
this.rebuild_rows(cx);
@@ -331,8 +399,7 @@ impl Render for RepoListView {
.relative()
.image_cache(image_cache("repos", MAX_IMAGES))
.size_full()
.pb_3()
.child(self.render_header(cx))
.child(self.render_header(count, cx))
.when(!has_repos, |this| {
this.child(
v_flex().size_full().items_center().justify_center().child(
@@ -214,7 +214,7 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("Loading your grasp servers"),
.child("Loading your grasp servers..."),
)
},
)