This commit is contained in:
2026-09-04 17:12:52 +07:00
parent 1d224218df
commit 1496b7afeb
24 changed files with 270 additions and 645 deletions
+17 -5
View File
@@ -1,4 +1,7 @@
use gpui::{Pixels, px};
use std::sync::Arc;
use gpui::{Context, Pixels, Window, px};
use gpui_base::dock::PanelView;
mod dock_area;
mod invalid_panel;
@@ -8,12 +11,21 @@ mod window_controls;
pub use dock_area::SignedDockSkin;
pub use gpui_component::dock::{
AnyDrag, BasePanel, BasePanelView, ClosePanel, DockArea, DockAreaState, DockContext, DockEvent,
DockLayout, DockPlacement, DockState, DragPanel, DropIndicator, DropPlaceholderBounds,
DropTarget, Panel, PanelControl, PanelEvent, PanelHandle, PanelInfo, PanelState, PanelStyle,
PanelView, TitleStyle, ToggleZoom, panel_handle, register_panel,
BasePanel, DockArea, DockEvent, DockLayout, DockPlacement, Panel, PanelEvent, panel_handle,
};
/// Add an already-wrapped panel handle to the center of `area`.
///
/// Every panel entry point opens its panel there.
pub fn add_center_panel(
area: &mut DockArea,
panel: Arc<dyn PanelView>,
window: &mut Window,
cx: &mut Context<DockArea>,
) {
area.add_panel_view(panel, DockPlacement::Center, None, window, cx);
}
/// The fixed height of the tab bar, which doubles as the window title bar.
pub const TAB_BAR_HEIGHT: Pixels = px(44.);
+2 -3
View File
@@ -17,6 +17,7 @@ use gpui_base::dock::{
use gpui_base::{ElementExt, InteractiveElementExt, Tab, Tabs};
use gpui_component::animation::{Lerp as _, ease_out_cubic};
use gpui_component::button::{Button, ButtonVariants as _};
use gpui_component::dock::{ClosePanel, PanelControl, PanelHandle, ToggleZoom};
use gpui_component::menu::DropdownMenu as _;
use gpui_component::{
ActiveTheme as _, Disableable as _, IconName, Selectable as _, Sizable as _, h_flex, v_flex,
@@ -24,9 +25,7 @@ use gpui_component::{
use signed_ui::title_bar_drag_handlers;
use crate::dock_area::SkinShared;
use crate::{
ClosePanel, PanelControl, PanelHandle, TAB_BAR_HEIGHT, ToggleZoom, t, window_controls,
};
use crate::{TAB_BAR_HEIGHT, t, window_controls};
/// The drag preview's size, reported to base for the drop placeholder.
const DRAG_PREVIEW_SIZE: gpui::Size<gpui::Pixels> = size(px(96.), px(30.));
+2 -1
View File
@@ -10,6 +10,7 @@ use gpui_base::dock::{
DRAG_BAR_HEIGHT, HANDLE_SIZE, NodeId, ResizeSide, TileContext, TilesRenderer,
};
use gpui_component::button::{Button, ButtonVariants as _};
use gpui_component::dock::PanelHandle;
use gpui_component::menu::{DropdownMenu as _, PopupMenuItem};
use gpui_component::scroll::Scrollbar;
use gpui_component::{
@@ -17,8 +18,8 @@ use gpui_component::{
};
use crate::dock_area::SkinShared;
use crate::t;
use crate::tab_panel::panel_title;
use crate::{PanelHandle, t};
/// How far a resize handle sticks out past the tile's edge.
const HANDLE_OFFSET: Pixels = px(-4.);
+1 -11
View File
@@ -105,7 +105,7 @@ impl Default for LocalReposSettings {
/// recorded when the user clones a repository or picks a folder in the New PR panel.
///
/// The panel can then prefill the folder later without asking again.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct CheckoutRecord {
/// Local folder of the checkout.
@@ -116,16 +116,6 @@ pub struct CheckoutRecord {
pub last_used: u64,
}
impl Default for CheckoutRecord {
fn default() -> Self {
Self {
path: PathBuf::new(),
addr: String::new(),
last_used: 0,
}
}
}
/// Remembered local checkouts, see [`CheckoutRecord`].
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
+36 -21
View File
@@ -71,7 +71,7 @@ const SCAN_MAX_DEPTH: usize = 12;
/// Directories never descended into during a scan.
///
/// Dependency caches can be enormous without ever containing user repositories.
const SCAN_SKIPPED_DIRS: [&str; 1] = ["node_modules"];
const SCAN_SKIPPED_DIR: &str = "node_modules";
/// Walk `root` recursively and collect the paths of git repositories below it.
pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
@@ -107,7 +107,7 @@ pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
}
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
if name.starts_with('.') || SCAN_SKIPPED_DIRS.contains(&name.as_ref()) {
if name.starts_with('.') || name == SCAN_SKIPPED_DIR {
continue;
}
stack.push((entry.path(), depth + 1));
@@ -708,7 +708,8 @@ pub fn sanitize_path_component(id: &str) -> String {
/// In-memory object cache for history walks, see [`open_with_cache`].
///
/// Without one, every walk re-decodes the same commit objects from the object database.
/// Without one, a walk re-decodes the same commit objects from the object database.
/// Sized generously: a walk can cover a large portion of the repository's history.
const OBJECT_CACHE_BYTES: usize = 64 * 1024 * 1024;
/// Metadata of a commit, as shown in the repository browser's file header.
@@ -797,21 +798,35 @@ pub fn find_readme(repo: &gix::Repository) -> Result<Option<PathBuf>> {
.and_then(|path| path.strip_prefix(workdir).ok().map(Path::to_path_buf)))
}
/// Open the repository at `workdir` with an in-memory object cache sized for history walks.
/// Open the repository at `workdir` with an in-memory object cache.
///
/// Only history walks use it, they re-decode the same commit objects repeatedly.
/// Single-object reads open the repository plain.
fn open_with_cache(workdir: &Path) -> Result<gix::Repository> {
let mut repo = gix::open(workdir)?;
repo.object_cache_size_if_unset(OBJECT_CACHE_BYTES);
Ok(repo)
}
/// A [`FileCommit`] from a walk commit, with author, message title and shortened id.
/// `include_description` controls whether the message body is copied.
///
/// History lists never display it,
/// so skipping it saves an allocation per listed commit.
/// A [`FileCommit`] from a commit, with author, message title, body and shortened id.
///
/// The diff panel fetches the full commit on demand.
fn file_commit(commit: &gix::Commit<'_>, include_description: bool) -> Result<FileCommit> {
fn file_commit(commit: &gix::Commit<'_>) -> Result<FileCommit> {
file_commit_with_description(commit, true)
}
/// A [`FileCommit`] without the message body, for history lists that never display it.
///
/// Skipping the body saves an allocation per listed commit.
fn file_commit_summary(commit: &gix::Commit<'_>) -> Result<FileCommit> {
file_commit_with_description(commit, false)
}
/// [`file_commit`] and [`file_commit_summary`], `include_description` picks the body.
fn file_commit_with_description(
commit: &gix::Commit<'_>,
include_description: bool,
) -> Result<FileCommit> {
let author = commit.author()?;
let message = commit.message()?;
Ok(FileCommit {
@@ -903,7 +918,7 @@ fn last_commits(repo: &gix::Repository, rels: &[PathBuf]) -> Result<Vec<(PathBuf
if blob.map(|entry| entry.id().detach()) != parent_blob.map(|entry| entry.id().detach())
{
found.push((rel.clone(), file_commit(&commit, true)?));
found.push((rel.clone(), file_commit(&commit)?));
pending.swap_remove(ix);
} else {
ix += 1;
@@ -952,7 +967,7 @@ pub fn all_commits(repo: &gix::Repository) -> Result<CommitList> {
let info = info?;
total += 1;
if commits.len() < MAX_LISTED_COMMITS {
commits.push(file_commit(&info.object()?, false)?);
commits.push(file_commit_summary(&info.object()?)?);
}
}
Ok(CommitList { total, commits })
@@ -1040,7 +1055,7 @@ pub struct CommitDiff {
///
/// Compared against its first parent, the empty tree for the root commit.
pub fn worktree_commit_diff(workdir: &Path, id: &str) -> Result<CommitDiff> {
commit_diff(&open_with_cache(workdir)?, id)
commit_diff(&gix::open(workdir)?, id)
}
fn commit_diff(repo: &gix::Repository, id: &str) -> Result<CommitDiff> {
@@ -1058,7 +1073,7 @@ fn commit_diff(repo: &gix::Repository, id: &str) -> Result<CommitDiff> {
///
/// Directories and submodules are skipped, files are sorted by path.
pub fn worktree_commit_range_diff(workdir: &Path, base: &str, tip: &str) -> Result<CommitDiff> {
let repo = open_with_cache(workdir)?;
let repo = gix::open(workdir)?;
let base_tree = repo
.rev_parse_single(base.as_bytes())?
.object()?
@@ -1093,7 +1108,7 @@ pub fn worktree_commit_range_commits(
let mut commits = Vec::new();
for info in walk.all()? {
let info = info?;
commits.push(file_commit(&info.object()?, false)?);
commits.push(file_commit_summary(&info.object()?)?);
}
Ok(commits)
}
@@ -1667,7 +1682,7 @@ pub fn head_commit(repo: &gix::Repository) -> Result<Option<FileCommit>> {
return Ok(None);
};
let commit = head.object()?.into_commit();
Ok(Some(file_commit(&commit, true)?))
Ok(Some(file_commit(&commit)?))
}
/// Full metadata of the commit `id`, short or full, in the repository at `workdir`.
@@ -1675,11 +1690,11 @@ pub fn head_commit(repo: &gix::Repository) -> Result<Option<FileCommit>> {
///
/// `Ok(None)` when the id cannot be resolved.
pub fn worktree_commit(workdir: &Path, id: &str) -> Result<Option<FileCommit>> {
let repo = open_with_cache(workdir)?;
let repo = gix::open(workdir)?;
match repo.rev_parse_single(id.as_bytes()) {
Ok(commit_id) => {
let commit = commit_id.object()?.into_commit();
Ok(Some(file_commit(&commit, true)?))
Ok(Some(file_commit(&commit)?))
}
Err(_) => Ok(None),
}
@@ -1709,7 +1724,7 @@ pub fn repo_tags(repo: &gix::Repository) -> Result<Vec<String>> {
/// Short names of local branches, `refs/heads/*`, sorted alphabetically.
pub fn worktree_branches(workdir: &Path) -> Result<Vec<String>> {
repo_branches(&open_with_cache(workdir)?)
repo_branches(&gix::open(workdir)?)
}
/// Short name of the branch HEAD points to, or `None` when detached.
@@ -1770,7 +1785,7 @@ pub fn repo_ref_state(repo: &gix::Repository) -> Result<RepoRefState> {
/// [`repo_ref_state`] for the repository at `workdir`.
pub fn worktree_ref_state(workdir: &Path) -> Result<RepoRefState> {
repo_ref_state(&open_with_cache(workdir)?)
repo_ref_state(&gix::open(workdir)?)
}
/// Everything the browser needs to refresh after a branch or tag switch.
@@ -1791,7 +1806,7 @@ pub struct WorktreeSnapshot {
///
/// Collects entries, the README, the branch HEAD points to and its commit.
pub fn worktree_snapshot(workdir: &Path) -> Result<WorktreeSnapshot> {
let repo = open_with_cache(workdir)?;
let repo = gix::open(workdir)?;
let readme_path = find_readme(&repo)?;
let readme = match &readme_path {
Some(path) => worktree_read(&repo, path)?,
+22 -14
View File
@@ -160,6 +160,14 @@ impl Backend {
this
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
fn push_task(&mut self, task: Task<Result<(), Error>>) {
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
}
/// Bootstrap the client.
///
/// Restore the saved session, if any.
@@ -180,7 +188,7 @@ impl Backend {
Ok::<(), Error>(())
});
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
match task.await {
Ok(()) => {
this.update(cx, |_this, cx| cx.notify())?;
@@ -208,7 +216,7 @@ impl Backend {
let user = cx.read_credentials(USER_KEYRING);
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let content = match user.await {
Ok(Some((_username, secret))) => String::from_utf8(secret)?,
_ => {
@@ -906,7 +914,7 @@ impl Backend {
let pubkey = keys.public_key().to_hex();
let write = cx.write_credentials(USER_KEYRING, &pubkey, nsec.as_bytes());
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
if let Err(e) = write.await {
this.update(cx, |_, cx| cx.emit(BackendEvent::error(e.to_string())))?;
return Ok(());
@@ -932,7 +940,7 @@ impl Backend {
let credential = with_master_key(&uri_string, &keys);
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let result = async {
let mut signer = NostrConnect::new(
connect_uri,
@@ -964,7 +972,7 @@ impl Backend {
pub fn logout(&mut self, cx: &mut Context<Self>) {
let delete = cx.delete_credentials(USER_KEYRING);
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
delete.await.ok();
this.update(cx, |this, cx| {
@@ -984,7 +992,7 @@ impl Backend {
fn bootstrap_user(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
let client = self.client.clone();
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let result = async {
let events: Vec<Event> = client
.fetch_events(filters::grasp_list(public_key))
@@ -1068,7 +1076,7 @@ impl Backend {
Ok(())
});
self.tasks.push(task);
self.push_task(task);
}
/// Add relays and connect to them.
@@ -1083,7 +1091,7 @@ impl Backend {
Ok::<(), Error>(())
});
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
match task.await {
Ok(()) => {
this.update(cx, |_this, cx| cx.notify())?;
@@ -1125,7 +1133,7 @@ impl Backend {
let client = self.client.clone();
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
if let Err(e) = connect_repo_relays_only(&client, relays, filters).await {
log::warn!("repo relay fetch failed: {e}");
// Allow an immediate retry after a failure.
@@ -1145,7 +1153,7 @@ impl Backend {
let task =
cx.background_spawn(async move { subscribe_bootstrap_only(&client, filters).await });
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?;
}
@@ -1168,7 +1176,7 @@ impl Backend {
let (tx, mut rx) = SyncProgress::channel();
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let mut last_percent: u64 = 0;
while rx.changed().await.is_ok() {
@@ -1201,7 +1209,7 @@ impl Backend {
sync_bootstrap_only(&client, filter, opts).await
});
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
match task.await {
Ok(summary) => {
log::debug!(
@@ -1287,7 +1295,7 @@ impl Backend {
fn send_fire_and_forget(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
let task = self.send(builder, cx);
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
this.update(cx, |_this, cx| {
cx.emit(BackendEvent::error(e.to_string()));
@@ -1313,7 +1321,7 @@ impl Backend {
let task = self.send(EventBuilder::new(Kind::EventDeletion, "").tags(tags), cx);
self.tasks.push(cx.spawn(async move |_this, _cx| {
self.push_task(cx.spawn(async move |_this, _cx| {
if let Err(e) = task.await {
log::warn!("failed to retract repository events: {e}");
}
+22 -15
View File
@@ -1,7 +1,6 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::Error;
@@ -67,9 +66,9 @@ struct Remembered {
/// Global store of local-checkout associations and per-checkout statuses.
pub struct CheckoutsStore {
/// Checkout paths per announced repository.
by_repo: Arc<HashMap<RepoAddr, Vec<PathBuf>>>,
by_repo: HashMap<RepoAddr, Vec<PathBuf>>,
/// Ready-to-contribute statuses of the requested repositories.
statuses: Arc<HashMap<RepoAddr, Vec<CheckoutStatus>>>,
statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>,
/// Repositories whose statuses are recomputed on every input change.
///
/// Those are the repository detail panels currently open.
@@ -79,7 +78,7 @@ pub struct CheckoutsStore {
/// The sidebar rows of the user's own repositories and their detail panels.
push_requested: HashSet<RepoAddr>,
/// Ready-to-push statuses of the requested own repositories.
push_statuses: Arc<HashMap<RepoAddr, Vec<CheckoutStatus>>>,
push_statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>,
/// Last announced head branch per requested repository.
///
/// A recompute defaults the base the same way.
@@ -129,19 +128,19 @@ impl CheckoutsStore {
this.status_requested.clear();
this.push_requested.clear();
this.requested_head.clear();
this.statuses = Arc::new(HashMap::new());
this.push_statuses = Arc::new(HashMap::new());
this.statuses = HashMap::new();
this.push_statuses = HashMap::new();
this.refresh(cx);
}
}));
}
let mut store = Self {
by_repo: Arc::new(HashMap::new()),
statuses: Arc::new(HashMap::new()),
by_repo: HashMap::new(),
statuses: HashMap::new(),
status_requested: HashSet::new(),
push_requested: HashSet::new(),
push_statuses: Arc::new(HashMap::new()),
push_statuses: HashMap::new(),
requested_head: HashMap::new(),
refresh: RefreshGate::default(),
_subscriptions: subscriptions,
@@ -155,6 +154,14 @@ impl CheckoutsStore {
store
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
fn push_task(&mut self, task: Task<Result<(), Error>>) {
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
}
/// Remember a successful local-checkout use.
pub fn record(&mut self, path: PathBuf, addr: RepoAddr, cx: &mut Context<Self>) {
if cfg!(target_arch = "wasm32") {
@@ -243,7 +250,7 @@ impl CheckoutsStore {
this.update(cx, |this, cx| this.run_refresh(cx))
});
self.tasks.push(task);
self.push_task(task);
}
/// One resolve and apply cycle, the debounced entry point.
@@ -345,7 +352,7 @@ impl CheckoutsStore {
Ok::<_, Error>((associations, statuses, push_statuses))
});
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let (associations, statuses, push_statuses) = match work.await {
Ok(results) => results,
Err(_) => {
@@ -357,9 +364,9 @@ impl CheckoutsStore {
};
let again = this.update(cx, |this, cx| {
this.by_repo = Arc::new(associations);
this.statuses = Arc::new(statuses);
this.push_statuses = Arc::new(push_statuses);
this.by_repo = associations;
this.statuses = statuses;
this.push_statuses = push_statuses;
cx.notify();
this.refresh.finish()
@@ -386,7 +393,7 @@ impl CheckoutsStore {
this.update(cx, |this, cx| this.run_refresh(cx))
});
this.tasks.push(task);
this.push_task(task);
}
})?;
-2
View File
@@ -13,8 +13,6 @@ pub struct GitStore(GitCache);
impl GitStore {
/// Register the clone cache rooted at `root` as an app-wide global.
///
/// Replaces any installed store, [`signed_state::init`] installs an empty one.
pub fn set_global(root: impl Into<PathBuf>, cx: &mut App) -> Self {
let store = Self::new(root);
cx.set_global(GlobalGitStore(store.0.clone()));
+8 -2
View File
@@ -23,7 +23,12 @@ use signed_nostr::new_backend;
/// Initialize the backend and stores, and install them as globals.
/// Call once at startup, before opening any window that uses the stores.
#[cfg(not(target_arch = "wasm32"))]
pub fn init(db_path: impl AsRef<Path>, scan_paths: Vec<PathBuf>, cx: &mut App) -> Entity<Backend> {
pub fn init(
db_path: impl AsRef<Path>,
repos_root: impl Into<PathBuf>,
scan_paths: Vec<PathBuf>,
cx: &mut App,
) -> Entity<Backend> {
// rustls uses the `aws_lc_rs` provider by default.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
@@ -41,7 +46,8 @@ pub fn init(db_path: impl AsRef<Path>, scan_paths: Vec<PathBuf>, cx: &mut App) -
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
GitStore::set_global(PathBuf::new(), cx);
// The local git clone cache, the grasp mirrors.
GitStore::set_global(repos_root, cx);
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(scan_paths, cx)), cx);
CheckoutsStore::set_global(cx.new(CheckoutsStore::new), cx);
+44 -36
View File
@@ -3,8 +3,11 @@ use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
use anyhow::Error;
use flume::{Receiver, RecvTimeoutError, Sender};
use gpui::{App, AppContext, Context, Entity, Global, SharedString, Subscription, Task};
use flume::{Receiver, Sender};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, Global, SharedString, Subscription, Task,
WeakEntity,
};
use nostr_sdk::prelude::*;
use utils::shorten_pubkey;
@@ -60,14 +63,6 @@ impl Profile {
}
}
/// Message from the fetch task to the main thread.
enum Dispatch {
/// A batched sync finished.
///
/// Re-read seen profiles from the database.
Synced,
}
/// How long to wait for more requests before firing a batched sync.
const BATCH_TIMEOUT: Duration = Duration::from_millis(500);
@@ -114,23 +109,15 @@ impl ProfileStore {
_ => {}
});
// Fetch requests are queued on a channel.
// Fetch requests are queued on a channel, batched into one sync per debounce window.
let client = backend.read(cx).client();
let (sender, receiver) = flume::unbounded::<PublicKey>();
let (dispatch_tx, dispatch_rx) = flume::unbounded::<Dispatch>();
let entity = cx.entity().downgrade();
let mut tasks = Vec::new();
tasks.push(cx.background_spawn(async move {
Self::handle_requests(&client, &dispatch_tx, &receiver).await
}));
// Re-read seen profiles from the database after each batch sync.
tasks.push(cx.spawn(async move |this, cx| {
while let Ok(Dispatch::Synced) = dispatch_rx.recv_async().await {
this.update(cx, |this, cx| this.apply_seen(cx)).ok();
}
Ok(())
tasks.push(cx.spawn(async move |_this, cx| {
Self::handle_requests(entity, &client, &receiver, cx).await
}));
let mut store = Self {
@@ -145,6 +132,14 @@ impl ProfileStore {
store
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
fn push_task(&mut self, task: Task<Result<(), Error>>) {
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
}
/// Get a profile.
///
/// Returns a placeholder with default metadata. Queues a fetch when the profile is not cached yet.
@@ -186,7 +181,7 @@ impl ProfileStore {
Ok::<_, Error>(profiles)
});
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let profiles = work.await?;
this.update(cx, |this, cx| {
@@ -222,7 +217,7 @@ impl ProfileStore {
Ok::<_, Error>(profile)
});
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let profile = work.await?;
this.update(cx, |this, cx| {
@@ -278,7 +273,7 @@ impl ProfileStore {
Ok::<_, Error>(profiles)
});
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let profiles = work.await?;
this.update(cx, |this, cx| {
@@ -294,28 +289,43 @@ impl ProfileStore {
/// Sync metadata for requested authors in batches, debounced to collect requests.
///
/// Results are dispatched to the main thread, which re-reads the database.
/// After each batch, the seen profiles are re-read from the database on the main thread.
async fn handle_requests(
this: WeakEntity<ProfileStore>,
client: &Client,
dispatch: &Sender<Dispatch>,
receiver: &Receiver<PublicKey>,
cx: &mut AsyncApp,
) -> Result<(), Error> {
let mut batch: HashSet<PublicKey> = HashSet::new();
loop {
// Wait for the first request of a batch.
match receiver.recv_timeout(BATCH_TIMEOUT) {
match receiver.recv_async().await {
Ok(public_key) => {
batch.insert(public_key);
}
Err(RecvTimeoutError::Disconnected) => return Ok(()),
Err(RecvTimeoutError::Timeout) => continue,
};
Err(_) => return Ok(()),
}
// Collect everything that arrives within the debounce window.
// The channel has no async timeout, race the receive against a timer.
let deadline = Instant::now() + BATCH_TIMEOUT;
while let Ok(public_key) = receiver.recv_deadline(deadline) {
batch.insert(public_key);
loop {
let now = Instant::now();
if now >= deadline {
break;
}
let timer = cx.background_executor().timer(deadline - now);
futures::pin_mut!(timer);
let recv = receiver.recv_async();
futures::pin_mut!(recv);
match futures::future::select(recv, timer).await {
futures::future::Either::Left((Ok(public_key), _)) => {
batch.insert(public_key);
}
futures::future::Either::Left((Err(_), _)) => return Ok(()),
futures::future::Either::Right(_) => break,
}
}
let filter = Filter::new()
@@ -327,9 +337,7 @@ impl ProfileStore {
// Re-apply from the database afterwards.
match sync_bootstrap_only(client, filter, SyncOptions::default()).await {
Ok(_) => {
if dispatch.send(Dispatch::Synced).is_err() {
log::warn!("profile dispatch channel closed, dropping sync result");
}
let _ = this.update(cx, |this, cx| this.apply_seen(cx));
}
Err(e) => log::warn!("profile sync failed: {e}"),
}
+10 -2
View File
@@ -129,6 +129,14 @@ impl RepoListStore {
store
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
fn push_task(&mut self, task: Task<Result<(), Error>>) {
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
}
/// Negentropy-sync announcements with the bootstrap relays.
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
let backend = Backend::global(cx);
@@ -170,7 +178,7 @@ impl RepoListStore {
this.update(cx, |this, cx| this.run_refresh(cx))
});
self.tasks.push(task);
self.push_task(task);
}
/// One query and apply cycle, the debounced entry point.
@@ -291,7 +299,7 @@ impl RepoListStore {
Ok::<_, Error>((announcements, last_activity, counts))
});
self.tasks.push(cx.spawn(async move |this, cx| {
self.push_task(cx.spawn(async move |this, cx| {
let (announcements, last_activity, counts) = match work.await {
Ok(results) => results,
// Database errors are transient, keep the last list.
+4 -3
View File
@@ -14,6 +14,9 @@ const ACCENT_PROBABILITY: f32 = 0.25;
/// Each left-half cell is mirrored to a right-half one.
const MIN_FILLED: usize = 5;
/// Side length of the avatar in pixels, no setter.
const AVATAR_SIZE: Pixels = px(16.);
/// A deterministic, offline pixel-art avatar.
/// An 8×8 grid with horizontal mirror symmetry.
/// Seeded from a stable string such as the repository id and owner public key.
@@ -21,7 +24,6 @@ const MIN_FILLED: usize = 5;
#[derive(IntoElement)]
pub struct PixelAvatar {
seed: u64,
size: Pixels,
style: StyleRefinement,
}
@@ -31,7 +33,6 @@ impl PixelAvatar {
pub fn new(seed: impl AsRef<str>) -> Self {
Self {
seed: fnv1a(seed.as_ref().as_bytes()),
size: px(16.),
style: StyleRefinement::default(),
}
}
@@ -79,7 +80,7 @@ impl RenderOnce for PixelAvatar {
.grid()
.grid_cols(GRID_SIZE as u16)
.grid_rows(GRID_SIZE as u16)
.size(self.size)
.size(AVATAR_SIZE)
.flex_shrink_0()
.overflow_hidden()
.bg(main.opacity(0.16))
@@ -5,8 +5,10 @@ use assets::CustomIconName;
use gpui::prelude::*;
use gpui::{AnyElement, App, Entity, SharedString, div, px};
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::combobox::{Caret, ComboboxTriggerContext};
use gpui_component::input::{Textarea, TextareaState};
use gpui_component::menu::PopupMenu;
use gpui_component::searchable_list::SearchableVec;
use gpui_component::tag::Tag;
use gpui_component::tree::TreeItem;
use gpui_component::{ActiveTheme, Icon, Sizable, StyledExt, h_flex, v_flex};
@@ -365,6 +367,44 @@ pub(super) fn pr_roots(store: &RepoStore) -> &[Event] {
&store.pull_requests
}
/// The trigger body of the branch/tag selectors.
///
/// The kind icon, the selection or placeholder, and the caret.
/// `Combobox` replaces its default trigger entirely,
/// the only way to show an icon inside it.
pub(super) fn ref_selector_trigger(
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
icon: CustomIconName,
cx: &App,
) -> AnyElement {
let muted = cx.theme().muted_foreground;
h_flex()
.w_full()
.min_w_0()
.gap_1()
.items_center()
.child(Icon::new(icon).small().flex_shrink_0())
.child(
div()
.flex_1()
.min_w_0()
.overflow_hidden()
.text_ellipsis()
.whitespace_nowrap()
.when(ctx.selection().is_empty(), |this| this.text_color(muted))
.child(
ctx.selection()
.first()
.map(|(_, item)| item.clone())
.or_else(|| ctx.placeholder().cloned())
.unwrap_or_default(),
),
)
.child(Caret::new(ctx.size()).text_color(muted))
.into_any_element()
}
/// Section heading of a detail sidebar, shared by the issue and PR panels.
pub(super) fn sidebar_title(text: &str, cx: &App) -> AnyElement {
div()
@@ -459,14 +499,7 @@ pub(super) fn sidebar_section(
}
/// The comments on a root event, issue or PR, one card per comment.
///
/// Comment bodies become shared strings once per comment, not per render.
pub(super) fn comments_section(
store: &Entity<RepoStore>,
root: EventId,
contents: &mut HashMap<EventId, SharedString>,
cx: &App,
) -> AnyElement {
pub(super) fn comments_section(store: &Entity<RepoStore>, root: EventId, cx: &App) -> AnyElement {
let store = store.read(cx);
let comments: Vec<&Event> = store.comments_of(&root).collect();
let title = SharedString::from(format!("Discussions {}", comments.len()));
@@ -479,10 +512,7 @@ pub(super) fn comments_section(
let author = profile.name();
let picture = profile.picture();
let age = relative_time(comment.created_at);
let content = contents
.entry(comment.id)
.or_insert_with(|| SharedString::from(comment.content.clone()))
.clone();
let content = SharedString::from(comment.content.as_str());
v_flex()
.gap_1()
@@ -1,5 +1,3 @@
use std::collections::HashMap;
use dock::{BasePanel, Panel, PanelEvent};
use gpui::prelude::*;
use gpui::{
@@ -22,7 +20,6 @@ pub struct IssueDetailView {
/// Repo store holding the issues and their statuses.
store: Entity<RepoStore>,
issue_id: EventId,
contents: HashMap<EventId, SharedString>,
/// Input state of the comment textarea.
comment_input: Entity<TextareaState>,
focus_handle: FocusHandle,
@@ -43,7 +40,6 @@ impl IssueDetailView {
store,
issue_id,
comment_input,
contents: HashMap::new(),
}
}
}
@@ -91,17 +87,11 @@ impl Render for IssueDetailView {
let (title, author, picture, status, age, issue_id, content) = {
let profile_store = ProfileStore::global(cx);
let profile = profile_store.read(cx).get(&issue.pubkey);
let content = self
.contents
.entry(issue.id)
.or_insert_with(|| {
if issue.content.is_empty() {
SharedString::from("No description provided.")
} else {
SharedString::from(&issue.content)
}
})
.clone();
let content = if issue.content.is_empty() {
SharedString::from("No description provided.")
} else {
SharedString::from(&issue.content)
};
(
activity_subject(issue),
@@ -172,12 +162,7 @@ impl Render for IssueDetailView {
)
.child(div().text_sm().child(content)),
)
.child(comments_section(
&self.store,
issue_id,
&mut self.contents,
cx,
))
.child(comments_section(&self.store, issue_id, cx))
.child(comment_form(
&self.store,
issue_id,
@@ -1,7 +1,7 @@
use std::rc::Rc;
use assets::CustomIconName;
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use dock::{BasePanel, DockArea, Panel, PanelEvent, add_center_panel, panel_handle};
use gpui::prelude::*;
use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
@@ -110,7 +110,7 @@ impl IssuesView {
let panel = cx.new(|cx| IssueDetailView::new(self.store.clone(), issue_id, window, cx));
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
+23 -118
View File
@@ -5,7 +5,7 @@ use std::time::Duration;
use anyhow::Error;
use assets::CustomIconName;
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use dock::{BasePanel, DockArea, Panel, PanelEvent, add_center_panel, panel_handle};
use gix::Repository;
use gpui::prelude::*;
use gpui::{
@@ -16,9 +16,7 @@ use gpui::{
use gpui_base::{Button as BaseButton, Disableable, Popover};
use gpui_component::alert::Alert;
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::combobox::{
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
};
use gpui_component::combobox::{Combobox, ComboboxEvent, ComboboxState};
use gpui_component::menu::DropdownMenu;
use gpui_component::searchable_list::SearchableVec;
use gpui_component::tree::TreeState;
@@ -26,14 +24,14 @@ use gpui_component::{
ActiveTheme, Colorize, Icon, IconName, Sizable, StyledExt, ThemeStyled,
VirtualListScrollHandle, h_flex, v_flex,
};
use nostr::prelude::{EventId, RelayUrl, ToBech32};
use nostr::prelude::{RelayUrl, ToBech32};
use signed_core::{Announcement, RepoAddr, RepoStatus, filters};
use signed_git::{CommitList, FileCommit};
use signed_state::{
Backend, CheckoutStatus, CheckoutsStore, GitStore, LocalReposStore, ProfileStore,
RepoListStore, RepoStore, pr_proposes_checkout,
};
use signed_ui::{DropdownButton, PixelAvatar, UserAvatar, copy_row};
use signed_ui::{CountBadge, DropdownButton, PixelAvatar, UserAvatar, copy_row};
mod about;
mod browser;
@@ -55,7 +53,10 @@ use browser::{
};
use commits::COMMIT_ROW_HEIGHT;
use diff::CommitDiffView;
use helpers::{ShareTargets, TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
use helpers::{
ShareTargets, TreeItemSeed, build_tree_items, is_markdown_path, ref_selector_trigger,
tree_items,
};
use issues::{IssuesView, open_new_issue_dialog};
use pull_requests::PullRequestsView;
use send_patch::open_send_patch_panel;
@@ -106,21 +107,6 @@ struct RepoData {
head_commit: Option<FileCommit>,
}
/// Derived NIP-34 header data.
/// Renders avoid re-encoding bech32 share targets per frame.
/// They also avoid rebuilding the clone command strings.
struct HeaderCache {
/// Announcement event ID and owner NIP-05 this cache was built from.
/// Rebuilt when either changes.
/// A new announcement version, or the owner's profile arriving with a NIP-05 identifier.
key: (EventId, Option<String>),
announcement: Rc<Announcement>,
share: Rc<ShareTargets>,
ngit_command: SharedString,
nak_command: SharedString,
git_commands: Rc<Vec<SharedString>>,
}
/// Detail view of a repository, header, stats and metadata.
/// A file explorer with README preview, cloned from the announcement's `clone` URLs.
pub struct RepoDetailView {
@@ -193,10 +179,6 @@ pub struct RepoDetailView {
/// Bumped on every branch/tag switch.
/// In-flight loads with an older generation are discarded when they complete.
ref_generation: u64,
/// Derived NIP-34 header data, share targets and clone commands.
/// Rebuilt only when the announcement or the owner's NIP-05 changes.
/// Not on every render.
header_cache: Option<HeaderCache>,
/// In-flight tasks, finished tasks are pruned on every push.
/// The vec stays bounded by the number of concurrent loads.
tasks: Vec<Task<Result<(), Error>>>,
@@ -346,7 +328,6 @@ impl RepoDetailView {
tag_select,
switching_ref: false,
ref_generation: 0,
header_cache: None,
focus_handle: cx.focus_handle(),
tasks: Vec::new(),
_subscriptions: subscriptions,
@@ -922,7 +903,7 @@ impl RepoDetailView {
cx.new(|cx| CommitDiffView::new(worktree, repo_name, commit_id.into(), window, cx));
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
@@ -1056,7 +1037,7 @@ impl RepoDetailView {
let panel = cx.new(|cx| IssuesView::new(self.dock_area.clone(), store, window, cx));
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
@@ -1072,7 +1053,7 @@ impl RepoDetailView {
let panel = cx.new(|cx| PullRequestsView::new(self.dock_area.clone(), store, window, cx));
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
@@ -1232,43 +1213,6 @@ impl RepoDetailView {
});
}
/// Trigger body for the branch/tag selectors.
/// The kind icon, the selection or placeholder, and the caret.
/// `Combobox` replaces its default trigger entirely.
/// That is the only way to show an icon inside it.
fn render_ref_trigger(
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
icon: CustomIconName,
cx: &App,
) -> AnyElement {
let muted = cx.theme().muted_foreground;
h_flex()
.w_full()
.min_w_0()
.gap_1()
.items_center()
.child(Icon::new(icon).small().flex_shrink_0())
.child(
div()
.flex_1()
.min_w_0()
.overflow_hidden()
.text_ellipsis()
.whitespace_nowrap()
.when(ctx.selection().is_empty(), |this| this.text_color(muted))
.child(
ctx.selection()
.first()
.map(|(_, item)| item.clone())
.or_else(|| ctx.placeholder().cloned())
.unwrap_or_default(),
),
)
.child(Caret::new(ctx.size()).text_color(muted))
.into_any_element()
}
/// Refresh the file explorer, preview pane and commit list after a successful switch.
/// The selectors were already updated by [`Self::switch_ref`].
/// [`Self::switching_ref`] stays set until this reload finishes.
@@ -1422,9 +1366,8 @@ impl RepoDetailView {
return div().into_any_element();
};
// The header derives bech32 share targets and clone commands.
// Rebuild them only when the announcement or the owner's NIP-05 changes.
// Not on every render.
// Derived NIP-34 header data, share targets and clone commands.
// Rebuilt per frame: two bech32 encodes and a couple of format strings.
let nip05 = ProfileStore::global(cx)
.read(cx)
.get(&source.owner)
@@ -1432,32 +1375,12 @@ impl RepoDetailView {
.nip05
.clone()
.filter(|nip05| !nip05.trim().is_empty());
let key = (source.event_id, nip05);
if self
.header_cache
.as_ref()
.is_none_or(|cache| cache.key != key)
{
let announcement = source.clone();
let share = ShareTargets::from_announcement(&announcement);
let nostr_url = nostr_clone_url(&announcement, key.1.as_deref());
self.header_cache = Some(HeaderCache {
ngit_command: SharedString::from(format!("git clone {nostr_url}")),
nak_command: SharedString::from(format!("nak git clone {nostr_url}")),
git_commands: Rc::new(announcement.clone_urls()),
share: Rc::new(share),
announcement: Rc::new(announcement),
key,
});
}
let cache = self.header_cache.as_ref().expect("cache just built");
let announcement = cache.announcement.clone();
let share = cache.share.clone();
let ngit_command = cache.ngit_command.clone();
let nak_command = cache.nak_command.clone();
let git_commands = cache.git_commands.clone();
let announcement = Rc::new(source.clone());
let share = Rc::new(ShareTargets::from_announcement(&announcement));
let nostr_url = nostr_clone_url(&announcement, nip05.as_deref());
let ngit_command = SharedString::from(format!("git clone {nostr_url}"));
let nak_command = SharedString::from(format!("nak git clone {nostr_url}"));
let git_commands = Rc::new(announcement.clone_urls());
let name = self.display_name(cx);
let description = announcement.description();
@@ -2165,19 +2088,7 @@ impl RepoDetailView {
.child("Commits"),
)
.when_some(commits_count, |this, count| {
this.child(
h_flex()
.justify_center()
.px_1()
.py_0p5()
.min_w_4()
.text_size(px(8.))
.bg(cx.theme().muted)
.text_color(cx.theme().muted_foreground)
.rounded(cx.theme().radius)
.line_height(relative(1.))
.child(SharedString::from(count.to_string())),
)
this.child(CountBadge::new(count))
})
.text_color(cx.theme().button_foreground)
.rounded(cx.theme().radius)
@@ -2241,7 +2152,7 @@ impl RepoDetailView {
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.render_trigger(|ctx, _window, cx| {
Self::render_ref_trigger(ctx, CustomIconName::GitBranch, cx)
ref_selector_trigger(ctx, CustomIconName::GitBranch, cx)
}),
),
)
@@ -2255,7 +2166,7 @@ impl RepoDetailView {
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.render_trigger(|ctx, _window, cx| {
Self::render_ref_trigger(ctx, CustomIconName::Tag, cx)
ref_selector_trigger(ctx, CustomIconName::Tag, cx)
}),
),
),
@@ -2492,13 +2403,7 @@ pub(crate) fn open_repo_panel(
if let Some(dock_area) = dock_area.upgrade() {
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(
panel_handle(detail.clone()),
DockPlacement::Center,
None,
window,
cx,
);
add_center_panel(dock_area, panel_handle(detail.clone()), window, cx);
});
}
@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use assets::CustomIconName;
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use dock::{BasePanel, DockArea, Panel, PanelEvent, add_center_panel, panel_handle};
use gpui::prelude::*;
use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, PathPromptOptions,
@@ -11,9 +11,7 @@ use gpui::{
};
use gpui_base::{Button as BaseButton, StyledExt};
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::combobox::{
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
};
use gpui_component::combobox::{Combobox, ComboboxEvent, ComboboxState};
use gpui_component::input::{Input, InputState, Textarea, TextareaState};
use gpui_component::menu::{DropdownMenu, PopupMenu, PopupMenuItem};
use gpui_component::scroll::Scrollbar;
@@ -30,10 +28,11 @@ use signed_git::{
sanitize_path_component, worktree_commit_range_commits, worktree_commit_range_diff,
};
use signed_state::{Backend, CheckoutsStore, GitStore, RepoListStore, RepoStore};
use signed_ui::placeholder;
use signed_ui::{CountBadge, placeholder};
use super::commits::{COMMIT_ROW_HEIGHT, commit_row};
use super::diff::{CommitDiffView, DiffPane};
use super::helpers::ref_selector_trigger;
/// The new pull request panel of a repository.
pub struct NewPullRequestView {
@@ -1019,7 +1018,7 @@ impl NewPullRequestView {
});
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
@@ -1080,7 +1079,7 @@ impl NewPullRequestView {
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.render_trigger(|ctx, _window, cx| {
render_ref_trigger(ctx, CustomIconName::GitBranch, cx)
ref_selector_trigger(ctx, CustomIconName::GitBranch, cx)
}),
),
),
@@ -1105,7 +1104,7 @@ impl NewPullRequestView {
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.render_trigger(|ctx, _window, cx| {
render_ref_trigger(ctx, CustomIconName::GitBranch, cx)
ref_selector_trigger(ctx, CustomIconName::GitBranch, cx)
}),
),
),
@@ -1259,7 +1258,7 @@ impl NewPullRequestView {
.child(Icon::new(CustomIconName::GitFile).small())
.child("Files"),
)
.child(count_badge(files, cx))
.child(CountBadge::new(files))
.text_color(cx.theme().button_foreground)
.rounded(cx.theme().radius)
.hover(|this| this.bg(cx.theme().button_hover))
@@ -1287,7 +1286,7 @@ impl NewPullRequestView {
.child(Icon::new(CustomIconName::GitCommit).small())
.child("Commits"),
)
.child(count_badge(commits, cx))
.child(CountBadge::new(commits))
.text_color(cx.theme().button_foreground)
.rounded(cx.theme().radius)
.hover(|this| this.bg(cx.theme().button_hover))
@@ -1390,55 +1389,6 @@ impl NewPullRequestView {
}
}
/// The count badge of a tab, styled like the repository panel's.
fn count_badge(count: usize, cx: &App) -> impl IntoElement {
h_flex()
.justify_center()
.px_1()
.py_0p5()
.min_w_4()
.text_size(px(8.))
.bg(cx.theme().muted)
.text_color(cx.theme().muted_foreground)
.rounded(cx.theme().radius)
.line_height(relative(1.))
.child(SharedString::from(count.to_string()))
}
/// The trigger of a branch selector.
fn render_ref_trigger(
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
icon: CustomIconName,
cx: &App,
) -> AnyElement {
let muted = cx.theme().muted_foreground;
h_flex()
.w_full()
.min_w_0()
.gap_1()
.items_center()
.child(Icon::new(icon).small().flex_shrink_0())
.child(
div()
.flex_1()
.min_w_0()
.overflow_hidden()
.text_ellipsis()
.whitespace_nowrap()
.when(ctx.selection().is_empty(), |this| this.text_color(muted))
.child(
ctx.selection()
.first()
.map(|(_, item)| item.clone())
.or_else(|| ctx.placeholder().cloned())
.unwrap_or_default(),
),
)
.child(Caret::new(ctx.size()).text_color(muted))
.into_any_element()
}
/// Open the new pull request panel in the center dock.
pub(super) fn open_new_pull_panel(
dock_area: WeakEntity<DockArea>,
@@ -1449,7 +1399,7 @@ pub(super) fn open_new_pull_panel(
let panel = cx.new(|cx| NewPullRequestView::new(dock_area.clone(), store, window, cx));
let _ = dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
@@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
@@ -17,7 +16,6 @@ use gpui_component::input::{Textarea, TextareaState};
use gpui_component::scroll::{ScrollableElement, Scrollbar};
use gpui_component::spinner::Spinner;
use gpui_component::tab::{Tab, TabBar};
use gpui_component::tag::Tag;
use gpui_component::{
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, WindowExt, h_flex, v_flex,
v_virtual_list,
@@ -26,7 +24,7 @@ use nostr::prelude::{Event, EventId, Kind, Nip34Tag};
use signed_core::{activity_subject, pull_request_patch};
use signed_git::{FileCommit, patch_commits, patch_diffs};
use signed_state::{Backend, GitStore, ProfileStore, RepoStore};
use signed_ui::{UserAvatar, placeholder, status_badge};
use signed_ui::{CountBadge, UserAvatar, placeholder, status_badge};
use utils::{relative_time, relative_time_secs};
use super::diff::{CommitDiffView, DiffPane};
@@ -68,8 +66,6 @@ pub struct PullRequestDetailView {
commit_item_sizes: Rc<Vec<Size<Pixels>>>,
/// Virtual list state of the commits tab.
commit_scroll_handle: VirtualListScrollHandle,
/// Comment bodies as shared strings, keyed by comment event ID.
contents: HashMap<EventId, SharedString>,
/// In-flight tasks, finished tasks are pruned on every push.
tasks: Vec<Task<Result<(), anyhow::Error>>>,
}
@@ -110,7 +106,6 @@ impl PullRequestDetailView {
pane,
commit_item_sizes: Rc::new(Vec::new()),
commit_scroll_handle: VirtualListScrollHandle::new(),
contents: HashMap::new(),
tasks: Vec::new(),
}
}
@@ -315,22 +310,14 @@ impl PullRequestDetailView {
Tab::new()
.label("Files")
.when_some(files_count, |this, count| {
this.suffix(
Tag::secondary()
.xsmall()
.child(SharedString::from(count.to_string())),
)
this.suffix(CountBadge::new(count))
}),
)
.child(
Tab::new()
.label("Commits")
.when_some(commits_count, |this, count| {
this.suffix(
Tag::secondary()
.xsmall()
.child(SharedString::from(count.to_string())),
)
this.suffix(CountBadge::new(count))
}),
)
.into_any_element()
@@ -410,12 +397,7 @@ impl PullRequestDetailView {
this.child(div().text_sm().child(self.description.clone()))
}),
)
.child(comments_section(
&self.store,
root_id,
&mut self.contents,
cx,
))
.child(comments_section(&self.store, root_id, cx))
.child(comment_form(
&self.store,
root_id,
@@ -1,7 +1,7 @@
use std::rc::Rc;
use assets::CustomIconName;
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use dock::{BasePanel, DockArea, Panel, PanelEvent, add_center_panel, panel_handle};
use gpui::prelude::*;
use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
@@ -125,7 +125,7 @@ impl PullRequestsView {
});
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
@@ -1,4 +1,4 @@
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use dock::{BasePanel, DockArea, Panel, PanelEvent, add_center_panel, panel_handle};
use gpui::prelude::*;
use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString,
@@ -202,7 +202,7 @@ pub(super) fn open_send_patch_panel(
let panel = cx.new(|cx| SendPatchView::new(dock_area.clone(), store, window, cx));
let _ = dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
+5 -9
View File
@@ -4,7 +4,9 @@ use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use assets::CustomIconName;
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, TAB_BAR_HEIGHT, panel_handle};
use dock::{
BasePanel, DockArea, Panel, PanelEvent, TAB_BAR_HEIGHT, add_center_panel, panel_handle,
};
use gpui::prelude::*;
use gpui::{
AnyElement, App, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, ObjectFit, Render,
@@ -153,7 +155,7 @@ impl SidebarPanel {
self.explore = Some(panel.downgrade());
let _ = self.dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
@@ -197,13 +199,7 @@ impl SidebarPanel {
cx.new(|cx| RepoDetailView::new_local(self.dock_area.clone(), path, window, cx));
let _ = self.dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel_view(
panel_handle(detail),
DockPlacement::Center,
None,
window,
cx,
);
add_center_panel(dock_area, panel_handle(detail), window, cx);
});
}