.
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{AnyElement, App, Context, SharedString, div, transparent_white};
|
use gpui::{AnyElement, App, Context, SharedString, div, transparent_white};
|
||||||
@@ -10,6 +13,48 @@ use signed_state::{Backend, CheckoutStatus, CheckoutsStore, pr_proposes_checkout
|
|||||||
use super::RepoDetailView;
|
use super::RepoDetailView;
|
||||||
use crate::views::pull_requests::new::open_new_pull_panel;
|
use crate::views::pull_requests::new::open_new_pull_panel;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct Banners {
|
||||||
|
dismissed: HashSet<(PathBuf, String)>,
|
||||||
|
ready_requested: bool,
|
||||||
|
/// Re-requested only when the announced HEAD or the base default changes.
|
||||||
|
ready_head: Option<String>,
|
||||||
|
ready_statuses: Vec<CheckoutStatus>,
|
||||||
|
push_statuses: Vec<CheckoutStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Banners {
|
||||||
|
pub(super) fn dismissal(&self, status: &CheckoutStatus) -> bool {
|
||||||
|
self.dismissed
|
||||||
|
.contains(&(status.path.clone(), status.branch.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn dismiss(&mut self, status: &CheckoutStatus) {
|
||||||
|
self.dismissed
|
||||||
|
.insert((status.path.clone(), status.branch.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn ready_requested_at(&self) -> (bool, &Option<String>) {
|
||||||
|
(self.ready_requested, &self.ready_head)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mark_ready_requested(&mut self, head: Option<String>) {
|
||||||
|
self.ready_requested = true;
|
||||||
|
self.ready_head = head;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_statuses(
|
||||||
|
&mut self,
|
||||||
|
ready: Vec<CheckoutStatus>,
|
||||||
|
push: Vec<CheckoutStatus>,
|
||||||
|
) -> bool {
|
||||||
|
let changed = ready != self.ready_statuses || push != self.push_statuses;
|
||||||
|
self.ready_statuses = ready;
|
||||||
|
self.push_statuses = push;
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RepoDetailView {
|
impl RepoDetailView {
|
||||||
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
@@ -23,10 +68,7 @@ impl RepoDetailView {
|
|||||||
let statuses = CheckoutsStore::global(cx).read(cx).ready_statuses_of(addr);
|
let statuses = CheckoutsStore::global(cx).read(cx).ready_statuses_of(addr);
|
||||||
|
|
||||||
'status: for status in statuses {
|
'status: for status in statuses {
|
||||||
if self
|
if self.banners.dismissal(&status) {
|
||||||
.banner_dismissed
|
|
||||||
.contains(&(status.path.clone(), status.branch.clone()))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for pr in &store.pull_requests {
|
for pr in &store.pull_requests {
|
||||||
@@ -55,17 +97,15 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
let statuses = CheckoutsStore::global(cx).read(cx).push_statuses_of(addr);
|
let statuses = CheckoutsStore::global(cx).read(cx).push_statuses_of(addr);
|
||||||
|
|
||||||
statuses.into_iter().find(|status| {
|
statuses
|
||||||
!self
|
.into_iter()
|
||||||
.banner_dismissed
|
.find(|status| !self.banners.dismissal(status))
|
||||||
.contains(&(status.path.clone(), status.branch.clone()))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn render_push_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
pub(super) fn render_push_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||||
let status = self.push_suggestion(cx)?;
|
let status = self.push_suggestion(cx)?;
|
||||||
let key = (status.path.clone(), status.branch.clone());
|
|
||||||
let path = status.path.clone();
|
let path = status.path.clone();
|
||||||
|
let branch = status.branch.clone();
|
||||||
// The push busy flag lives on the store; it disables the banner's triggers.
|
// The push busy flag lives on the store; it disables the banner's triggers.
|
||||||
let pushing = self.store.read(cx).pushing;
|
let pushing = self.store.read(cx).pushing;
|
||||||
|
|
||||||
@@ -98,7 +138,7 @@ impl RepoDetailView {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.font_family(cx.theme().mono_font_family.clone())
|
.font_family(cx.theme().mono_font_family.clone())
|
||||||
.child(status.branch),
|
.child(branch),
|
||||||
)
|
)
|
||||||
.child("has")
|
.child("has")
|
||||||
.child(
|
.child(
|
||||||
@@ -138,7 +178,7 @@ impl RepoDetailView {
|
|||||||
.ghost()
|
.ghost()
|
||||||
.disabled(pushing)
|
.disabled(pushing)
|
||||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
||||||
this.banner_dismissed.insert(key.clone());
|
this.banners.dismiss(&status);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
@@ -213,7 +253,8 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
pub(super) fn render_ready_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
pub(super) fn render_ready_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||||
let status = self.ready_suggestion(cx)?;
|
let status = self.ready_suggestion(cx)?;
|
||||||
let key = (status.path.clone(), status.branch.clone());
|
let branch = status.branch.clone();
|
||||||
|
let base = status.base.clone();
|
||||||
|
|
||||||
let commits = if status.ahead == 1 {
|
let commits = if status.ahead == 1 {
|
||||||
SharedString::from("1 commit")
|
SharedString::from("1 commit")
|
||||||
@@ -244,7 +285,7 @@ impl RepoDetailView {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.font_family(cx.theme().mono_font_family.clone())
|
.font_family(cx.theme().mono_font_family.clone())
|
||||||
.child(status.branch),
|
.child(branch),
|
||||||
)
|
)
|
||||||
.child("is")
|
.child("is")
|
||||||
.child(
|
.child(
|
||||||
@@ -270,7 +311,7 @@ impl RepoDetailView {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.font_family(cx.theme().mono_font_family.clone())
|
.font_family(cx.theme().mono_font_family.clone())
|
||||||
.child(status.base),
|
.child(base),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -298,7 +339,7 @@ impl RepoDetailView {
|
|||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
||||||
this.banner_dismissed.insert(key.clone());
|
this.banners.dismiss(&status);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
fn render_header_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_header_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let commits_count = self.history.read(cx).commit_count();
|
let commits_count = self.history.read(cx).commit_count();
|
||||||
let worktree_empty = self.switching_ref || self.worktree.is_none();
|
let worktree_empty = self.refs.switching_ref || self.worktree.is_none();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -580,7 +580,7 @@ impl RepoDetailView {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(120.)).child(
|
div().w(px(120.)).child(
|
||||||
Combobox::new(&self.branch_select)
|
Combobox::new(&self.refs.branch_select)
|
||||||
.placeholder("Branch")
|
.placeholder("Branch")
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.menu_width(px(200.))
|
.menu_width(px(200.))
|
||||||
@@ -594,7 +594,7 @@ impl RepoDetailView {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(120.)).child(
|
div().w(px(120.)).child(
|
||||||
Combobox::new(&self.tag_select)
|
Combobox::new(&self.refs.tag_select)
|
||||||
.placeholder("Tag")
|
.placeholder("Tag")
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.menu_width(px(200.))
|
.menu_width(px(200.))
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gix::Repository;
|
use gix::Repository;
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{Context, Entity, PathPromptOptions, SharedString, Window};
|
use gpui::{Context, PathPromptOptions, SharedString, Window};
|
||||||
use gpui_component::combobox::ComboboxState;
|
|
||||||
use gpui_component::searchable_list::SearchableVec;
|
|
||||||
use nostr::prelude::Url;
|
use nostr::prelude::Url;
|
||||||
use signed_git::FileCommit;
|
use signed_git::FileCommit;
|
||||||
use signed_state::GitStore;
|
use signed_state::GitStore;
|
||||||
@@ -185,25 +183,17 @@ impl RepoDetailView {
|
|||||||
let branches: Vec<SharedString> = branches.iter().map(Into::into).collect();
|
let branches: Vec<SharedString> = branches.iter().map(Into::into).collect();
|
||||||
let tags: Vec<SharedString> = tags.iter().map(Into::into).collect();
|
let tags: Vec<SharedString> = tags.iter().map(Into::into).collect();
|
||||||
|
|
||||||
let branches_changed = Self::sync_ref_selector(
|
let branches_changed = this.refs.set_branches(
|
||||||
&this.branch_select,
|
|
||||||
&mut this.ref_branches,
|
|
||||||
branches,
|
branches,
|
||||||
current_branch.map(Into::into),
|
current_branch.map(Into::into),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let tags_changed = Self::sync_ref_selector(
|
|
||||||
&this.tag_select,
|
|
||||||
&mut this.ref_tags,
|
|
||||||
tags,
|
|
||||||
None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
let tags_changed = this.refs.set_tags(tags, window, cx);
|
||||||
let new_head_commit = head_commit.as_ref().map(|c| &c.id);
|
let new_head_commit = head_commit.as_ref().map(|c| &c.id);
|
||||||
let current_head_commit = this.head_commit.as_ref().map(|c| &c.id);
|
let current_head_commit = this.head_commit.as_ref().map(|c| &c.id);
|
||||||
|
|
||||||
let head_changed = new_head_commit != current_head_commit;
|
let head_changed = new_head_commit != current_head_commit;
|
||||||
this.head_commit = head_commit;
|
this.head_commit = head_commit;
|
||||||
|
|
||||||
@@ -263,50 +253,10 @@ impl RepoDetailView {
|
|||||||
let branches: Vec<SharedString> = branches.into_iter().map(Into::into).collect();
|
let branches: Vec<SharedString> = branches.into_iter().map(Into::into).collect();
|
||||||
let tags: Vec<SharedString> = tags.into_iter().map(Into::into).collect();
|
let tags: Vec<SharedString> = tags.into_iter().map(Into::into).collect();
|
||||||
|
|
||||||
Self::sync_ref_selector(
|
self.refs
|
||||||
&self.branch_select,
|
.set_branches(branches, current_branch.map(Into::into), window, cx);
|
||||||
&mut self.ref_branches,
|
|
||||||
branches,
|
|
||||||
current_branch.map(Into::into),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::sync_ref_selector(&self.tag_select, &mut self.ref_tags, tags, None, window, cx);
|
self.refs.set_tags(tags, window, cx);
|
||||||
}
|
|
||||||
|
|
||||||
/// Point a ref selector at `items`, selecting `selected` when given.
|
|
||||||
fn sync_ref_selector(
|
|
||||||
select: &Entity<ComboboxState<SearchableVec<SharedString>>>,
|
|
||||||
cached: &mut Vec<SharedString>,
|
|
||||||
items: Vec<SharedString>,
|
|
||||||
selected: Option<SharedString>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> bool {
|
|
||||||
let items_changed = *cached != items;
|
|
||||||
|
|
||||||
let selection_changed = selected
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|value| select.read(cx).selected_value().as_ref() != Some(value));
|
|
||||||
|
|
||||||
if !items_changed && !selection_changed {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
select.update(cx, |state, cx| {
|
|
||||||
if items_changed {
|
|
||||||
state.set_items(SearchableVec::from(items.clone()), window, cx);
|
|
||||||
}
|
|
||||||
if let Some(value) = selected
|
|
||||||
&& (items_changed || selection_changed)
|
|
||||||
{
|
|
||||||
state.set_selected_values(std::slice::from_ref(&value), window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*cached = items;
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -378,14 +328,15 @@ fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
|
|||||||
let entries = signed_git::worktree_entries(repo)?;
|
let entries = signed_git::worktree_entries(repo)?;
|
||||||
let tree = build_tree_items(&entries);
|
let tree = build_tree_items(&entries);
|
||||||
let readme_path = signed_git::find_readme(repo)?;
|
let readme_path = signed_git::find_readme(repo)?;
|
||||||
|
|
||||||
let readme = match &readme_path {
|
let readme = match &readme_path {
|
||||||
Some(path) => signed_git::worktree_read(repo, path)?,
|
Some(path) => signed_git::worktree_read(repo, path)?,
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let worktree = repo.workdir().map(Path::to_path_buf);
|
let worktree = repo.workdir().map(Path::to_path_buf);
|
||||||
// Ref listing is auxiliary UI.
|
let head_commit = signed_git::head_commit(repo).unwrap_or(None);
|
||||||
// A broken ref must not prevent the explorer from loading.
|
|
||||||
// Failures degrade to empty selectors.
|
|
||||||
let (branches, tags, current_branch) = match &worktree {
|
let (branches, tags, current_branch) = match &worktree {
|
||||||
Some(_) => (
|
Some(_) => (
|
||||||
signed_git::repo_branches(repo).unwrap_or_default(),
|
signed_git::repo_branches(repo).unwrap_or_default(),
|
||||||
@@ -394,7 +345,6 @@ fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
|
|||||||
),
|
),
|
||||||
None => (Vec::new(), Vec::new(), None),
|
None => (Vec::new(), Vec::new(), None),
|
||||||
};
|
};
|
||||||
let head_commit = signed_git::head_commit(repo).unwrap_or(None);
|
|
||||||
|
|
||||||
Ok(RepoData {
|
Ok(RepoData {
|
||||||
tree,
|
tree,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
@@ -9,13 +8,12 @@ use gpui::{
|
|||||||
SharedString, Subscription, Task, WeakEntity, Window, div,
|
SharedString, Subscription, Task, WeakEntity, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_component::alert::Alert;
|
use gpui_component::alert::Alert;
|
||||||
use gpui_component::combobox::{ComboboxEvent, ComboboxState};
|
use gpui_component::combobox::ComboboxEvent;
|
||||||
use gpui_component::searchable_list::SearchableVec;
|
|
||||||
use gpui_component::spinner::Spinner;
|
use gpui_component::spinner::Spinner;
|
||||||
use gpui_component::{ActiveTheme, Sizable, v_flex};
|
use gpui_component::{ActiveTheme, Sizable, v_flex};
|
||||||
use signed_core::{Announcement, RepoAddr};
|
use signed_core::{Announcement, RepoAddr};
|
||||||
use signed_git::FileCommit;
|
use signed_git::FileCommit;
|
||||||
use signed_state::{CheckoutStatus, CheckoutsStore, RepoStore};
|
use signed_state::{CheckoutsStore, RepoStore};
|
||||||
|
|
||||||
mod about;
|
mod about;
|
||||||
mod actions;
|
mod actions;
|
||||||
@@ -30,8 +28,10 @@ mod store;
|
|||||||
|
|
||||||
pub(crate) use actions::{RepoItem, open_repo_item, open_repo_panel};
|
pub(crate) use actions::{RepoItem, open_repo_item, open_repo_panel};
|
||||||
|
|
||||||
|
use self::banners::Banners;
|
||||||
use self::files::RepoFilesView;
|
use self::files::RepoFilesView;
|
||||||
use self::history::RepoHistoryView;
|
use self::history::RepoHistoryView;
|
||||||
|
use self::refs::RefSwitcher;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
enum RefKind {
|
enum RefKind {
|
||||||
@@ -41,10 +41,6 @@ enum RefKind {
|
|||||||
Tag,
|
Tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Header actions dispatched by the header dropdown menus.
|
|
||||||
///
|
|
||||||
/// `pub(crate)` because the pull-request list panel shares this action set,
|
|
||||||
/// offering the New-PR and Send-patch actions in its own dropdown.
|
|
||||||
#[derive(Clone, Action, PartialEq, Eq)]
|
#[derive(Clone, Action, PartialEq, Eq)]
|
||||||
#[action(namespace = repo, no_json)]
|
#[action(namespace = repo, no_json)]
|
||||||
pub(crate) enum RepoAction {
|
pub(crate) enum RepoAction {
|
||||||
@@ -74,38 +70,15 @@ pub struct RepoDetailView {
|
|||||||
loading: bool,
|
loading: bool,
|
||||||
error: Option<SharedString>,
|
error: Option<SharedString>,
|
||||||
head_commit: Option<FileCommit>,
|
head_commit: Option<FileCommit>,
|
||||||
branch_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
|
refs: RefSwitcher,
|
||||||
tag_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
|
|
||||||
/// Branch names currently in `branch_select`, for cheap no-op detection.
|
|
||||||
ref_branches: Vec<SharedString>,
|
|
||||||
/// Tag names currently in `tag_select`, for cheap no-op detection.
|
|
||||||
ref_tags: Vec<SharedString>,
|
|
||||||
switching_ref: bool,
|
|
||||||
/// In-flight loads with an older generation are discarded when they complete.
|
/// In-flight loads with an older generation are discarded when they complete.
|
||||||
ref_generation: u64,
|
ref_generation: u64,
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
/// `(path, branch)` ready-suggestions dismissed by the user, per panel.
|
banners: Banners,
|
||||||
banner_dismissed: HashSet<(PathBuf, String)>,
|
|
||||||
/// Whether the ready statuses were requested at all.
|
|
||||||
ready_requested: bool,
|
|
||||||
/// The announced HEAD they were last requested with. Re-requested only when
|
|
||||||
/// the HEAD, the base default, changes, e.g. when the store's first refresh
|
|
||||||
/// lands.
|
|
||||||
ready_head: Option<String>,
|
|
||||||
/// The global checkouts store's ready-to-contribute statuses of this
|
|
||||||
/// repository, last seen when they drove a render.
|
|
||||||
///
|
|
||||||
/// The store notifies on any recompute pass; the observer re-renders this
|
|
||||||
/// panel only when these slices changed.
|
|
||||||
ready_statuses: Vec<CheckoutStatus>,
|
|
||||||
push_statuses: Vec<CheckoutStatus>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepoDetailView {
|
impl RepoDetailView {
|
||||||
/// `hint` is an announcement already in hand. It seeds the store's relays
|
|
||||||
/// and the explorer's clone URLs; without it the panel waits for the store
|
|
||||||
/// to load the announcement from the local database.
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
dock_area: WeakEntity<DockArea>,
|
dock_area: WeakEntity<DockArea>,
|
||||||
addr: RepoAddr,
|
addr: RepoAddr,
|
||||||
@@ -136,42 +109,31 @@ impl RepoDetailView {
|
|||||||
let checkouts = CheckoutsStore::global(cx);
|
let checkouts = CheckoutsStore::global(cx);
|
||||||
let files = cx.new(RepoFilesView::new);
|
let files = cx.new(RepoFilesView::new);
|
||||||
let history = cx.new(|_cx| RepoHistoryView::new(store.clone(), dock_area.clone()));
|
let history = cx.new(|_cx| RepoHistoryView::new(store.clone(), dock_area.clone()));
|
||||||
|
let refs = RefSwitcher::new(window, cx);
|
||||||
let branch_select = cx.new(|cx| {
|
|
||||||
ComboboxState::new(
|
|
||||||
SearchableVec::new(Vec::<SharedString>::new()),
|
|
||||||
Vec::new(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.searchable(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
let tag_select = cx.new(|cx| {
|
|
||||||
ComboboxState::new(
|
|
||||||
SearchableVec::new(Vec::<SharedString>::new()),
|
|
||||||
Vec::new(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.searchable(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut subscriptions = vec![
|
let mut subscriptions = vec![
|
||||||
cx.subscribe_in(&branch_select, window, |this, _state, event, window, cx| {
|
cx.subscribe_in(
|
||||||
if let ComboboxEvent::Change(values) = event
|
&refs.branch_select,
|
||||||
&& let Some(name) = values.first()
|
window,
|
||||||
{
|
|this, _state, event, window, cx| {
|
||||||
this.switch_ref(RefKind::Branch, name.clone(), window, cx);
|
if let ComboboxEvent::Change(values) = event
|
||||||
}
|
&& let Some(name) = values.first()
|
||||||
}),
|
{
|
||||||
cx.subscribe_in(&tag_select, window, |this, _state, event, window, cx| {
|
this.switch_ref(RefKind::Branch, name.clone(), window, cx);
|
||||||
if let ComboboxEvent::Change(values) = event
|
}
|
||||||
&& let Some(name) = values.first()
|
},
|
||||||
{
|
),
|
||||||
this.switch_ref(RefKind::Tag, name.clone(), window, cx);
|
cx.subscribe_in(
|
||||||
}
|
&refs.tag_select,
|
||||||
}),
|
window,
|
||||||
|
|this, _state, event, window, cx| {
|
||||||
|
if let ComboboxEvent::Change(values) = event
|
||||||
|
&& let Some(name) = values.first()
|
||||||
|
{
|
||||||
|
this.switch_ref(RefKind::Tag, name.clone(), window, cx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// The ready-to-contribute and ready-to-push banners are driven by the global checkouts store.
|
// The ready-to-contribute and ready-to-push banners are driven by the global checkouts store.
|
||||||
@@ -197,18 +159,10 @@ impl RepoDetailView {
|
|||||||
loading: true,
|
loading: true,
|
||||||
error: None,
|
error: None,
|
||||||
head_commit: None,
|
head_commit: None,
|
||||||
branch_select,
|
refs,
|
||||||
tag_select,
|
|
||||||
ref_branches: Vec::new(),
|
|
||||||
ref_tags: Vec::new(),
|
|
||||||
switching_ref: false,
|
|
||||||
ref_generation: 0,
|
ref_generation: 0,
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
banner_dismissed: HashSet::new(),
|
banners: Banners::default(),
|
||||||
ready_requested: false,
|
|
||||||
ready_head: None,
|
|
||||||
ready_statuses: Vec::new(),
|
|
||||||
push_statuses: Vec::new(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,123 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{Context, Entity, SharedString, Window};
|
use gpui::{App, Context, Entity, SharedString, Window};
|
||||||
use gpui_component::combobox::ComboboxState;
|
use gpui_component::combobox::ComboboxState;
|
||||||
use gpui_component::searchable_list::SearchableVec;
|
use gpui_component::searchable_list::SearchableVec;
|
||||||
|
|
||||||
use super::{RefKind, RepoDetailView};
|
use super::{RefKind, RepoDetailView};
|
||||||
use crate::views::tree::{build_tree_items, sorted_worktree_paths};
|
use crate::views::tree::{build_tree_items, sorted_worktree_paths};
|
||||||
|
|
||||||
|
pub(super) struct RefSwitcher {
|
||||||
|
pub(super) branch_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
|
||||||
|
pub(super) tag_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
|
||||||
|
ref_branches: Vec<SharedString>,
|
||||||
|
ref_tags: Vec<SharedString>,
|
||||||
|
pub(super) switching_ref: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RefSwitcher {
|
||||||
|
pub(super) fn new(window: &mut Window, cx: &mut App) -> Self {
|
||||||
|
let branch_select = cx.new(|cx| {
|
||||||
|
ComboboxState::new(
|
||||||
|
SearchableVec::new(Vec::<SharedString>::new()),
|
||||||
|
Vec::new(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.searchable(true)
|
||||||
|
});
|
||||||
|
let tag_select = cx.new(|cx| {
|
||||||
|
ComboboxState::new(
|
||||||
|
SearchableVec::new(Vec::<SharedString>::new()),
|
||||||
|
Vec::new(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.searchable(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
branch_select,
|
||||||
|
tag_select,
|
||||||
|
ref_branches: Vec::new(),
|
||||||
|
ref_tags: Vec::new(),
|
||||||
|
switching_ref: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_branches(
|
||||||
|
&mut self,
|
||||||
|
branches: Vec<SharedString>,
|
||||||
|
selected: Option<SharedString>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> bool {
|
||||||
|
sync_selector(
|
||||||
|
&self.branch_select,
|
||||||
|
&mut self.ref_branches,
|
||||||
|
branches,
|
||||||
|
selected,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_tags(
|
||||||
|
&mut self,
|
||||||
|
tags: Vec<SharedString>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> bool {
|
||||||
|
sync_selector(&self.tag_select, &mut self.ref_tags, tags, None, window, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_selection(
|
||||||
|
&self,
|
||||||
|
select: &Entity<ComboboxState<SearchableVec<SharedString>>>,
|
||||||
|
previous: &Option<SharedString>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
select.update(cx, |state, cx| match previous {
|
||||||
|
Some(value) => state.set_selected_values(std::slice::from_ref(value), window, cx),
|
||||||
|
None => state.clear_selection(cx),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_selector(
|
||||||
|
select: &Entity<ComboboxState<SearchableVec<SharedString>>>,
|
||||||
|
cached: &mut Vec<SharedString>,
|
||||||
|
items: Vec<SharedString>,
|
||||||
|
selected: Option<SharedString>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> bool {
|
||||||
|
let items_changed = *cached != items;
|
||||||
|
|
||||||
|
let selection_changed = selected
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|value| select.read(cx).selected_value().as_ref() != Some(value));
|
||||||
|
|
||||||
|
if !items_changed && !selection_changed {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.update(cx, |state, cx| {
|
||||||
|
if items_changed {
|
||||||
|
state.set_items(SearchableVec::from(items.clone()), window, cx);
|
||||||
|
}
|
||||||
|
if let Some(value) = selected
|
||||||
|
&& (items_changed || selection_changed)
|
||||||
|
{
|
||||||
|
state.set_selected_values(std::slice::from_ref(&value), window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*cached = items;
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
impl RepoDetailView {
|
impl RepoDetailView {
|
||||||
pub(super) fn switch_ref<T>(
|
pub(super) fn switch_ref<T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -17,7 +128,7 @@ impl RepoDetailView {
|
|||||||
) where
|
) where
|
||||||
T: Into<SharedString>,
|
T: Into<SharedString>,
|
||||||
{
|
{
|
||||||
if self.switching_ref {
|
if self.refs.switching_ref {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,21 +137,23 @@ impl RepoDetailView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
let previous_branch = self.branch_select.read(cx).selected_value();
|
let previous_branch = self.refs.branch_select.read(cx).selected_value();
|
||||||
let previous_tag = self.tag_select.read(cx).selected_value();
|
let previous_tag = self.refs.tag_select.read(cx).selected_value();
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
RefKind::Branch => {
|
RefKind::Branch => {
|
||||||
self.tag_select
|
self.refs
|
||||||
|
.tag_select
|
||||||
.update(cx, |state, cx| state.clear_selection(cx));
|
.update(cx, |state, cx| state.clear_selection(cx));
|
||||||
}
|
}
|
||||||
RefKind::Tag => {
|
RefKind::Tag => {
|
||||||
self.branch_select
|
self.refs
|
||||||
|
.branch_select
|
||||||
.update(cx, |state, cx| state.clear_selection(cx));
|
.update(cx, |state, cx| state.clear_selection(cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.switching_ref = true;
|
self.refs.switching_ref = true;
|
||||||
self.ref_generation += 1;
|
self.ref_generation += 1;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
@@ -64,9 +177,19 @@ impl RepoDetailView {
|
|||||||
Ok(()) => this.reload_worktree(cx),
|
Ok(()) => this.reload_worktree(cx),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
this.error = Some(format!("Failed to check out {name}: {error}").into());
|
this.error = Some(format!("Failed to check out {name}: {error}").into());
|
||||||
this.switching_ref = false;
|
this.refs.switching_ref = false;
|
||||||
this.restore_selection(&this.branch_select, &previous_branch, window, cx);
|
this.refs.restore_selection(
|
||||||
this.restore_selection(&this.tag_select, &previous_tag, window, cx);
|
&this.refs.branch_select,
|
||||||
|
&previous_branch,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
this.refs.restore_selection(
|
||||||
|
&this.refs.tag_select,
|
||||||
|
&previous_tag,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -78,19 +201,6 @@ impl RepoDetailView {
|
|||||||
self.tasks.push(task);
|
self.tasks.push(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_selection(
|
|
||||||
&self,
|
|
||||||
select: &Entity<ComboboxState<SearchableVec<SharedString>>>,
|
|
||||||
previous: &Option<SharedString>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
select.update(cx, |state, cx| match previous {
|
|
||||||
Some(value) => state.set_selected_values(std::slice::from_ref(value), window, cx),
|
|
||||||
None => state.clear_selection(cx),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reload_worktree(&mut self, cx: &mut Context<Self>) {
|
fn reload_worktree(&mut self, cx: &mut Context<Self>) {
|
||||||
let Some(worktree) = self.worktree.clone() else {
|
let Some(worktree) = self.worktree.clone() else {
|
||||||
return;
|
return;
|
||||||
@@ -108,7 +218,8 @@ impl RepoDetailView {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.switching_ref = false;
|
this.refs.switching_ref = false;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok((snapshot, tree, paths)) => {
|
Ok((snapshot, tree, paths)) => {
|
||||||
this.head_commit = snapshot.head_commit;
|
this.head_commit = snapshot.head_commit;
|
||||||
@@ -130,6 +241,7 @@ impl RepoDetailView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ impl RepoDetailView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let head = self.store.read(cx).head.clone();
|
let head = self.store.read(cx).head.clone();
|
||||||
|
let (requested, requested_head) = self.banners.ready_requested_at();
|
||||||
|
|
||||||
if self.ready_requested && self.ready_head == head {
|
if requested && requested_head == &head {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ready_requested = true;
|
self.banners.mark_ready_requested(head.clone());
|
||||||
self.ready_head = head.clone();
|
|
||||||
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
let checkout = CheckoutsStore::global(cx);
|
let checkout = CheckoutsStore::global(cx);
|
||||||
@@ -83,9 +83,6 @@ impl RepoDetailView {
|
|||||||
let ready_statuses = checkouts.ready_statuses_of(&addr);
|
let ready_statuses = checkouts.ready_statuses_of(&addr);
|
||||||
let push_statuses = checkouts.push_statuses_of(&addr);
|
let push_statuses = checkouts.push_statuses_of(&addr);
|
||||||
|
|
||||||
let changed = ready_statuses != self.ready_statuses || push_statuses != self.push_statuses;
|
self.banners.set_statuses(ready_statuses, push_statuses)
|
||||||
self.ready_statuses = ready_statuses;
|
|
||||||
self.push_statuses = push_statuses;
|
|
||||||
changed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user