clean up
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run

This commit is contained in:
2026-11-12 10:36:53 +07:00
parent 4a871e417a
commit a27eca45c8
26 changed files with 1566 additions and 1352 deletions
+3 -1
View File
@@ -128,7 +128,9 @@ impl Render for OnboardingDialog {
.loading(busy)
.disabled(busy)
.on_click(move |_, _, cx| {
let _ = this.update(cx, |this, cx| this.create_identity(cx));
crate::shared::log_dropped(
this.update(cx, |this, cx| this.create_identity(cx)),
);
})
.into_any_element();
+1 -1
View File
@@ -123,7 +123,7 @@ impl Render for PassphraseDialog {
.loading(busy)
.disabled(busy)
.on_click(move |_, _, cx| {
let _ = this.update(cx, |this, cx| this.unlock(cx));
crate::shared::log_dropped(this.update(cx, |this, cx| this.unlock(cx)));
})
.into_any_element();
+2 -2
View File
@@ -13,7 +13,7 @@ use metadata::ProfileDirectory;
use nostr::prelude::{Event, Kind, PublicKey};
use utils::relative_time;
pub fn repo_header(name: SharedString, unread: usize) -> AnyElement {
pub fn repo_header(id: SharedString, name: SharedString, unread: usize) -> AnyElement {
h_flex()
.h_12()
.w_full()
@@ -27,7 +27,7 @@ pub fn repo_header(name: SharedString, unread: usize) -> AnyElement {
.child(name),
)
.when(unread > 0, |this| {
this.child(Badge::new("inbox-unread", unread.to_string()))
this.child(Badge::new(id.clone(), unread.to_string()))
})
.into_any_element()
}
+36 -27
View File
@@ -27,13 +27,6 @@ use crate::repo::actions::{RepoItem, open_repo_item};
const MAX_SUB_ACTIVITIES: usize = 5;
struct InboxSection {
address: Option<RepoAddr>,
unread: usize,
entries: Vec<usize>,
latest: Timestamp,
}
#[derive(Clone, Copy)]
enum InboxRow {
Repo(usize),
@@ -41,12 +34,18 @@ enum InboxRow {
Empty,
}
struct InboxSection {
address: Option<RepoAddr>,
unread: usize,
entries: Vec<usize>,
latest: Timestamp,
}
pub struct InboxView {
dock: WeakEntity<DockState>,
threads: Arc<Vec<InboxItem>>,
sections: Arc<Vec<InboxSection>>,
rows: Arc<Vec<InboxRow>>,
unread_count: usize,
state: InboxReadState,
state_loaded: bool,
refresh: RefreshGate,
@@ -89,7 +88,6 @@ impl InboxView {
threads: Arc::new(Vec::new()),
sections: Arc::new(Vec::new()),
rows: Arc::new(Vec::new()),
unread_count: 0,
state: InboxReadState::default(),
state_loaded: false,
refresh: RefreshGate::default(),
@@ -182,7 +180,7 @@ impl InboxView {
let work = cx.background_spawn(async move { Inbox::query(&client, me, &state).await });
self.tasks.push(cx.spawn(async move |this, cx| {
let (threads, unread_count) = match work.await {
let (threads, _unread_count) = match work.await {
Ok(results) => results,
Err(error) => {
log::warn!("inbox refresh failed: {error}");
@@ -196,7 +194,6 @@ impl InboxView {
return false;
}
this.threads = Arc::new(threads);
this.unread_count = unread_count;
this.rebuild(cx);
cx.notify();
this.refresh.finish()
@@ -214,7 +211,6 @@ impl InboxView {
for item in items.iter_mut() {
item.apply_state(&self.state);
}
self.unread_count = items.iter().filter(|item| item.is_unread()).count();
self.threads = Arc::new(items);
self.rebuild(cx);
}
@@ -310,7 +306,6 @@ impl InboxView {
self.threads = Arc::new(Vec::new());
self.sections = Arc::new(Vec::new());
self.rows = Arc::new(Vec::new());
self.unread_count = 0;
self.state = InboxReadState::default();
self.state_loaded = false;
self.refresh = RefreshGate::default();
@@ -348,7 +343,15 @@ impl InboxView {
return div().into_any_element();
};
let name = repo_name(section.address.as_ref(), cx);
repo_header(name, section.unread)
let id = SharedString::from(format!(
"inbox-unread:{}",
section
.address
.as_ref()
.map(ToString::to_string)
.unwrap_or_default()
));
repo_header(id, name, section.unread)
}
InboxRow::Entry(section_ix, entry_ix) => {
let Some(section) = self.sections.get(section_ix) else {
@@ -389,19 +392,6 @@ impl InboxView {
}
}
fn repo_name(addr: Option<&RepoAddr>, cx: &App) -> SharedString {
let Some(addr) = addr else {
return SharedString::from("Untitled");
};
RepoListStore::global(cx)
.read(cx)
.announcements
.iter()
.find(|announcement| announcement.addr() == *addr)
.and_then(|announcement| announcement.name.as_deref().map(SharedString::from))
.unwrap_or_else(|| SharedString::from("Untitled"))
}
impl Panel for InboxView {
fn title(&self, _cx: &App) -> Option<SharedString> {
Some(SharedString::from("Inbox"))
@@ -465,3 +455,22 @@ impl Render for InboxView {
.into_any_element()
}
}
fn repo_name(addr: Option<&RepoAddr>, cx: &App) -> SharedString {
let Some(addr) = addr else {
return SharedString::from("Untitled");
};
RepoListStore::global(cx)
.read(cx)
.announcements
.iter()
.find(|announcement| announcement.addr() == *addr)
.map(|announcement| {
announcement
.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
})
.unwrap_or_else(|| SharedString::from("Untitled"))
}
+1 -1
View File
@@ -40,7 +40,7 @@ impl IssueDetailView {
let directory = ProfileDirectory::new(cx);
let weak = cx.entity().downgrade();
cx.defer(move |cx| {
let _ = weak.update(cx, |this, cx| this.track_participants(cx));
crate::shared::log_dropped(weak.update(cx, |this, cx| this.track_participants(cx)));
});
Self {
+1 -1
View File
@@ -60,7 +60,7 @@ impl IssuesView {
});
let weak = cx.entity().downgrade();
cx.defer(move |cx| {
let _ = weak.update(cx, |this, cx| this.rebuild(cx));
crate::shared::log_dropped(weak.update(cx, |this, cx| this.rebuild(cx)));
});
Self {
+1 -1
View File
@@ -83,7 +83,7 @@ impl Render for NewIssueDialog {
Button::new("create-issue")
.label("Create issue")
.on_click(move |_, _, cx| {
let _ = this.update(cx, |this, cx| this.submit(cx));
crate::shared::log_dropped(this.update(cx, |this, cx| this.submit(cx)));
})
.into_any_element() as AnyElement,
]
@@ -85,7 +85,7 @@ impl PullRequestDetailView {
});
cx.defer(move |cx| {
let _ = weak.update(cx, |this, cx| this.sync(cx));
crate::shared::log_dropped(weak.update(cx, |this, cx| this.sync(cx)));
});
Self {
@@ -291,7 +291,6 @@ impl PullRequestDetailView {
None => SharedString::from(format!("commit:{}:{commit_id}", worktree.display())),
};
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
let repo_name = self.repo_name.clone();
let commit_id = commit_id.to_owned();
@@ -313,7 +312,6 @@ impl PullRequestDetailView {
window,
cx,
);
let _ = dock_for_build;
}
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
@@ -339,10 +337,10 @@ impl PullRequestDetailView {
]
})
.on_change(move |index, _, cx| {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
this.active_tab = index;
cx.notify();
});
}));
})
.into_any_element()
}
@@ -476,9 +474,9 @@ impl PullRequestDetailView {
&commit,
move |window, cx| {
if let Some(worktree) = worktree.clone() {
let _ = view.update(cx, |this, cx| {
crate::shared::log_dropped(view.update(cx, |this, cx| {
this.open_commit_diff(worktree, &id, window, cx);
});
}));
}
},
cx,
@@ -555,7 +553,7 @@ impl PullRequestDetailView {
.when(can_update, |this| {
this.child(Button::new("update-pr").label("Update").small().on_click(
move |_, _, cx| {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
let root = this
.store
.read(cx)
@@ -568,7 +566,7 @@ impl PullRequestDetailView {
if let Some(root) = root {
dialogs::update::open(this.store.clone(), root, cx);
}
});
}));
},
))
})
@@ -13,10 +13,6 @@ use signed_state::RepoStore;
use crate::shared::dialogs::{close_global, open_global};
pub fn open(store: Entity<RepoStore>, root: Event, cx: &mut App) {
open_global(UpdatePrDialog::new(store, root, cx), cx);
}
pub struct UpdatePrDialog {
store: Entity<RepoStore>,
root: Event,
@@ -76,10 +72,14 @@ impl Render for UpdatePrDialog {
Button::new("submit-update")
.label("Update pull request")
.on_click(move |_, _, cx| {
let _ = this.update(cx, |this, cx| this.submit(cx));
crate::shared::log_dropped(this.update(cx, |this, cx| this.submit(cx)));
})
.into_any_element() as AnyElement,
]
})
}
}
pub fn open(store: Entity<RepoStore>, root: Event, cx: &mut App) {
open_global(UpdatePrDialog::new(store, root, cx), cx);
}
+88 -16
View File
@@ -1,5 +1,12 @@
use gpui::SharedString;
use signed_core::RepoStatus;
use std::path::PathBuf;
use gpui::{SharedString, WeakEntity};
use gpui_native_kit::menu::MenuItem;
use nostr::prelude::PublicKey;
use signed_core::{Announcement, RepoAddr, RepoStatus};
use utils::middle_truncate;
use super::NewPullRequestView;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PullRequestFilter {
@@ -32,20 +39,6 @@ impl PullRequestFilter {
}
}
pub fn pr_label(repo_name: &SharedString, pr_id: &str) -> SharedString {
if repo_name.is_empty() {
SharedString::from(pr_id)
} else {
SharedString::from(format!("{repo_name}/{pr_id}"))
}
}
use std::path::PathBuf;
use nostr::prelude::PublicKey;
use signed_core::Announcement;
use utils::middle_truncate;
pub struct ForkCompare {
pub announcement: Announcement,
pub namespace: String,
@@ -62,6 +55,14 @@ impl ForkCompare {
}
}
pub fn pr_label(repo_name: &SharedString, pr_id: &str) -> SharedString {
if repo_name.is_empty() {
SharedString::from(pr_id)
} else {
SharedString::from(format!("{repo_name}/{pr_id}"))
}
}
pub fn fork_display_name(announcement: &Announcement) -> SharedString {
announcement
.name
@@ -77,3 +78,74 @@ pub fn shorten_owner(owner: &PublicKey) -> String {
pub fn truncate_label(label: &str) -> SharedString {
SharedString::from(middle_truncate(label, 18, 0))
}
pub fn source_menu_items(
view: WeakEntity<NewPullRequestView>,
associated: Vec<PathBuf>,
active_path: Option<PathBuf>,
candidates: Vec<Announcement>,
user: Option<PublicKey>,
active_fork: Option<RepoAddr>,
) -> Vec<MenuItem> {
let mut items = Vec::new();
for path in associated {
let label = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string());
let target = view.clone();
let path_for_action = path.clone();
items.push(
MenuItem::new(truncate_label(&label))
.checked(active_path.as_ref() == Some(&path))
.handler(move |window, cx| {
let path = path_for_action.clone();
if let Err(error) =
target.update(cx, |this, cx| this.apply_folder_path(path, window, cx))
{
log::warn!("failed to apply checkout: {error}");
}
}),
);
}
let folder_view = view.clone();
items.push(
MenuItem::new("Choose another folder…").handler(move |window, cx| {
if let Err(error) = folder_view.update(cx, |this, cx| this.choose_checkout(window, cx))
{
log::warn!("failed to choose checkout: {error}");
}
}),
);
items.push(MenuItem::separator());
if candidates.is_empty() {
items.push(MenuItem::new("No announced forks of this repository").disabled(true));
} else {
for candidate in candidates {
let subtitle = if Some(candidate.owner) == user {
SharedString::from("Your fork")
} else {
SharedString::from(format!("by {}", shorten_owner(&candidate.owner)))
};
let title = format!("{} — {}", fork_display_name(&candidate), subtitle);
let target = view.clone();
let checked = active_fork == Some(candidate.addr());
let announcement = candidate;
items.push(
MenuItem::new(truncate_label(&title))
.checked(checked)
.handler(move |window, cx| {
let announcement = announcement.clone();
if let Err(error) =
target.update(cx, |this, cx| this.choose_fork(announcement, window, cx))
{
log::warn!("failed to choose fork: {error}");
}
}),
);
}
}
items
}
+6 -6
View File
@@ -64,9 +64,9 @@ impl PullRequestsView {
});
cx.defer(move |cx| {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
this.rebuild(cx);
});
}));
});
Self {
@@ -255,18 +255,18 @@ impl PullRequestsView {
}
})
.on_open_change(move |open, _, cx| {
let _ = menu_open_weak.update(cx, |this, cx| {
crate::shared::log_dropped(menu_open_weak.update(cx, |this, cx| {
this.new_pr_menu_open = open;
cx.notify();
});
}));
})
.items(move |_, _| {
vec![MenuItem::new("Send Patch").handler({
let weak = patch_weak.clone();
move |window, cx| {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
this.open_send_patch(window, cx);
});
}));
}
})]
}),
+70 -461
View File
@@ -1,11 +1,11 @@
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use assets::CustomIconName;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _, Subscription,
UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list,
ParentElement as _, Render, SharedString, Styled as _, Subscription, UniformListScrollHandle,
WeakEntity, Window, div, px, uniform_list,
};
use gpui_native_kit::button::Button;
use gpui_native_kit::combobox::{Combobox, ComboboxState};
@@ -14,45 +14,45 @@ use gpui_native_kit::data::tabs::{Tab, Tabs};
use gpui_native_kit::icons::{Icon, IconRole};
use gpui_native_kit::input::{Input, InputState};
use gpui_native_kit::layout::dock::{DockState, Panel};
use gpui_native_kit::menu::{Menu, MenuItem};
use gpui_native_kit::menu::Menu;
use gpui_native_kit::scrollbar::Scrollbar;
use gpui_native_kit::spinner::Spinner;
use gpui_native_kit::theme::ActiveTheme as _;
use metadata::ProfileDirectory;
use nostr::prelude::Url;
use signed_core::Announcement;
use signed_git::{GitCache, Repo};
use signed_state::{Backend, CheckoutsStore, Mirrors, RepoListStore, RepoStore};
use signed_state::{CheckoutsStore, RepoStore};
use super::helpers::{ForkCompare, fork_display_name, shorten_owner, truncate_label};
use super::helpers::{ForkCompare, fork_display_name, truncate_label};
use crate::repo::commit_diff::{CommitDiffView, DiffPane};
use crate::repo::helpers::commit_row;
use crate::shared::widgets::empty_state;
mod source;
pub struct NewPullRequestView {
dock: WeakEntity<DockState>,
panel_id: SharedString,
store: Entity<RepoStore>,
pub(super) store: Entity<RepoStore>,
repo_name: SharedString,
repo_path: Option<PathBuf>,
branches: Vec<SharedString>,
fork: Option<ForkCompare>,
base: SharedString,
compare: SharedString,
base_select: Entity<ComboboxState<SharedString>>,
compare_select: Entity<ComboboxState<SharedString>>,
pub(super) repo_path: Option<PathBuf>,
pub(super) branches: Vec<SharedString>,
pub(super) fork: Option<ForkCompare>,
pub(super) base: SharedString,
pub(super) compare: SharedString,
pub(super) base_select: Entity<ComboboxState<SharedString>>,
pub(super) compare_select: Entity<ComboboxState<SharedString>>,
subject: Entity<InputState>,
description: Entity<InputState>,
merge_base: Option<String>,
commits: Option<Vec<signed_git::FileCommit>>,
loading: bool,
error: Option<SharedString>,
pub(super) merge_base: Option<String>,
pub(super) commits: Option<Vec<signed_git::FileCommit>>,
pub(super) loading: bool,
pub(super) error: Option<SharedString>,
submitting: bool,
compare_generation: u64,
pub(super) compare_generation: u64,
pub(super) fork_generation: u64,
active_tab: usize,
pane: Entity<DiffPane>,
pub(super) pane: Entity<DiffPane>,
scroll_handle: UniformListScrollHandle,
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
pub(super) tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
directory: Entity<ProfileDirectory>,
source_open: bool,
_subscriptions: Vec<Subscription>,
@@ -80,10 +80,13 @@ impl NewPullRequestView {
let weak = weak.clone();
move |value, _window, cx| {
if let Some(base) = value {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
if this.base == base {
return;
}
this.base = base;
this.reload_compare(cx);
});
}));
}
}
}),
@@ -98,10 +101,13 @@ impl NewPullRequestView {
let weak = weak.clone();
move |value, _window, cx| {
if let Some(compare) = value {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
if this.compare == compare {
return;
}
this.compare = compare;
this.reload_compare(cx);
});
}));
}
}
}),
@@ -115,7 +121,7 @@ impl NewPullRequestView {
}
window.defer(cx, move |window, cx| {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
let Some(addr) = this.store.read(cx).addr().cloned() else {
return;
};
@@ -130,7 +136,7 @@ impl NewPullRequestView {
};
this.apply_folder_path(path, window, cx);
});
}));
});
Self {
@@ -153,6 +159,7 @@ impl NewPullRequestView {
error: None,
submitting: false,
compare_generation: 0,
fork_generation: 0,
active_tab: 0,
pane,
scroll_handle: UniformListScrollHandle::new(),
@@ -163,363 +170,6 @@ impl NewPullRequestView {
}
}
fn has_source(&self) -> bool {
self.repo_path.is_some() || self.fork.is_some()
}
fn work_path(&self) -> Option<PathBuf> {
match &self.fork {
Some(fork) => Some(fork.mirror_path.clone()),
None => self.repo_path.clone(),
}
}
fn base_ref(&self) -> String {
match &self.fork {
Some(_) => ForkCompare::base_ref(&self.base),
None => self.base.to_string(),
}
}
fn compare_ref(&self) -> String {
match &self.fork {
Some(fork) => fork.compare_ref(&self.compare),
None => self.compare.to_string(),
}
}
fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Choose local checkout".into()),
});
let task: gpui::Task<Result<(), anyhow::Error>> =
cx.spawn_in(window, async move |this, cx| {
let picked = match prompt.await {
Ok(Ok(Some(mut paths))) => paths.pop(),
_ => None,
};
let Some(path) = picked else {
return Ok(());
};
this.update_in(cx, |this, window, cx| {
this.apply_folder_path(path, window, cx)
})?;
Ok(())
});
self.tasks.push(task);
}
fn apply_folder_path(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) {
let path_string = path.to_string_lossy().to_string();
let info = Repo::try_open(&path).and_then(|repo| {
let branches = repo.branches().ok()?;
let current = repo.current_branch();
Some((branches, current))
});
self.apply_checkout(path_string, info, window, cx);
}
fn apply_checkout(
&mut self,
path: String,
info: Option<(Vec<String>, Option<String>)>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.fork = None;
let Some((branches, current)) = info else {
self.error = Some("The chosen folder is not a git repository".into());
self.repo_path = None;
self.branches.clear();
self.merge_base = None;
self.commits = None;
self.pane.update(cx, |pane, cx| pane.clear(cx));
cx.notify();
return;
};
if branches.is_empty() {
self.error = Some("The repository has no branches yet".into());
self.repo_path = None;
self.branches.clear();
cx.notify();
return;
}
let announced = self.store.read(cx).head.clone();
let base = announced
.as_ref()
.filter(|branch| branches.contains(branch))
.cloned()
.or_else(|| branches.iter().find(|branch| *branch == "main").cloned())
.unwrap_or_else(|| branches[0].clone());
let compare = current
.filter(|branch| branches.contains(branch))
.unwrap_or_else(|| base.clone());
self.repo_path = Some(PathBuf::from(&path));
self.error = None;
self.branches = branches.into_iter().map(SharedString::from).collect();
if let Some(addr) = self.store.read(cx).addr().cloned() {
CheckoutsStore::global(cx).update(cx, |store, cx| {
store.record(PathBuf::from(&path), addr, cx);
});
}
self.base = SharedString::from(base);
self.compare = SharedString::from(compare);
self.sync_selectors(window, cx);
self.reload_compare(cx);
}
fn sync_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let options: Vec<(SharedString, SharedString)> = self
.branches
.iter()
.map(|branch| (branch.clone(), branch.clone()))
.collect();
let base = self.base.clone();
let compare = self.compare.clone();
self.base_select.update(cx, |state, cx| {
state.set_options(options.clone(), cx);
state.set_selected(Some(base), window, cx);
});
self.compare_select.update(cx, |state, cx| {
state.set_options(options, cx);
state.set_selected(Some(compare), window, cx);
});
}
fn fork_candidates(&self, cx: &App) -> Vec<Announcement> {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return Vec::new();
};
let euc = self
.store
.read(cx)
.announcement
.as_ref()
.and_then(|a| a.euc.clone());
let user = Backend::global(cx).read(cx).current_user();
let announcements = RepoListStore::global(cx).read(cx).announcements.clone();
Announcement::forks_in(&announcements, &addr, euc.as_deref(), user)
.into_iter()
.cloned()
.collect()
}
fn choose_fork(
&mut self,
announcement: Announcement,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(base) = self.store.read(cx).addr().cloned() else {
return;
};
let mirror_path = Mirrors::path(&base);
let namespace = GitCache::fork_namespace(&announcement);
let clone_urls: Vec<Url> = announcement.clone.clone();
let base_clone_urls: Vec<Url> = self
.store
.read(cx)
.announcement
.as_ref()
.map(|a| a.clone.clone())
.unwrap_or_default();
let expected = self.fork.as_ref().map(|fork| fork.announcement.addr());
let handle = window.window_handle();
self.loading = true;
self.error = None;
cx.notify();
let task: gpui::Task<Result<(), anyhow::Error>> = cx.spawn(async move |this, cx| {
let result = cx
.background_spawn({
let namespace = namespace.clone();
let mirror_path = mirror_path.clone();
async move {
Mirrors::ensure(&base, &base_clone_urls)?;
let repo = Repo::open(&mirror_path)?;
repo.delete_refs_with_prefix("refs/fork")?;
repo.fetch_refs(
&clone_urls,
&format!("+refs/heads/*:refs/fork/{namespace}/*"),
)?;
let strip = |refs: Vec<String>, prefix: &str| {
let mut names: Vec<String> = refs
.into_iter()
.filter_map(|name| {
name.strip_prefix(prefix)
.map(|rest| rest.trim_start_matches('/').to_owned())
})
.filter(|name| !name.is_empty())
.collect();
names.sort();
names
};
let base_branches = strip(
repo.refs_with_prefix("refs/remotes/origin")?,
"refs/remotes/origin",
);
let compare_branches = strip(
repo.refs_with_prefix(&format!("refs/fork/{namespace}"))?,
&format!("refs/fork/{namespace}"),
);
Ok::<_, anyhow::Error>((base_branches, compare_branches))
}
})
.await;
if let Err(error) = cx.update_window(handle, |_, window, cx| {
this.update(cx, |this, cx| {
let applied = this.fork.as_ref().map(|fork| fork.announcement.addr());
if applied != expected {
this.loading = false;
cx.notify();
return;
}
this.apply_fork(announcement, mirror_path, namespace, result, window, cx);
})
}) {
log::warn!("failed to apply fork: {error}");
}
Ok(())
});
self.tasks.push(task);
}
fn apply_fork(
&mut self,
announcement: Announcement,
mirror_path: PathBuf,
namespace: String,
result: Result<(Vec<String>, Vec<String>), anyhow::Error>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.loading = false;
let (base_branches, compare_branches) = match result {
Ok(branches) => branches,
Err(error) => {
self.error = Some(format!("Could not compare against the fork: {error}").into());
cx.notify();
return;
}
};
if compare_branches.is_empty() {
self.error = Some("The fork has no branches to compare".into());
cx.notify();
return;
}
if base_branches.is_empty() {
self.error = Some("Could not list the target repository's branches.".into());
cx.notify();
return;
}
let announced = self.store.read(cx).head.clone();
let base = announced
.filter(|branch| base_branches.contains(branch))
.map(SharedString::from)
.or_else(|| {
base_branches
.iter()
.find(|branch| branch.as_str() == "main")
.map(|branch| SharedString::from(branch.clone()))
})
.unwrap_or_else(|| SharedString::from(base_branches[0].clone()));
let compare = compare_branches
.iter()
.find(|branch| branch.as_str() == "main")
.map(|branch| SharedString::from(branch.clone()))
.unwrap_or_else(|| SharedString::from(compare_branches[0].clone()));
self.fork = Some(ForkCompare {
announcement,
namespace,
mirror_path,
});
self.error = None;
self.base = base;
self.compare = compare;
self.branches = base_branches.into_iter().map(SharedString::from).collect();
self.sync_selectors(window, cx);
self.reload_compare(cx);
}
fn reload_compare(&mut self, cx: &mut Context<Self>) {
let Some(repo_path) = self.work_path() else {
return;
};
let base = self.base_ref();
let compare = self.compare_ref();
let base_name = self.base.to_string();
let compare_name = self.compare.to_string();
self.loading = true;
self.error = None;
self.compare_generation += 1;
let generation = self.compare_generation;
cx.notify();
if base == compare {
self.loading = false;
self.merge_base = None;
self.commits = None;
self.pane.update(cx, |pane, cx| pane.clear(cx));
self.error = Some("Choose different base and compare branches".into());
cx.notify();
return;
}
let task: gpui::Task<Result<(), anyhow::Error>> = cx.spawn(async move |this, cx| {
let result = cx
.background_spawn({
async move {
let repo = Repo::open(Path::new(&repo_path))?;
let merge_base = repo.merge_base(&base, &compare)?.ok_or_else(|| {
anyhow::anyhow!(
"{base_name} and {compare_name} share no common ancestor"
)
})?;
let commits = repo.commit_range(&merge_base, &compare)?;
let diff = repo.range_diff(&merge_base, &compare)?;
Ok::<_, anyhow::Error>((merge_base, commits, diff))
}
})
.await;
this.update(cx, |this, cx| {
if generation != this.compare_generation {
return;
}
this.loading = false;
match result {
Ok((merge_base, commits, diff)) => {
this.merge_base = Some(merge_base);
this.commits = Some(commits);
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
}
Err(error) => {
this.merge_base = None;
this.commits = None;
this.pane.update(cx, |pane, cx| pane.clear(cx));
this.error = Some(error.to_string().into());
}
}
cx.notify();
})?;
Ok(())
});
self.tasks.push(task);
}
fn submit(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
if self.submitting || self.loading {
return;
@@ -566,7 +216,7 @@ impl NewPullRequestView {
this.update(cx, |this, _cx| {
this.submitting = false;
})?;
let _ = dock.update(cx, |dock, cx| dock.close_panel(&panel_id, cx));
crate::shared::log_dropped(dock.update(cx, |dock, cx| dock.close_panel(&panel_id, cx)));
Ok(())
});
self.tasks.push(task);
@@ -619,7 +269,19 @@ impl NewPullRequestView {
fn render_compare_bar(&self, cx: &mut Context<Self>) -> AnyElement {
let weak = cx.entity().downgrade();
let weak_menu = cx.entity().downgrade();
let source_items = self.source_menu_items(cx, cx.entity().downgrade());
let source_view = cx.entity().downgrade();
let associated = self
.store
.read(cx)
.addr()
.map(|addr| CheckoutsStore::global(cx).read(cx).associations_of(addr))
.unwrap_or_default();
let active_path = (self.fork.is_none())
.then(|| self.repo_path.clone())
.flatten();
let candidates = self.fork_candidates(cx);
let user = signed_state::Backend::global(cx).read(cx).current_user();
let active_fork = self.fork.as_ref().map(|fork| fork.announcement.addr());
let can_submit = self.submit_enabled(cx);
let source_label = match &self.fork {
Some(fork) => truncate_label(&fork_display_name(&fork.announcement)),
@@ -653,10 +315,10 @@ impl NewPullRequestView {
Menu::new("pr-source")
.open(self.source_open)
.on_change(move |open, _, cx| {
let _ = weak_menu.update(cx, |this, cx| {
crate::shared::log_dropped(weak_menu.update(cx, |this, cx| {
this.source_open = open;
cx.notify();
});
}));
})
.trigger(move |_, _| {
Button::new("compare-source")
@@ -664,7 +326,16 @@ impl NewPullRequestView {
.variant(gpui_native_kit::button::ButtonVariant::Secondary)
.into_any_element()
})
.items(move |_, _| source_items.clone()),
.items(move |_, _| {
super::helpers::source_menu_items(
source_view.clone(),
associated.clone(),
active_path.clone(),
candidates.clone(),
user,
active_fork.clone(),
)
}),
),
)
.when_some(refresh_fork, |this, fork| {
@@ -684,76 +355,14 @@ impl NewPullRequestView {
.loading(self.submitting)
.disabled(!can_submit)
.on_click(move |_, window, cx| {
let _ = weak.update(cx, |this, cx| this.submit(window, cx));
crate::shared::log_dropped(
weak.update(cx, |this, cx| this.submit(window, cx)),
);
}),
)
.into_any_element()
}
fn source_menu_items(&self, cx: &App, view: WeakEntity<Self>) -> Vec<MenuItem> {
let associated = self
.store
.read(cx)
.addr()
.map(|addr| CheckoutsStore::global(cx).read(cx).associations_of(addr))
.unwrap_or_default();
let active_path = (self.fork.is_none())
.then(|| self.repo_path.clone())
.flatten();
let active_fork = self.fork.as_ref().map(|fork| fork.announcement.addr());
let mut items = Vec::new();
for path in associated {
let label = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string());
let view = view.clone();
items.push(
MenuItem::new(truncate_label(&label))
.checked(active_path.as_ref() == Some(&path))
.handler(move |window, cx| {
let path = path.clone();
let _ =
view.update(cx, |this, cx| this.apply_folder_path(path, window, cx));
}),
);
}
let folder_view = view.clone();
items.push(
MenuItem::new("Choose another folder…").handler(move |window, cx| {
let _ = folder_view.update(cx, |this, cx| this.choose_checkout(window, cx));
}),
);
items.push(MenuItem::separator());
let candidates = self.fork_candidates(cx);
if candidates.is_empty() {
items.push(MenuItem::new("No announced forks of this repository").disabled(true));
} else {
let user = Backend::global(cx).read(cx).current_user();
for candidate in candidates {
let subtitle = if Some(candidate.owner) == user {
SharedString::from("Your fork")
} else {
SharedString::from(format!("by {}", shorten_owner(&candidate.owner)))
};
let title = format!("{} — {}", fork_display_name(&candidate), subtitle);
let view = view.clone();
items.push(
MenuItem::new(truncate_label(&title))
.checked(active_fork == Some(candidate.addr()))
.handler(move |window, cx| {
let announcement = candidate.clone();
let _ = view
.update(cx, |this, cx| this.choose_fork(announcement, window, cx));
}),
);
}
}
items
}
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
let files = self.pane.read(cx).diff().map_or(0, |diff| diff.files.len());
let commits = self.commits.as_ref().map_or(0, |commits| commits.len());
@@ -770,10 +379,10 @@ impl NewPullRequestView {
]
})
.on_change(move |index, _, cx| {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
this.active_tab = index;
cx.notify();
});
}));
})
.into_any_element()
}
@@ -824,9 +433,9 @@ impl NewPullRequestView {
ix,
&commit,
move |window, cx| {
let _ = view.update(cx, |this, cx| {
crate::shared::log_dropped(view.update(cx, |this, cx| {
this.open_commit_diff(&id, window, cx);
});
}));
},
cx,
)
@@ -0,0 +1,397 @@
use std::path::{Path, PathBuf};
use gpui::{App, AppContext as _, Context, PathPromptOptions, SharedString, Window};
use nostr::prelude::Url;
use signed_core::Announcement;
use signed_git::{GitCache, Repo};
use signed_state::{Backend, CheckoutsStore, Mirrors, RepoListStore};
use super::NewPullRequestView;
use crate::pull_requests::helpers::ForkCompare;
impl NewPullRequestView {
pub(super) fn has_source(&self) -> bool {
self.repo_path.is_some() || self.fork.is_some()
}
pub(super) fn work_path(&self) -> Option<PathBuf> {
match &self.fork {
Some(fork) => Some(fork.mirror_path.clone()),
None => self.repo_path.clone(),
}
}
pub(super) fn base_ref(&self) -> String {
match &self.fork {
Some(_) => ForkCompare::base_ref(&self.base),
None => self.base.to_string(),
}
}
pub(super) fn compare_ref(&self) -> String {
match &self.fork {
Some(fork) => fork.compare_ref(&self.compare),
None => self.compare.to_string(),
}
}
pub(crate) fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Choose local checkout".into()),
});
let task: gpui::Task<Result<(), anyhow::Error>> =
cx.spawn_in(window, async move |this, cx| {
let picked = match prompt.await {
Ok(Ok(Some(mut paths))) => paths.pop(),
_ => None,
};
let Some(path) = picked else {
return Ok(());
};
this.update_in(cx, |this, window, cx| {
this.apply_folder_path(path, window, cx)
})?;
Ok(())
});
self.tasks.push(task);
}
pub(crate) fn apply_folder_path(
&mut self,
path: PathBuf,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.fork_generation = self.fork_generation.wrapping_add(1);
let generation = self.fork_generation;
let path_string = path.to_string_lossy().to_string();
let task: gpui::Task<Result<(), anyhow::Error>> =
cx.spawn_in(window, async move |this, cx| {
let info = cx
.background_spawn({
let path = path.clone();
async move {
let repo = Repo::try_open(&path)?;
match repo.branches() {
Ok(branches) => Some((branches, repo.current_branch())),
Err(error) => {
log::warn!("failed to list branches: {error}");
None
}
}
}
})
.await;
this.update_in(cx, |this, window, cx| {
if generation != this.fork_generation {
return;
}
this.apply_checkout(path_string, info, window, cx);
})?;
Ok(())
});
self.tasks.push(task);
}
fn apply_checkout(
&mut self,
path: String,
info: Option<(Vec<String>, Option<String>)>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.fork = None;
let Some((branches, current)) = info else {
self.error = Some("The chosen folder is not a git repository".into());
self.repo_path = None;
self.branches.clear();
self.merge_base = None;
self.commits = None;
self.pane.update(cx, |pane, cx| pane.clear(cx));
cx.notify();
return;
};
if branches.is_empty() {
self.error = Some("The repository has no branches yet".into());
self.repo_path = None;
self.branches.clear();
self.merge_base = None;
self.commits = None;
self.pane.update(cx, |pane, cx| pane.clear(cx));
cx.notify();
return;
}
let announced = self.store.read(cx).head.clone();
let base = announced
.as_ref()
.filter(|branch| branches.contains(branch))
.cloned()
.or_else(|| branches.iter().find(|branch| *branch == "main").cloned())
.unwrap_or_else(|| branches[0].clone());
let compare = current
.filter(|branch| branches.contains(branch))
.unwrap_or_else(|| base.clone());
self.repo_path = Some(PathBuf::from(&path));
self.error = None;
self.branches = branches.into_iter().map(SharedString::from).collect();
if let Some(addr) = self.store.read(cx).addr().cloned() {
CheckoutsStore::global(cx).update(cx, |store, cx| {
store.record(PathBuf::from(&path), addr, cx);
});
}
self.base = SharedString::from(base);
self.compare = SharedString::from(compare);
self.sync_selectors(window, cx);
self.reload_compare(cx);
}
fn sync_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let options: Vec<(SharedString, SharedString)> = self
.branches
.iter()
.map(|branch| (branch.clone(), branch.clone()))
.collect();
let base = self.base.clone();
let compare = self.compare.clone();
self.base_select.update(cx, |state, cx| {
state.set_options(options.clone(), cx);
state.set_selected(Some(base), window, cx);
});
self.compare_select.update(cx, |state, cx| {
state.set_options(options, cx);
state.set_selected(Some(compare), window, cx);
});
}
pub(super) fn fork_candidates(&self, cx: &App) -> Vec<Announcement> {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return Vec::new();
};
let euc = self
.store
.read(cx)
.announcement
.as_ref()
.and_then(|a| a.euc.clone());
let user = Backend::global(cx).read(cx).current_user();
let announcements = RepoListStore::global(cx).read(cx).announcements.clone();
Announcement::forks_in(&announcements, &addr, euc.as_deref(), user)
.into_iter()
.cloned()
.collect()
}
pub(crate) fn choose_fork(
&mut self,
announcement: Announcement,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(base) = self.store.read(cx).addr().cloned() else {
return;
};
let mirror_path = Mirrors::path(&base);
let namespace = GitCache::fork_namespace(&announcement);
let clone_urls: Vec<Url> = announcement.clone.clone();
let base_clone_urls: Vec<Url> = self
.store
.read(cx)
.announcement
.as_ref()
.map(|a| a.clone.clone())
.unwrap_or_default();
self.fork_generation = self.fork_generation.wrapping_add(1);
let generation = self.fork_generation;
let handle = window.window_handle();
self.loading = true;
self.error = None;
cx.notify();
let task: gpui::Task<Result<(), anyhow::Error>> = cx.spawn(async move |this, cx| {
let result = cx
.background_spawn({
let namespace = namespace.clone();
let mirror_path = mirror_path.clone();
async move {
Mirrors::ensure(&base, &base_clone_urls)?;
let repo = Repo::open(&mirror_path)?;
repo.delete_refs_with_prefix("refs/fork")?;
repo.fetch_refs(
&clone_urls,
&format!("+refs/heads/*:refs/fork/{namespace}/*"),
)?;
let strip = |refs: Vec<String>, prefix: &str| {
let mut names: Vec<String> = refs
.into_iter()
.filter_map(|name| {
name.strip_prefix(prefix)
.map(|rest| rest.trim_start_matches('/').to_owned())
})
.filter(|name| !name.is_empty())
.collect();
names.sort();
names
};
let base_branches = strip(
repo.refs_with_prefix("refs/remotes/origin")?,
"refs/remotes/origin",
);
let compare_branches = strip(
repo.refs_with_prefix(&format!("refs/fork/{namespace}"))?,
&format!("refs/fork/{namespace}"),
);
Ok::<_, anyhow::Error>((base_branches, compare_branches))
}
})
.await;
if let Err(error) = cx.update_window(handle, |_, window, cx| {
this.update(cx, |this, cx| {
if generation != this.fork_generation {
return;
}
this.apply_fork(announcement, mirror_path, namespace, result, window, cx);
})
}) {
log::warn!("failed to apply fork: {error}");
}
Ok(())
});
self.tasks.push(task);
}
fn apply_fork(
&mut self,
announcement: Announcement,
mirror_path: PathBuf,
namespace: String,
result: Result<(Vec<String>, Vec<String>), anyhow::Error>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.loading = false;
let (base_branches, compare_branches) = match result {
Ok(branches) => branches,
Err(error) => {
self.error = Some(format!("Could not compare against the fork: {error}").into());
cx.notify();
return;
}
};
if compare_branches.is_empty() {
self.error = Some("The fork has no branches to compare".into());
cx.notify();
return;
}
if base_branches.is_empty() {
self.error = Some("Could not list the target repository's branches.".into());
cx.notify();
return;
}
let announced = self.store.read(cx).head.clone();
let base = announced
.filter(|branch| base_branches.contains(branch))
.map(SharedString::from)
.or_else(|| {
base_branches
.iter()
.find(|branch| branch.as_str() == "main")
.map(|branch| SharedString::from(branch.clone()))
})
.unwrap_or_else(|| SharedString::from(base_branches[0].clone()));
let compare = compare_branches
.iter()
.find(|branch| branch.as_str() == "main")
.map(|branch| SharedString::from(branch.clone()))
.unwrap_or_else(|| SharedString::from(compare_branches[0].clone()));
self.fork = Some(ForkCompare {
announcement,
namespace,
mirror_path,
});
self.error = None;
self.base = base;
self.compare = compare;
self.branches = base_branches.into_iter().map(SharedString::from).collect();
self.sync_selectors(window, cx);
self.reload_compare(cx);
}
pub(super) fn reload_compare(&mut self, cx: &mut Context<Self>) {
let Some(repo_path) = self.work_path() else {
return;
};
let base = self.base_ref();
let compare = self.compare_ref();
let base_name = self.base.to_string();
let compare_name = self.compare.to_string();
self.loading = true;
self.error = None;
self.compare_generation += 1;
let generation = self.compare_generation;
cx.notify();
if base == compare {
self.loading = false;
self.merge_base = None;
self.commits = None;
self.pane.update(cx, |pane, cx| pane.clear(cx));
self.error = Some("Choose different base and compare branches".into());
cx.notify();
return;
}
let task: gpui::Task<Result<(), anyhow::Error>> = cx.spawn(async move |this, cx| {
let result = cx
.background_spawn({
async move {
let repo = Repo::open(Path::new(&repo_path))?;
let merge_base = repo.merge_base(&base, &compare)?.ok_or_else(|| {
anyhow::anyhow!(
"{base_name} and {compare_name} share no common ancestor"
)
})?;
let commits = repo.commit_range(&merge_base, &compare)?;
let diff = repo.range_diff(&merge_base, &compare)?;
Ok::<_, anyhow::Error>((merge_base, commits, diff))
}
})
.await;
this.update(cx, |this, cx| {
if generation != this.compare_generation {
return;
}
this.loading = false;
match result {
Ok((merge_base, commits, diff)) => {
this.merge_base = Some(merge_base);
this.commits = Some(commits);
this.pane.update(cx, |pane, cx| pane.set_diff(diff, cx));
}
Err(error) => {
this.merge_base = None;
this.commits = None;
this.pane.update(cx, |pane, cx| pane.clear(cx));
this.error = Some(error.to_string().into());
}
}
cx.notify();
})?;
Ok(())
});
self.tasks.push(task);
}
}
@@ -102,9 +102,10 @@ impl SendPatchView {
}
let panel_id = self.panel_id.clone();
let _ = self
.dock
.update(cx, |dock, cx| dock.close_panel(&panel_id, cx));
crate::shared::log_dropped(
self.dock
.update(cx, |dock, cx| dock.close_panel(&panel_id, cx)),
);
}
fn render_footer(&self, cx: &Context<Self>) -> AnyElement {
@@ -127,7 +128,7 @@ impl SendPatchView {
.loading(self.submitting)
.disabled(!can_submit)
.on_click(move |_, _, cx| {
let _ = weak.update(cx, |this, cx| this.submit(cx));
crate::shared::log_dropped(weak.update(cx, |this, cx| this.submit(cx)));
}),
)
.into_any_element()
+7 -7
View File
@@ -7,6 +7,13 @@ use signed_state::RepoStore;
use crate::repo::RepoDetailView;
use crate::shared::panels;
/// An item of a repository to open from outside its detail panel.
pub enum RepoItem {
Issue(EventId),
PullRequest(EventId),
Patch,
}
/// Opens a repository as a dock panel, keyed by its address.
pub fn open_repo_panel(
dock: &WeakEntity<DockState>,
@@ -33,13 +40,6 @@ pub fn open_repo_panel(
);
}
/// An item of a repository to open from outside its detail panel.
pub enum RepoItem {
Issue(EventId),
PullRequest(EventId),
Patch,
}
/// Opens an issue or pull request using a store built here.
pub fn open_repo_item(
dock: &WeakEntity<DockState>,
+3 -1
View File
@@ -222,7 +222,9 @@ impl Render for CreateRepoDialog {
.loading(busy)
.disabled(busy)
.on_click(move |_, window, cx| {
let _ = this.update(cx, |this, cx| this.create(window, cx));
crate::shared::log_dropped(
this.update(cx, |this, cx| this.create(window, cx)),
);
})
.into_any_element();
+3 -1
View File
@@ -243,7 +243,9 @@ impl Render for InitRepoDialog {
.loading(busy)
.disabled(busy)
.on_click(move |_, window, cx| {
let _ = this.update(cx, |this, cx| this.init_repository(window, cx));
crate::shared::log_dropped(
this.update(cx, |this, cx| this.init_repository(window, cx)),
);
})
.into_any_element();
+5 -13
View File
@@ -69,8 +69,7 @@ pub struct RepoFilesView {
}
impl RepoFilesView {
pub fn new(cx: &mut Context<Self>) -> Self {
let _ = cx;
pub fn new(_cx: &mut Context<Self>) -> Self {
Self {
seeds: Vec::new(),
expanded: HashSet::new(),
@@ -448,13 +447,7 @@ impl RepoFilesView {
.into_any_element()
}
fn set_code(
&mut self,
path: SharedString,
text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn set_code(&mut self, path: SharedString, text: &str, cx: &mut Context<Self>) {
let hash = source_hash(text);
let language = code_language(path.as_ref());
@@ -480,7 +473,6 @@ impl RepoFilesView {
}
let state = cx.new(|cx| CodeViewState::new("repo-code", text, language, cx));
let _ = window;
self.code = Some(CodeView {
path,
state,
@@ -514,7 +506,7 @@ impl RepoFilesView {
self.set_markdown(Some(path.into()), &text, cx);
}
} else if self.code.as_ref().map(|code| code.path.as_str()) != Some(path) {
self.set_code(path.into(), &text, window, cx);
self.set_code(path.into(), &text, cx);
}
}
cx.notify();
@@ -574,7 +566,7 @@ impl RepoFilesView {
})
.await;
this.update_in(cx, |this, window, cx| {
this.update_in(cx, |this, _window, cx| {
this.loading_files.remove(&path);
match content {
@@ -590,7 +582,7 @@ impl RepoFilesView {
let same = this.code.as_ref().map(|code| code.path.as_str())
== Some(path.as_str());
if !same {
this.set_code(path.clone().into(), text, window, cx);
this.set_code(path.clone().into(), text, cx);
}
}
this.preview_bytes += text.len();
+667
View File
@@ -0,0 +1,667 @@
use std::collections::HashSet;
use assets::CustomIconName;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, AppContext as _, ClipboardItem, Context, IntoElement, ParentElement as _,
SharedString, Styled as _, Window, div, px,
};
use gpui_native_kit::avatar::Avatar;
use gpui_native_kit::button::{Button, ButtonVariant};
use gpui_native_kit::clipboard::ClipboardButton;
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
use gpui_native_kit::data::tabs::{Tab, Tabs};
use gpui_native_kit::dropdown_menu::DropdownMenu;
use gpui_native_kit::icons::{Icon, IconRole};
use gpui_native_kit::menu::MenuItem;
use gpui_native_kit::popover::Popover;
use gpui_native_kit::theme::ActiveTheme as _;
use gpui_native_kit::tooltip::Tooltip;
use signed_state::{Backend, Nip34Kind};
use super::RepoDetailView;
use super::actions;
use super::dialogs;
use super::helpers::{
ShareTargets, bound_repo_label, clone_group, fork_row, nostr_clone_url, repo_display_name,
};
use crate::issues::IssuesView;
use crate::pull_requests::{NewPullRequestView, PullRequestsView, SendPatchView};
impl RepoDetailView {
pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self
.announcement(cx)
.and_then(|announcement| announcement.upstream.as_ref())
.and_then(|upstream| upstream.addr.clone())
else {
return;
};
actions::open_repo_panel(&self.dock, &addr, None, window, cx);
}
/// Open the dialog that publishes the local repository to NIP-34.
fn open_init_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(local_path) = self.store.read(cx).path.clone() else {
return;
};
dialogs::init::open(local_path, cx.entity().downgrade(), window, cx);
}
fn open_pulls(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return;
};
let id = SharedString::from(format!("pulls:{addr}"));
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
crate::shared::panels::open(
&dock,
id,
move |_, cx| {
cx.new(|cx| PullRequestsView::new(dock_for_build.clone(), store.clone(), cx))
},
window,
cx,
);
}
pub(super) fn open_new_pull_request(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return;
};
let id = SharedString::from(format!("new-pr:{addr}"));
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
crate::shared::panels::open(
&dock,
id.clone(),
move |window, cx| {
cx.new(|cx| {
NewPullRequestView::new(
dock_for_build.clone(),
id.clone(),
store.clone(),
window,
cx,
)
})
},
window,
cx,
);
}
fn open_send_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return;
};
let id = SharedString::from(format!("send-patch:{addr}"));
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
crate::shared::panels::open(
&dock,
id.clone(),
move |window, cx| {
cx.new(|cx| {
SendPatchView::new(
dock_for_build.clone(),
id.clone(),
store.clone(),
window,
cx,
)
})
},
window,
cx,
);
}
fn open_issues(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return;
};
let id = SharedString::from(format!("issues:{addr}"));
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
crate::shared::panels::open(
&dock,
id,
move |_, cx| cx.new(|cx| IssuesView::new(dock_for_build.clone(), store.clone(), cx)),
window,
cx,
);
}
pub(super) fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
if self.store.read(cx).addr().is_none() {
return self.render_local_header(cx);
}
let store = self.store.read(cx);
let pushing = store.pushing;
let cloning = store.cloning;
let Some(announcement) = store.announcement.clone() else {
return div().into_any_element();
};
let nip05 = self
.directory
.read(cx)
.profile(&announcement.owner, cx)
.metadata()
.nip05
.clone()
.filter(|nip05| !nip05.trim().is_empty());
let share = 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: Vec<String> = announcement.clone_urls();
let name = repo_display_name(self.store.read(cx));
let description = announcement.description();
let avatar = Avatar::new(format!("{}:{}", announcement.owner, announcement.id));
let share_naddr = share.naddr.clone();
let share_for_menu = share.clone();
let announcement_for_menu = announcement.clone();
let weak_share = cx.entity().downgrade();
let weak_menu_open = cx.entity().downgrade();
let weak_menu_items = cx.entity().downgrade();
let weak_clone = cx.entity().downgrade();
let weak_download = cx.entity().downgrade();
let ngit_for_popover = ngit_command.clone();
let nak_for_popover = nak_command.clone();
let git_for_popover = git_commands.clone();
v_flex()
.flex_none()
.p_4()
.w_full()
.gap_6()
.border_b_1()
.border_color(cx.theme().colors.border)
.child(
h_flex()
.w_full()
.gap_4()
.items_start()
.justify_between()
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.child(
h_flex()
.gap_2()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(avatar)
.child(name),
)
.child(
div()
.min_w_0()
.text_sm()
.text_color(cx.theme().colors.muted_foreground)
.line_clamp(2)
.child(description),
)
.when_some(fork_row(&announcement, cx), |this, row| this.child(row))
.child(
h_flex()
.mt_2()
.w_full()
.gap_1()
.child(
div()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.font_weight(gpui::FontWeight::SEMIBOLD)
.child("Maintainers:"),
)
.child(self.render_maintainers(cx)),
),
)
.child(
h_flex()
.flex_none()
.gap_2()
.justify_end()
.child({
let weak_pulls = cx.entity().downgrade();
let weak_pulls_open = cx.entity().downgrade();
let _store_for_pulls = self.store.clone();
let pulls_count = self.store.read(cx).pull_request_count();
DropdownMenu::new("pulls")
.label(format!("Pull requests {pulls_count}"))
.icon(Icon::named(CustomIconName::GitPullRequest))
.variant(ButtonVariant::Secondary)
.open(self.pulls_menu_open)
.on_click(move |_, window, cx| {
if let Err(error) = weak_pulls.update(cx, |this, cx| {
this.open_pulls(window, cx);
}) {
log::warn!("failed to open pull requests: {error}");
}
})
.on_open_change(move |open, _, cx| {
if let Err(error) = weak_pulls_open.update(cx, |this, cx| {
this.pulls_menu_open = open;
cx.notify();
}) {
log::warn!("failed to update pulls menu: {error}");
}
})
.items({
let weak_new = cx.entity().downgrade();
let weak_patch = cx.entity().downgrade();
move |_, _| {
let weak_new = weak_new.clone();
let weak_patch = weak_patch.clone();
vec![
MenuItem::new("New pull request").handler(move |window, cx| {
crate::shared::log_dropped(weak_new.update(cx, |this, cx| {
this.open_new_pull_request(window, cx);
}));
}),
MenuItem::new("Send patch").handler(move |window, cx| {
crate::shared::log_dropped(weak_patch.update(cx, |this, cx| {
this.open_send_patch(window, cx);
}));
}),
]
}
})
})
.child({
let weak_issues = cx.entity().downgrade();
let weak_issues_open = cx.entity().downgrade();
let store_for_issues = self.store.clone();
let issues_count = self.store.read(cx).issue_count();
DropdownMenu::new("issues")
.label(format!("Issues {issues_count}"))
.icon(Icon::named(CustomIconName::GitIssueDone))
.variant(ButtonVariant::Secondary)
.open(self.issues_menu_open)
.on_click(move |_, window, cx| {
if let Err(error) = weak_issues.update(cx, |this, cx| {
this.open_issues(window, cx);
}) {
log::warn!("failed to open issues: {error}");
}
})
.on_open_change(move |open, _, cx| {
if let Err(error) =
weak_issues_open.update(cx, |this, cx| {
this.issues_menu_open = open;
cx.notify();
})
{
log::warn!("failed to update issues menu: {error}");
}
})
.items(move |_, _| {
vec![MenuItem::new("New issue").handler({
let store = store_for_issues.clone();
move |_, cx| {
crate::issues::new_issue::open(store.clone(), cx);
}
})]
})
})
.child(
DropdownMenu::new("share")
.icon(Icon::named(IconRole::Copy))
.variant(ButtonVariant::Secondary)
.open(self.share_open)
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
share_naddr.clone(),
));
})
.on_open_change(move |open, _, cx| {
if let Err(error) = weak_share.update(cx, |this, cx| {
this.share_open = open;
cx.notify();
}) {
log::warn!("failed to update share menu: {error}");
}
})
.items(move |_, _| share_for_menu.menu()),
)
.child(
DropdownMenu::new("repo-menu")
.icon(Icon::named(CustomIconName::EllipsisVertical))
.variant(ButtonVariant::Secondary)
.loading(pushing)
.disabled(pushing)
.open(self.repo_menu_open)
.on_open_change(move |open, _, cx| {
if let Err(error) = weak_menu_open.update(cx, |this, cx| {
this.repo_menu_open = open;
cx.notify();
}) {
log::warn!("failed to update repo menu: {error}");
}
})
.items(move |_, cx| {
let owner = Backend::global(cx)
.read(cx)
.current_user()
== Some(announcement_for_menu.owner);
let mut items = vec![MenuItem::new("About").handler({
let weak = weak_menu_items.clone();
let announcement = announcement_for_menu.clone();
move |_, cx| {
if let Err(error) = weak.update(cx, |this, cx| {
dialogs::about::open(
announcement.clone(),
this.directory.clone(),
cx,
);
}) {
log::warn!("failed to open about: {error}");
}
}
})];
if owner {
items.push(MenuItem::new("Republish").handler({
let weak = weak_menu_items.clone();
move |window, cx| {
if let Err(error) = weak.update(cx, |this, cx| {
this.push_repository(window, cx);
}) {
log::warn!("failed to republish: {error}");
}
}
}));
items.push(MenuItem::new("Reset remote to local").handler({
let weak = weak_menu_items.clone();
move |window, cx| {
if let Err(error) = weak.update(cx, |this, cx| {
this.reset_remote(window, cx);
}) {
log::warn!("failed to reset remote: {error}");
}
}
}));
items.push(MenuItem::separator());
items.push(
MenuItem::new("Delete")
.destructive(true)
.handler({
let weak = weak_menu_items.clone();
move |window, cx| {
if let Err(error) =
weak.update(cx, |this, cx| {
this.delete_repository(window, cx);
})
{
log::warn!(
"failed to delete repository: {error}"
);
}
}
}),
);
}
items
}),
)
.child(
Popover::new("clone")
.open(self.clone_open)
.on_change(move |open, _, cx| {
if let Err(error) = weak_clone.update(cx, |this, cx| {
this.clone_open = open;
cx.notify();
}) {
log::warn!("failed to update clone popover: {error}");
}
})
.trigger(move |_, _| {
Button::new("clone")
.icon(Icon::named(CustomIconName::GitClone))
.tooltip(Tooltip::text("Clone"))
.loading(cloning)
.disabled(cloning)
.into_any_element()
})
.content(move |_, cx| {
v_flex()
.w(px(440.))
.p_3()
.gap_4()
.child(clone_group(
"Clone with ngit",
"copy-ngit",
&ngit_for_popover,
))
.child(clone_group(
"Clone with nak",
"copy-nak",
&nak_for_popover,
))
.child(
v_flex()
.gap_1()
.child(
div()
.text_xs()
.text_color(
cx.theme().colors.muted_foreground,
)
.child("Grasp Servers"),
)
.when(git_for_popover.is_empty(), |this| {
this.child(
div()
.text_xs()
.child("No git clone urls."),
)
})
.children(git_for_popover.iter().enumerate().map(
|(ix, command)| {
ClipboardButton::new(
SharedString::from(format!(
"copy-git-{ix}"
)),
command.clone(),
)
},
)),
)
.child(
h_flex().justify_end().child(
Button::new("download")
.icon(Icon::named(CustomIconName::GitClone))
.label("Download")
.on_click({
let weak = weak_download.clone();
move |_, window, cx| {
if let Err(error) =
weak.update(cx, |this, cx| {
this.clone_to_folder(window, cx);
})
{
log::warn!(
"failed to clone: {error}"
);
}
}
}),
),
)
.into_any_element()
}),
),
),
)
.child(self.render_tabs(cx))
.into_any_element()
}
fn render_local_header(&self, cx: &mut Context<Self>) -> AnyElement {
let name = repo_display_name(self.store.read(cx));
let path = self
.store
.read(cx)
.path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_default();
let avatar = Avatar::new(path.clone());
let bound = self.store.read(cx).nip34.clone();
let action = match bound
.as_ref()
.filter(|binding| binding.kind == Nip34Kind::Initialized)
{
Some(binding) => bound_repo_label(binding, &self.directory, cx),
None => Button::new("init")
.icon(Icon::named(CustomIconName::Init))
.label("Initialize on Nostr")
.tooltip(Tooltip::text("Publish this repository to Nostr"))
.on_click(cx.listener(|this, _, window, cx| {
this.open_init_dialog(window, cx);
}))
.into_any_element(),
};
v_flex()
.flex_none()
.px_4()
.pb_4()
.w_full()
.gap_6()
.border_b_1()
.border_color(cx.theme().colors.border)
.child(
h_flex()
.w_full()
.gap_4()
.items_start()
.justify_between()
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.child(
h_flex()
.gap_2()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(avatar)
.child(name),
)
.child(
div()
.min_w_0()
.text_sm()
.text_color(cx.theme().colors.muted_foreground)
.child(path),
),
)
.child(action),
)
.child(self.render_tabs(cx))
.into_any_element()
}
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
let count = self.history.read(cx).commit_count();
let muted = cx.theme().colors.muted_foreground;
let weak = cx.entity().downgrade();
let commits_label = match count {
Some(count) => SharedString::from(format!("Commits ({count})")),
None => SharedString::from("Commits"),
};
Tabs::new("repo-tabs")
.selected(self.active_tab)
.tabs(move |_, _| {
vec![
Tab::new("files", "Files")
.lead(Icon::named(CustomIconName::GitFile).color(muted)),
Tab::new("commits", commits_label.clone())
.lead(Icon::named(CustomIconName::GitCommit).color(muted)),
]
})
.on_change(move |index, _, cx| {
if let Err(error) = weak.update(cx, |this, cx| {
this.active_tab = index;
cx.notify();
}) {
log::warn!("failed to switch repo tab: {error}");
}
})
.into_any_element()
}
fn render_maintainers(&self, cx: &mut Context<Self>) -> AnyElement {
let Some(announcement) = self.announcement(cx) else {
return div().into_any_element();
};
let mut seen = HashSet::new();
let rest: Vec<_> = announcement
.maintainers
.iter()
.copied()
.filter(|key| key != &announcement.owner && seen.insert(*key))
.collect();
let owner = self.directory.read(cx).profile(&announcement.owner, cx);
let owner_name = owner.name();
h_flex()
.w_full()
.gap_3()
.child(
h_flex()
.gap_2()
.child(
h_flex()
.gap_1()
.child(
Avatar::new(announcement.owner)
.name(owner_name.clone())
.picture(owner.picture())
.small(),
)
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
)
.when(!rest.is_empty(), |this| {
this.child(
div()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.child(SharedString::from(format!("+{}", rest.len()))),
)
}),
)
.into_any_element()
}
}
+79 -79
View File
@@ -26,6 +26,24 @@ use utils::{middle_truncate, relative_time_secs};
use super::RepoDetailView;
use crate::shared::tree::{TreeItemSeed, build_tree_items};
const GUTTER_WIDTH: f32 = 44.;
pub const DIFF_ROW_HEIGHT: f32 = 20.;
pub const COMMIT_ROW_HEIGHT: f32 = 56.;
#[derive(Clone, Copy)]
pub(super) enum DiffRow {
Hunk {
old_start: u32,
old_lines: u32,
new_start: u32,
new_lines: u32,
},
Line {
hunk: usize,
line: usize,
},
}
pub(super) struct RepoData {
pub(super) tree: Vec<TreeItemSeed>,
pub(super) entries: Vec<PathBuf>,
@@ -38,6 +56,67 @@ pub(super) struct RepoData {
pub(super) head_commit: Option<FileCommit>,
}
#[derive(Clone)]
pub(super) struct ShareTargets {
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
pub(super) naddr: String,
/// Hex ID of the announcement event itself.
event_id: String,
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
coordinate: String,
gitworkshop: String,
ditto: String,
}
impl ShareTargets {
pub(super) fn from_announcement(announcement: &Announcement) -> Self {
let addr = announcement.addr();
let coordinate = addr.to_string();
let naddr = Nip19Coordinate::new(addr.into(), announcement.relays.iter().cloned())
.to_bech32()
.unwrap_or_else(|_| coordinate.clone());
Self {
naddr: naddr.clone(),
event_id: announcement
.event_id
.to_bech32()
.unwrap_or_else(|_| announcement.event_id.to_string()),
coordinate,
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
ditto: format!("https://ditto.pub/{naddr}"),
}
}
pub(super) fn menu(&self) -> Vec<MenuItem> {
let items = [
("GitWorkshop", truncate_naddr_link(&self.gitworkshop, 4)),
("Ditto", truncate_naddr_link(&self.ditto, 4)),
("Event ID", middle_truncate(&self.event_id, 10, 10)),
("Coordinate", middle_truncate(&self.coordinate, 10, 10)),
];
let values = [
self.gitworkshop.clone(),
self.ditto.clone(),
self.event_id.clone(),
self.coordinate.clone(),
];
items
.into_iter()
.zip(values)
.map(|((label, detail), value)| {
MenuItem::new(SharedString::from(format!("{label} {detail}"))).handler(
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(value.clone()));
},
)
})
.collect()
}
}
pub(super) fn clone_group(label: &str, id: &'static str, command: &SharedString) -> AnyElement {
v_flex()
.gap_1()
@@ -106,67 +185,6 @@ pub(super) fn fork_row(
})
}
#[derive(Clone)]
pub(super) struct ShareTargets {
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
pub(super) naddr: String,
/// Hex ID of the announcement event itself.
event_id: String,
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
coordinate: String,
gitworkshop: String,
ditto: String,
}
impl ShareTargets {
pub(super) fn from_announcement(announcement: &Announcement) -> Self {
let addr = announcement.addr();
let coordinate = addr.to_string();
let naddr = Nip19Coordinate::new(addr.into(), announcement.relays.iter().cloned())
.to_bech32()
.unwrap_or_else(|_| coordinate.clone());
Self {
naddr: naddr.clone(),
event_id: announcement
.event_id
.to_bech32()
.unwrap_or_else(|_| announcement.event_id.to_string()),
coordinate,
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
ditto: format!("https://ditto.pub/{naddr}"),
}
}
pub(super) fn menu(&self) -> Vec<MenuItem> {
let items = [
("GitWorkshop", truncate_naddr_link(&self.gitworkshop, 4)),
("Ditto", truncate_naddr_link(&self.ditto, 4)),
("Event ID", middle_truncate(&self.event_id, 10, 10)),
("Coordinate", middle_truncate(&self.coordinate, 10, 10)),
];
let values = [
self.gitworkshop.clone(),
self.ditto.clone(),
self.event_id.clone(),
self.coordinate.clone(),
];
items
.into_iter()
.zip(values)
.map(|((label, detail), value)| {
MenuItem::new(SharedString::from(format!("{label} {detail}"))).handler(
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(value.clone()));
},
)
})
.collect()
}
}
pub(super) fn truncate_naddr_link(url: &str, tail: usize) -> String {
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
return url.to_string();
@@ -314,24 +332,6 @@ pub(super) fn is_markdown_path(path: &str) -> bool {
})
}
const GUTTER_WIDTH: f32 = 44.;
pub const DIFF_ROW_HEIGHT: f32 = 20.;
pub const COMMIT_ROW_HEIGHT: f32 = 56.;
#[derive(Clone, Copy)]
pub(super) enum DiffRow {
Hunk {
old_start: u32,
old_lines: u32,
new_start: u32,
new_lines: u32,
},
Line {
hunk: usize,
line: usize,
},
}
pub(super) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
let mut rows = Vec::new();
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
+84 -643
View File
@@ -5,32 +5,25 @@ use anyhow::Error;
use assets::CustomIconName;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, AppContext as _, ClipboardItem, Context, Entity, InteractiveElement as _,
IntoElement, ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _,
Subscription, Task, WeakEntity, Window, div, px,
AnyElement, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _, Subscription, Task,
WeakEntity, Window, div, px,
};
use gpui_native_kit::alert::{Alert, AlertSeverity};
use gpui_native_kit::avatar::Avatar;
use gpui_native_kit::badge::Badge;
use gpui_native_kit::button::{Button, ButtonVariant};
use gpui_native_kit::clipboard::ClipboardButton;
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
use gpui_native_kit::data::tabs::{Tab, Tabs};
use gpui_native_kit::dropdown_menu::DropdownMenu;
use gpui_native_kit::icons::{Icon, IconRole};
use gpui_native_kit::layout::dock::{DockState, Panel};
use gpui_native_kit::menu::MenuItem;
use gpui_native_kit::popover::Popover;
use gpui_native_kit::spinner::Spinner;
use gpui_native_kit::theme::ActiveTheme as _;
use gpui_native_kit::tooltip::Tooltip;
use metadata::ProfileDirectory;
use nostr::prelude::{PublicKey, Url};
use signed_core::{Announcement, RepoAddr};
use signed_git::{FileCommit, GitCache, Repo};
use signed_state::{
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
Nip34Binding, Nip34Kind, PushStepStatus, RepoStore, SyncStatusDirectory,
Nip34Binding, PushStepStatus, RepoStore, SyncStatusDirectory,
};
pub mod actions;
@@ -38,21 +31,17 @@ pub mod banners;
pub mod commit_diff;
pub mod dialogs;
pub mod files;
mod header;
pub mod helpers;
pub mod history;
pub mod refs;
use banners::Banners;
use files::RepoFilesView;
use helpers::{
RepoData, ShareTargets, bound_repo_label, clone_group, fork_row, load_repo_data,
nostr_clone_url, repo_display_name,
};
use helpers::{RepoData, load_repo_data, repo_display_name};
use history::RepoHistoryView;
use refs::RefSwitcher;
use crate::issues::IssuesView;
use crate::pull_requests::{NewPullRequestView, PullRequestsView, SendPatchView};
use crate::shared::tree::{build_tree_items, sorted_worktree_paths};
use crate::shared::widgets::repo_tab_avatar;
@@ -65,16 +54,16 @@ pub enum RefKind {
}
pub struct RepoDetailView {
dock: WeakEntity<DockState>,
store: Entity<RepoStore>,
pub(super) dock: WeakEntity<DockState>,
pub(super) store: Entity<RepoStore>,
/// A repository opened by address alone starts without an announcement.
repo_started: bool,
/// The Files tab, which owns the explorer, previews and the per-file commit map.
files: Entity<RepoFilesView>,
/// The Commits tab, which walks HEAD and opens commit diffs.
history: Entity<RepoHistoryView>,
pub(super) history: Entity<RepoHistoryView>,
/// The active tab: Files or Commits.
active_tab: usize,
pub(super) active_tab: usize,
/// The checked-out worktree path.
worktree: Option<PathBuf>,
loading: bool,
@@ -84,14 +73,14 @@ pub struct RepoDetailView {
banners: Banners,
tasks: Vec<Task<Result<(), Error>>>,
/// Profiles rendered by the header and maintainers.
directory: Entity<ProfileDirectory>,
pub(super) directory: Entity<ProfileDirectory>,
/// Sync status feeding the sync-to-Nostr banner.
sync_directory: Entity<SyncStatusDirectory>,
share_open: bool,
repo_menu_open: bool,
clone_open: bool,
issues_menu_open: bool,
pulls_menu_open: bool,
pub(super) share_open: bool,
pub(super) repo_menu_open: bool,
pub(super) clone_open: bool,
pub(super) issues_menu_open: bool,
pub(super) pulls_menu_open: bool,
_subscriptions: Vec<Subscription>,
}
@@ -151,10 +140,10 @@ impl RepoDetailView {
let store_for_attach = store.clone();
window.defer(cx, move |window, cx| {
let _ = weak.update(cx, |this, cx| {
crate::shared::log_dropped(weak.update(cx, |this, cx| {
this.attach_store(&store_for_attach, window, cx);
this.load_repo(window, cx);
});
}));
});
Self {
@@ -406,8 +395,16 @@ impl RepoDetailView {
return Ok::<_, Error>(None);
};
repo.fetch().ok();
let moved = repo.fast_forward_branches().unwrap_or(false);
if let Err(error) = repo.fetch() {
log::debug!("best-effort fetch failed: {error}");
}
let moved = match repo.fast_forward_branches() {
Ok(moved) => moved,
Err(error) => {
log::warn!("failed to fast-forward branches: {error}");
false
}
};
Ok::<_, Error>(Some((moved, repo.snapshot()?)))
})
.await
@@ -811,640 +808,83 @@ impl RepoDetailView {
}
/// Open the upstream repository, the `u` tag of this fork's announcement.
pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self
.announcement(cx)
.and_then(|announcement| announcement.upstream.as_ref())
.and_then(|upstream| upstream.addr.clone())
else {
return;
};
actions::open_repo_panel(&self.dock, &addr, None, window, cx);
}
/// Open the dialog that publishes the local repository to NIP-34.
fn open_init_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(local_path) = self.store.read(cx).path.clone() else {
return;
};
dialogs::init::open(local_path, cx.entity().downgrade(), window, cx);
}
fn open_pulls(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return;
};
let id = SharedString::from(format!("pulls:{addr}"));
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
crate::shared::panels::open(
&dock,
id,
move |_, cx| {
cx.new(|cx| PullRequestsView::new(dock_for_build.clone(), store.clone(), cx))
},
window,
cx,
);
}
fn open_new_pull_request(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return;
};
let id = SharedString::from(format!("new-pr:{addr}"));
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
crate::shared::panels::open(
&dock,
id.clone(),
move |window, cx| {
cx.new(|cx| {
NewPullRequestView::new(
dock_for_build.clone(),
id.clone(),
store.clone(),
window,
cx,
)
})
},
window,
cx,
);
}
fn open_send_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return;
};
let id = SharedString::from(format!("send-patch:{addr}"));
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
crate::shared::panels::open(
&dock,
id.clone(),
move |window, cx| {
cx.new(|cx| {
SendPatchView::new(
dock_for_build.clone(),
id.clone(),
store.clone(),
window,
cx,
)
})
},
window,
cx,
);
}
fn open_issues(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return;
};
let id = SharedString::from(format!("issues:{addr}"));
let dock = self.dock.clone();
let dock_for_build = dock.clone();
let store = self.store.clone();
crate::shared::panels::open(
&dock,
id,
move |_, cx| cx.new(|cx| IssuesView::new(dock_for_build.clone(), store.clone(), cx)),
window,
cx,
);
}
fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
if self.store.read(cx).addr().is_none() {
return self.render_local_header(cx);
}
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
let store = self.store.read(cx);
let pushing = store.pushing;
let cloning = store.cloning;
let addr = store.addr()?;
let user = Backend::global(cx).read(cx).current_user()?;
let Some(announcement) = store.announcement.clone() else {
return div().into_any_element();
if store.is_author(&user) {
return None;
}
let statuses = CheckoutsStore::global(cx).read(cx).ready_statuses_of(addr);
'status: for status in statuses {
if self.banners.dismissal(&status) {
continue;
}
for pr in &store.pull_requests {
if CheckoutsStore::pr_proposes_checkout(
pr,
store.status_of(pr) == signed_core::RepoStatus::Open,
user,
&status,
) {
continue 'status;
}
}
return Some(status);
}
None
}
fn render_ready_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
let status = self.ready_suggestion(cx)?;
let commits = if status.ahead == 1 {
SharedString::from("1 commit")
} else {
SharedString::from(format!("{} commits", status.ahead))
};
let nip05 = self
.directory
.read(cx)
.profile(&announcement.owner, cx)
.metadata()
.nip05
.clone()
.filter(|nip05| !nip05.trim().is_empty());
let share = 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: Vec<String> = announcement.clone_urls();
let name = repo_display_name(self.store.read(cx));
let description = announcement.description();
let avatar = Avatar::new(format!("{}:{}", announcement.owner, announcement.id));
let share_naddr = share.naddr.clone();
let share_for_menu = share.clone();
let announcement_for_menu = announcement.clone();
let weak_share = cx.entity().downgrade();
let weak_menu_open = cx.entity().downgrade();
let weak_menu_items = cx.entity().downgrade();
let weak_clone = cx.entity().downgrade();
let weak_download = cx.entity().downgrade();
let ngit_for_popover = ngit_command.clone();
let nak_for_popover = nak_command.clone();
let git_for_popover = git_commands.clone();
v_flex()
.flex_none()
Some(
h_flex()
.p_4()
.w_full()
.gap_6()
.border_b_1()
.border_color(cx.theme().colors.border)
.child(
h_flex()
.w_full()
.gap_4()
.items_start()
.justify_between()
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.bg(cx.theme().colors.muted)
.child(
h_flex()
.gap_2()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(avatar)
.child(name),
)
.child(
div()
.min_w_0()
.text_sm()
.text_color(cx.theme().colors.muted_foreground)
.line_clamp(2)
.child(description),
)
.when_some(fork_row(&announcement, cx), |this, row| this.child(row))
.child(
h_flex()
.mt_2()
.w_full()
.gap_1()
.child(
div()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.font_weight(gpui::FontWeight::SEMIBOLD)
.child("Maintainers:"),
)
.child(self.render_maintainers(cx)),
),
.child(Badge::new("ready-branch", status.branch.clone()))
.child(commits)
.child("ahead of")
.child(Badge::new("ready-base", status.base.clone())),
)
.child(
h_flex()
.flex_none()
.gap_2()
.justify_end()
.child({
let weak_pulls = cx.entity().downgrade();
let weak_pulls_open = cx.entity().downgrade();
let _store_for_pulls = self.store.clone();
let pulls_count = self.store.read(cx).pull_request_count();
DropdownMenu::new("pulls")
.label(format!("Pull requests {pulls_count}"))
.icon(Icon::named(CustomIconName::GitPullRequest))
.variant(ButtonVariant::Secondary)
.open(self.pulls_menu_open)
.on_click(move |_, window, cx| {
if let Err(error) = weak_pulls.update(cx, |this, cx| {
this.open_pulls(window, cx);
}) {
log::warn!("failed to open pull requests: {error}");
}
})
.on_open_change(move |open, _, cx| {
if let Err(error) = weak_pulls_open.update(cx, |this, cx| {
this.pulls_menu_open = open;
cx.notify();
}) {
log::warn!("failed to update pulls menu: {error}");
}
})
.items({
let weak_new = cx.entity().downgrade();
let weak_patch = cx.entity().downgrade();
move |_, _| {
let weak_new = weak_new.clone();
let weak_patch = weak_patch.clone();
vec![
MenuItem::new("New pull request").handler(move |window, cx| {
let _ = weak_new.update(cx, |this, cx| {
this.open_new_pull_request(window, cx);
});
}),
MenuItem::new("Send patch").handler(move |window, cx| {
let _ = weak_patch.update(cx, |this, cx| {
this.open_send_patch(window, cx);
});
}),
]
}
})
})
.child({
let weak_issues = cx.entity().downgrade();
let weak_issues_open = cx.entity().downgrade();
let store_for_issues = self.store.clone();
let issues_count = self.store.read(cx).issue_count();
DropdownMenu::new("issues")
.label(format!("Issues {issues_count}"))
.icon(Icon::named(CustomIconName::GitIssueDone))
.variant(ButtonVariant::Secondary)
.open(self.issues_menu_open)
.on_click(move |_, window, cx| {
if let Err(error) = weak_issues.update(cx, |this, cx| {
this.open_issues(window, cx);
}) {
log::warn!("failed to open issues: {error}");
}
})
.on_open_change(move |open, _, cx| {
if let Err(error) =
weak_issues_open.update(cx, |this, cx| {
this.issues_menu_open = open;
cx.notify();
})
{
log::warn!("failed to update issues menu: {error}");
}
})
.items(move |_, _| {
vec![MenuItem::new("New issue").handler({
let store = store_for_issues.clone();
move |_, cx| {
crate::issues::new_issue::open(store.clone(), cx);
}
})]
})
})
.child(
DropdownMenu::new("share")
.icon(Icon::named(IconRole::Copy))
.variant(ButtonVariant::Secondary)
.open(self.share_open)
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
share_naddr.clone(),
));
})
.on_open_change(move |open, _, cx| {
if let Err(error) = weak_share.update(cx, |this, cx| {
this.share_open = open;
cx.notify();
}) {
log::warn!("failed to update share menu: {error}");
}
})
.items(move |_, _| share_for_menu.menu()),
)
.child(
DropdownMenu::new("repo-menu")
.icon(Icon::named(CustomIconName::EllipsisVertical))
.variant(ButtonVariant::Secondary)
.loading(pushing)
.disabled(pushing)
.open(self.repo_menu_open)
.on_open_change(move |open, _, cx| {
if let Err(error) = weak_menu_open.update(cx, |this, cx| {
this.repo_menu_open = open;
cx.notify();
}) {
log::warn!("failed to update repo menu: {error}");
}
})
.items(move |_, cx| {
let owner = Backend::global(cx)
.read(cx)
.current_user()
== Some(announcement_for_menu.owner);
let mut items = vec![MenuItem::new("About").handler({
let weak = weak_menu_items.clone();
let announcement = announcement_for_menu.clone();
move |_, cx| {
if let Err(error) = weak.update(cx, |this, cx| {
dialogs::about::open(
announcement.clone(),
this.directory.clone(),
cx,
);
}) {
log::warn!("failed to open about: {error}");
}
}
})];
if owner {
items.push(MenuItem::new("Republish").handler({
let weak = weak_menu_items.clone();
move |window, cx| {
if let Err(error) = weak.update(cx, |this, cx| {
this.push_repository(window, cx);
}) {
log::warn!("failed to republish: {error}");
}
}
}));
items.push(MenuItem::new("Reset remote to local").handler({
let weak = weak_menu_items.clone();
move |window, cx| {
if let Err(error) = weak.update(cx, |this, cx| {
this.reset_remote(window, cx);
}) {
log::warn!("failed to reset remote: {error}");
}
}
}));
items.push(MenuItem::separator());
items.push(
MenuItem::new("Delete")
.destructive(true)
.handler({
let weak = weak_menu_items.clone();
move |window, cx| {
if let Err(error) =
weak.update(cx, |this, cx| {
this.delete_repository(window, cx);
})
{
log::warn!(
"failed to delete repository: {error}"
);
}
}
}),
);
}
items
}),
)
.child(
Popover::new("clone")
.open(self.clone_open)
.on_change(move |open, _, cx| {
if let Err(error) = weak_clone.update(cx, |this, cx| {
this.clone_open = open;
cx.notify();
}) {
log::warn!("failed to update clone popover: {error}");
}
})
.trigger(move |_, _| {
Button::new("clone")
.icon(Icon::named(CustomIconName::GitClone))
.tooltip(Tooltip::text("Clone"))
.loading(cloning)
.disabled(cloning)
.into_any_element()
})
.content(move |_, cx| {
v_flex()
.w(px(440.))
.p_3()
.gap_4()
.child(clone_group(
"Clone with ngit",
"copy-ngit",
&ngit_for_popover,
))
.child(clone_group(
"Clone with nak",
"copy-nak",
&nak_for_popover,
))
.child(
v_flex()
.gap_1()
.child(
div()
.text_xs()
.text_color(
cx.theme().colors.muted_foreground,
)
.child("Grasp Servers"),
)
.when(git_for_popover.is_empty(), |this| {
this.child(
div()
.text_xs()
.child("No git clone urls."),
)
})
.children(git_for_popover.iter().enumerate().map(
|(ix, command)| {
ClipboardButton::new(
SharedString::from(format!(
"copy-git-{ix}"
)),
command.clone(),
)
},
)),
)
.child(
h_flex().justify_end().child(
Button::new("download")
.icon(Icon::named(CustomIconName::GitClone))
.label("Download")
.on_click({
let weak = weak_download.clone();
move |_, window, cx| {
if let Err(error) =
weak.update(cx, |this, cx| {
this.clone_to_folder(window, cx);
})
{
log::warn!(
"failed to clone: {error}"
);
}
}
}),
),
)
.into_any_element()
}),
),
),
)
.child(self.render_tabs(cx))
.into_any_element()
}
fn render_local_header(&self, cx: &mut Context<Self>) -> AnyElement {
let name = repo_display_name(self.store.read(cx));
let path = self
.store
.read(cx)
.path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_default();
let avatar = Avatar::new(path.clone());
let bound = self.store.read(cx).nip34.clone();
let action = match bound
.as_ref()
.filter(|binding| binding.kind == Nip34Kind::Initialized)
{
Some(binding) => bound_repo_label(binding, &self.directory, cx),
None => Button::new("init")
.icon(Icon::named(CustomIconName::Init))
.label("Initialize on Nostr")
.tooltip(Tooltip::text("Publish this repository to Nostr"))
Button::new("create-pr-from-banner")
.label("Create pull request")
.small()
.on_click(cx.listener(|this, _, window, cx| {
this.open_init_dialog(window, cx);
}))
.into_any_element(),
};
v_flex()
.flex_none()
.px_4()
.pb_4()
.w_full()
.gap_6()
.border_b_1()
.border_color(cx.theme().colors.border)
.child(
h_flex()
.w_full()
.gap_4()
.items_start()
.justify_between()
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.child(
h_flex()
.gap_2()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(avatar)
.child(name),
this.open_new_pull_request(window, cx);
})),
)
.child(
div()
.min_w_0()
.text_sm()
.text_color(cx.theme().colors.muted_foreground)
.child(path),
Button::new("dismiss-ready-banner")
.icon(Icon::named(IconRole::Close))
.variant(ButtonVariant::Ghost)
.small()
.on_click(cx.listener(move |this, _, _, cx| {
this.banners.dismiss(&status);
cx.notify();
})),
),
)
.child(action),
.into_any_element(),
)
.child(self.render_tabs(cx))
.into_any_element()
}
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
let count = self.history.read(cx).commit_count();
let muted = cx.theme().colors.muted_foreground;
let weak = cx.entity().downgrade();
let commits_label = match count {
Some(count) => SharedString::from(format!("Commits ({count})")),
None => SharedString::from("Commits"),
};
Tabs::new("repo-tabs")
.selected(self.active_tab)
.tabs(move |_, _| {
vec![
Tab::new("files", "Files")
.lead(Icon::named(CustomIconName::GitFile).color(muted)),
Tab::new("commits", commits_label.clone())
.lead(Icon::named(CustomIconName::GitCommit).color(muted)),
]
})
.on_change(move |index, _, cx| {
if let Err(error) = weak.update(cx, |this, cx| {
this.active_tab = index;
cx.notify();
}) {
log::warn!("failed to switch repo tab: {error}");
}
})
.into_any_element()
}
fn render_maintainers(&self, cx: &mut Context<Self>) -> AnyElement {
let Some(announcement) = self.announcement(cx) else {
return div().into_any_element();
};
let mut seen = HashSet::new();
let rest: Vec<_> = announcement
.maintainers
.iter()
.copied()
.filter(|key| key != &announcement.owner && seen.insert(*key))
.collect();
let owner = self.directory.read(cx).profile(&announcement.owner, cx);
let owner_name = owner.name();
h_flex()
.w_full()
.gap_3()
.child(
h_flex()
.gap_2()
.child(
h_flex()
.gap_1()
.child(
Avatar::new(announcement.owner)
.name(owner_name.clone())
.picture(owner.picture())
.small(),
)
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
)
.when(!rest.is_empty(), |this| {
this.child(
div()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.child(SharedString::from(format!("+{}", rest.len()))),
)
}),
)
.into_any_element()
}
fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
@@ -1758,7 +1198,8 @@ impl Panel for RepoDetailView {
impl Render for RepoDetailView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let banner = self
.render_push_banner(cx)
.render_ready_banner(cx)
.or_else(|| self.render_push_banner(cx))
.or_else(|| self.render_sync_banner(cx));
let error = self.error.clone().or_else(|| {
+4 -15
View File
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use gpui::{
AnyElement, App, AppContext as _, Entity, IntoElement, ParentElement as _, PathPromptOptions,
SharedString, Styled as _, WeakEntity, Window, div, px,
SharedString, Styled as _, Window, div, px,
};
use gpui_native_kit::button::Button;
use gpui_native_kit::common::{h_flex, v_flex};
@@ -13,7 +13,6 @@ use gpui_native_kit::theme::ActiveTheme as _;
use gpui_native_kit::tooltip::Tooltip;
use settings::{AppearanceMode, EventFetchingStrategy, SettingsStore};
use super::SettingsDialog;
use crate::repo::dialogs::grasp_servers::{normalize_server, server_host};
/// A settings row with the label and description on the left and the control on the right.
@@ -306,10 +305,7 @@ fn add_scan_path(cx: &mut App) {
.detach();
}
pub fn default_folder_selector(
default_folder: &Entity<InputState>,
weak: WeakEntity<SettingsDialog>,
) -> AnyElement {
pub fn default_folder_selector(default_folder: &Entity<InputState>) -> AnyElement {
let folder = default_folder.clone();
h_flex()
.gap_1()
@@ -324,21 +320,15 @@ pub fn default_folder_selector(
.variant(gpui_native_kit::button::ButtonVariant::Ghost)
.tooltip(Tooltip::text("Choose folder"))
.on_click(move |_, window, cx| {
choose_default_folder(&folder, &weak, window, cx);
choose_default_folder(&folder, window, cx);
}),
)
.into_any_element()
}
fn choose_default_folder(
default_folder: &Entity<InputState>,
weak: &WeakEntity<SettingsDialog>,
window: &mut Window,
cx: &mut App,
) {
fn choose_default_folder(default_folder: &Entity<InputState>, window: &mut Window, cx: &mut App) {
let handle = window.window_handle();
let default_folder = default_folder.clone();
let weak = weak.clone();
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
directories: true,
@@ -359,7 +349,6 @@ fn choose_default_folder(
);
});
default_folder.update(cx, |input, cx| input.set_value(text, window, cx));
let _ = weak.update(cx, |_this, cx| cx.notify());
}) {
log::warn!("failed to apply default folder: {error}");
}
+2 -3
View File
@@ -46,10 +46,9 @@ impl SettingsDialog {
}
impl Render for SettingsDialog {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let grasp_input = self.grasp_input.clone();
let default_folder = self.default_folder.clone();
let weak = cx.entity().downgrade();
Dialog::new("settings-dialog")
.open(true)
@@ -95,7 +94,7 @@ impl Render for SettingsDialog {
cx,
"Default Folder",
"The folder for newly created repositories.",
default_folder_selector(&default_folder, weak.clone()),
default_folder_selector(&default_folder),
))
.into_any_element()
})
+9 -9
View File
@@ -37,6 +37,15 @@ impl DialogHost {
}
}
impl Render for DialogHost {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.absolute()
.inset_0()
.when_some(self.open.clone(), |host, view| host.child(view))
}
}
/// Replaces the mounted modal with `view`.
pub fn open_global(view: impl Into<AnyView>, cx: &mut App) {
let host = DialogHost::global(cx);
@@ -48,12 +57,3 @@ pub fn close_global(cx: &mut App) {
let host = DialogHost::global(cx);
host.update(cx, |host, cx| host.close(cx));
}
impl Render for DialogHost {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.absolute()
.inset_0()
.when_some(self.open.clone(), |host, view| host.child(view))
}
}
+13
View File
@@ -3,3 +3,16 @@ pub mod panels;
pub mod progress;
pub mod tree;
pub mod widgets;
/// Logs a discarded `Result` with its source location instead of dropping it silently.
#[track_caller]
pub fn log_dropped<T>(result: Result<T, anyhow::Error>) {
if let Err(error) = result {
let location = std::panic::Location::caller();
log::warn!(
"{}:{}: dropped result: {error}",
location.file(),
location.line()
);
}
}
+43 -22
View File
@@ -34,6 +34,16 @@ use crate::settings;
mod helpers;
#[derive(Clone, Copy)]
enum SidebarSlot {
Inbox,
Browse,
Search,
Header,
Repo(usize),
Local(usize),
}
pub struct SidebarView {
weak: WeakEntity<Self>,
dock: WeakEntity<DockState>,
@@ -333,6 +343,10 @@ impl SidebarView {
);
}
debug_assert_eq!(
entries.len(),
slots(announcements.len(), local_repos.len()).len()
);
entries
})
.on_change(move |index, window, cx| {
@@ -346,25 +360,19 @@ impl SidebarView {
let Some(index) = index else {
return;
};
match index {
0 => {
self.open_inbox(window, cx);
return;
}
1 | 2 => {
self.open_explore(window, cx);
return;
}
_ => {}
}
let Some(row) = index.checked_sub(4) else {
let Some(slot) = slots(self.announcements.len(), self.local_repos.len())
.get(index)
.copied()
else {
return;
};
if row < self.announcements.len() {
let announcement = self.announcements[row].clone();
match slot {
SidebarSlot::Inbox => self.open_inbox(window, cx),
SidebarSlot::Browse | SidebarSlot::Search => self.open_explore(window, cx),
SidebarSlot::Header => {}
SidebarSlot::Repo(ix) => {
if let Some(announcement) = self.announcements.get(ix).cloned() {
let addr = announcement.addr();
crate::repo::actions::open_repo_panel(
&self.dock,
@@ -373,14 +381,15 @@ impl SidebarView {
window,
cx,
);
return;
}
let local_ix = row - self.announcements.len();
if let Some(entry) = self.local_repos.get(local_ix).cloned() {
}
SidebarSlot::Local(ix) => {
if let Some(entry) = self.local_repos.get(ix).cloned() {
self.open_local_entry(entry, window, cx);
}
}
}
}
/// A local repository opens as the announced repository when its binding matches one.
fn open_local_entry(
@@ -456,9 +465,9 @@ impl SidebarView {
.label("Guide")
.variant(ButtonVariant::Ghost)
.on_click(move |_, window, cx| {
let _ = guide_weak.update(cx, |this, cx| {
crate::shared::log_dropped(guide_weak.update(cx, |this, cx| {
this.open_explore(window, cx);
});
}));
}),
)
.child(
@@ -660,3 +669,15 @@ impl SidebarView {
)
}
}
fn slots(announcements: usize, local_repos: usize) -> Vec<SidebarSlot> {
let mut slots = vec![
SidebarSlot::Inbox,
SidebarSlot::Browse,
SidebarSlot::Search,
SidebarSlot::Header,
];
slots.extend((0..announcements).map(SidebarSlot::Repo));
slots.extend((0..local_repos).map(SidebarSlot::Local));
slots
}