This commit is contained in:
2026-09-13 13:36:27 +07:00
parent f8277e4c2e
commit b3991810b6
77 changed files with 96 additions and 5189 deletions
@@ -25,22 +25,15 @@ use crate::views::repo::helpers::{
DIFF_ROW_HEIGHT, DiffRow, build_tree_items, diff_rows, find_item, render_diff_row, tree_items,
};
/// Width of the changed-files column.
const TREE_WIDTH: f32 = 260.;
/// Tree and per-file diff body, shared by the commit diff and compare views.
pub struct DiffPane {
/// Loaded diff, `None` until [`Self::set_diff`] is called.
diff: Option<CommitDiff>,
/// Changed-files explorer state.
tree_state: Entity<TreeState>,
/// Path of the file whose diff is shown in the detail column.
selected_file: Option<SharedString>,
/// Rows of the selected file's diff, hunk headers and lines.
rows: Vec<DiffRow>,
/// Per-row heights of [`Self::rows`].
item_sizes: Rc<Vec<Size<Pixels>>>,
/// Virtual list state of the diff rows.
scroll_handle: VirtualListScrollHandle,
}
@@ -56,12 +49,10 @@ impl DiffPane {
}
}
/// The loaded diff, for stats and badges in the host's header.
pub fn diff(&self) -> Option<&CommitDiff> {
self.diff.as_ref()
}
/// Replace the diff and rebuild the tree and the selected file's rows.
pub fn set_diff(&mut self, diff: CommitDiff, cx: &mut Context<Self>) {
let mut paths: Vec<PathBuf> = diff
.files
@@ -86,9 +77,6 @@ impl DiffPane {
}
}
/// Forget the diff, e.g. when the compared branches changed.
///
/// Clears the tree, the selection and the diff rows.
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.diff = None;
self.selected_file = None;
@@ -99,14 +87,12 @@ impl DiffPane {
});
}
/// Show the diff of the file at `path`, selected in the tree.
fn select_file(&mut self, path: &str, cx: &mut Context<Self>) {
self.selected_file = Some(path.into());
self.set_diff_rows(path);
cx.notify();
}
/// Rebuild the virtual list state for `path` and scroll back to the top.
fn set_diff_rows(&mut self, path: &str) {
let Some(diff) = self.diff.as_ref() else {
return;
@@ -119,7 +105,6 @@ impl DiffPane {
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
}
/// One row of the changed-files tree, icon and name, indented by depth.
fn render_tree_item(
ix: usize,
entry: &TreeEntry,
@@ -136,7 +121,6 @@ impl DiffPane {
})
}
/// Left column showing the changed-files tree.
fn render_tree_column(&self, cx: &mut Context<Self>) -> AnyElement {
let tree_state = self.tree_state.clone();
let view = cx.entity().downgrade();
@@ -166,7 +150,6 @@ impl DiffPane {
.into_any_element()
}
/// Right column, header of the selected file plus its diff.
fn render_detail_column(&self, cx: &mut Context<Self>) -> AnyElement {
let Some(diff) = self.diff.as_ref() else {
return placeholder("No changes", cx);
@@ -184,7 +167,6 @@ impl DiffPane {
self.render_file_diff(file, cx.entity(), cx)
}
/// The diff of one file, with a header showing status and stats.
fn render_file_diff(&self, file: &FileDiff, view: Entity<Self>, cx: &App) -> AnyElement {
let status_label = match file.status {
DiffStatus::Added => "A",
@@ -311,19 +293,14 @@ impl Render for DiffPane {
}
}
/// Detail panel showing the diff of one commit.
pub struct CommitDiffView {
focus_handle: FocusHandle,
/// Local clone the commit lives in.
worktree: PathBuf,
/// Display name of the repository the commit belongs to.
repo_name: SharedString,
/// The commit being shown in the header and tab title.
commit: FileCommit,
/// The diff is being computed on a background task.
loading: bool,
error: Option<SharedString>,
/// Changed-files explorer and per-file diff, also used by the new PR panel's compare view.
pane: Entity<DiffPane>,
}
@@ -359,7 +336,6 @@ impl CommitDiffView {
}
}
/// Load the commit diff and the full commit metadata.
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.loading = true;
self.error = None;
@@ -407,7 +383,6 @@ impl CommitDiffView {
task.detach();
}
/// Header with the commit id, summary, author/time and overall change stats.
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
let commit = &self.commit;
let (files, insertions, deletions) = self.pane.read(cx).diff().map_or((0, 0, 0), |diff| {
+3 -4
View File
@@ -2,8 +2,7 @@ use gpui::prelude::*;
use gpui::{AnyElement, App, SharedString, div};
use gpui_component::ActiveTheme;
/// Progress of an async dialog action: a busy flag disabling the form,
/// and an error line shown under it.
/// Progress of an async dialog action: a busy flag that disables the form and an error shown below it.
#[derive(Debug, Default)]
pub struct DialogProgress {
pub busy: bool,
@@ -11,13 +10,13 @@ pub struct DialogProgress {
}
impl DialogProgress {
/// An action started, disable the form and clear the previous error.
/// Marks an action as started, disabling the form and clearing the previous error.
pub fn begin(&mut self) {
self.busy = true;
self.error = None;
}
/// An action failed, re-enable the form and surface `message`.
/// Marks an action as failed, enabling the form and showing `message`.
pub fn fail(&mut self, message: impl Into<SharedString>) {
self.busy = false;
self.error = Some(message.into());
+1 -28
View File
@@ -21,20 +21,13 @@ use utils::relative_time;
use super::{RepoItem, open_repo_item};
/// Delay between a refresh request and the actual re-query.
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
/// Extra list rows measured above and below the visible area.
const LIST_OVERDRAW: Pixels = px(400.);
/// Maximum number of sub-activity lines shown under a thread row.
const MAX_SUB_ACTIVITIES: usize = 5;
/// A repository's slice of the inbox: the threads that belong to it.
struct InboxSection {
/// Repository the section groups, `None` for items without one.
/// `None` for items without a repository.
address: Option<RepoAddr>,
/// Number of threads with an unread event.
unread: usize,
/// Indices into the threads, newest activity first.
entries: Vec<usize>,
@@ -54,15 +47,11 @@ pub struct InboxView {
dock_area: WeakEntity<DockArea>,
/// One row per thread, merging notifications and own activity, newest first.
threads: Arc<Vec<InboxItem>>,
/// The threads grouped by repository, newest first.
sections: Arc<Vec<InboxSection>>,
/// The flattened repository headers and rows of the list.
rows: Arc<Vec<InboxRow>>,
/// Number of non-archived threads with an unread event.
unread_count: usize,
/// Copy of the global read state the current lists were derived with.
state: InboxReadState,
/// Set once the global state has been read for the current user.
state_loaded: bool,
refresh: RefreshGate,
list: ListState,
@@ -118,7 +107,6 @@ impl InboxView {
}
}
/// Mark every known notification read.
pub fn mark_all_read(&mut self, cx: &mut Context<Self>) {
let Some(me) = Backend::global(cx).read(cx).current_user() else {
return;
@@ -136,7 +124,6 @@ impl InboxView {
inbox.update(cx, |inbox, cx| inbox.mark_all_read(&all, me, cx));
}
/// Re-derive from the global state when it is loaded or changes.
pub fn sync_state(&mut self, cx: &mut Context<Self>) {
let backend = Backend::global(cx);
let inbox = backend.read(cx).inbox();
@@ -170,7 +157,6 @@ impl InboxView {
}
}
/// Handle a backend event that can change the derived sections.
fn handle_backend_event(&mut self, event: &BackendEvent, cx: &mut Context<Self>) {
match event {
BackendEvent::NostrUpdate(updates) => {
@@ -192,7 +178,6 @@ impl InboxView {
}
}
/// One-shot initial load, no debounce.
fn refresh_initial(&mut self, cx: &mut Context<Self>) {
debug_assert!(!self.refresh.debouncing());
if self.refresh.running() {
@@ -202,7 +187,6 @@ impl InboxView {
self.run_refresh(cx);
}
/// Re-query the local database.
fn refresh(&mut self, cx: &mut Context<Self>) {
if !self.state_loaded {
return;
@@ -218,7 +202,6 @@ impl InboxView {
}));
}
/// One query and apply cycle, the debounced entry point.
fn run_refresh(&mut self, cx: &mut Context<Self>) {
self.refresh.begin();
@@ -264,7 +247,6 @@ impl InboxView {
}));
}
/// Recompute the unread and archived flags from the current state.
fn regroup(&mut self, cx: &mut Context<Self>) {
let mut items = (*self.threads).clone();
@@ -277,7 +259,6 @@ impl InboxView {
self.rebuild(cx);
}
/// Regroup the current threads by repository and flatten them into rows.
fn rebuild(&mut self, cx: &mut Context<Self>) {
let backend = Backend::global(cx);
let repo_list = RepoListStore::global(cx);
@@ -308,7 +289,6 @@ impl InboxView {
self.rows = Arc::new(rows);
}
/// Group the threads into one section per repository.
fn group_sections(&self) -> Vec<InboxSection> {
let mut by_repo: HashMap<Option<RepoAddr>, InboxSection> = HashMap::new();
@@ -349,7 +329,6 @@ impl InboxView {
sections
}
/// Flatten the sections into the list of repository headers and their rows.
fn flatten_rows(&self, sections: &[InboxSection]) -> Vec<InboxRow> {
let mut rows = Vec::new();
@@ -369,7 +348,6 @@ impl InboxView {
rows
}
/// Forget everything derived for the current user.
fn clear(&mut self) {
self.threads = Arc::new(Vec::new());
self.sections = Arc::new(Vec::new());
@@ -445,7 +423,6 @@ impl InboxView {
}
}
/// Display name of the repository at `addr`, from the announcement store.
fn repo_name(addr: Option<&RepoAddr>, cx: &App) -> Option<SharedString> {
let repo_list = RepoListStore::global(cx);
let addr = addr?;
@@ -457,7 +434,6 @@ fn repo_name(addr: Option<&RepoAddr>, cx: &App) -> Option<SharedString> {
.map(|announcement| announcement.name().map(SharedString::from))
}
/// Header of a repository section.
fn repo_header(section: &InboxSection, cx: &App) -> AnyElement {
let name =
repo_name(section.address.as_ref(), cx).unwrap_or_else(|| SharedString::from("Untitled"));
@@ -481,7 +457,6 @@ fn repo_header(section: &InboxSection, cx: &App) -> AnyElement {
.into_any_element()
}
/// Placeholder under a repository header that has nothing to show.
fn empty_section_row(cx: &App) -> AnyElement {
h_flex()
.h_12()
@@ -600,7 +575,6 @@ fn sub_activity(event: &Event, me: Option<PublicKey>, cx: &App) -> AnyElement {
.into_any_element()
}
/// Phrase describing an activity event, read as `[name] [phrase]`.
fn activity_phrase(kind: Kind) -> &'static str {
if kind == COVER_NOTE_KIND {
return "added a note";
@@ -620,7 +594,6 @@ fn activity_phrase(kind: Kind) -> &'static str {
}
}
/// Centered muted icon and message filling its container.
fn empty_state(icon: impl IconNamed, message: &str, cx: &App) -> AnyElement {
v_flex()
.w_full()
@@ -15,13 +15,10 @@ use utils::relative_time;
use crate::views::repo::helpers::{comment_form, comments_section, issue_roots, sidebar_section};
/// Detail panel of a single issue.
pub struct IssueDetailView {
focus_handle: FocusHandle,
/// Repo store holding the issues and their statuses.
store: Entity<RepoStore>,
issue_id: EventId,
/// Input state of the comment textarea.
comment_input: Entity<TextareaState>,
_subscription: Subscription,
}
+2 -21
View File
@@ -25,22 +25,16 @@ pub(super) mod detail;
use self::detail::IssueDetailView;
/// Height of one issue row in the virtual list.
const ISSUE_ROW_HEIGHT: f32 = 73.;
/// Status filter of the issues list, chosen via the header's filter buttons.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IssueFilter {
/// Every issue, regardless of status.
All,
/// Issues whose resolved status is [`RepoStatus::Open`].
Open,
/// Issues whose resolved status is [`RepoStatus::Closed`].
Closed,
}
impl IssueFilter {
/// Whether an issue with `status` is included by this filter.
fn matches(self, status: RepoStatus) -> bool {
match self {
Self::All => true,
@@ -52,28 +46,17 @@ impl IssueFilter {
pub struct IssuesView {
focus_handle: FocusHandle,
/// Dock area the issue detail panel is opened in.
dock_area: WeakEntity<DockArea>,
/// Repo store holding the issues and their statuses.
store: Entity<RepoStore>,
/// Display name of the repository, for the panel title.
repo_name: SharedString,
/// Filter selected in the header filter buttons.
filter: IssueFilter,
/// Per-row heights of the virtual list.
item_sizes: Rc<Vec<Size<Pixels>>>,
/// Indices into the store's `issues` matching [`Self::filter`].
visible_issues: Vec<usize>,
/// Header counts `(total, open, closed)`, rebuilt with [`Self::visible_issues`].
counts: (usize, usize, usize),
/// Filter [`Self::visible_issues`] was last rebuilt for.
///
/// A filter change notifies even when the visible rows are unchanged,
/// e.g. switching between two empty filters.
// A filter change notifies even when the visible rows are unchanged,
// e.g. switching between two empty filters.
synced_filter: IssueFilter,
/// Virtual list state of the issues list.
scroll_handle: VirtualListScrollHandle,
/// Rebuild the rows and re-render when the store's data changes.
_subscription: Subscription,
}
@@ -235,7 +218,6 @@ impl IssuesView {
}
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
// Counts of the last list rebuild.
let (total, open, closed) = self.counts;
h_flex()
@@ -293,7 +275,6 @@ impl IssuesView {
}
}
/// Open the new issue dialog, a title and a content input.
pub(super) fn open_new_issue_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Issue title"));
let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue..."));
@@ -33,14 +33,11 @@ use utils::{relative_time, relative_time_secs};
use crate::views::commit_diff::{CommitDiffView, DiffPane};
use crate::views::repo::helpers::{comment_form, comments_section, pr_roots, sidebar_section};
/// Height of one commit row in the commits tab's virtual list.
const ROW_HEIGHT: f32 = 37.;
/// Shown once the store's first pass is applied and the root PR is still absent.
const NOT_FOUND: &str = "Pull request not found";
/// Root PR inputs one diff load is keyed to.
///
/// A store refresh re-binds the panel, and reloads only when these change.
#[derive(Clone, PartialEq, Eq)]
struct PrBinding {
@@ -56,22 +53,16 @@ struct PrBinding {
/// Detail panel of a single pull request.
pub struct PullRequestDetailView {
focus_handle: FocusHandle,
/// Dock area where new panels, e.g. commit diffs, are added.
dock_area: WeakEntity<DockArea>,
/// Repo store holding the PR, its status and comments.
store: Entity<RepoStore>,
/// Event id of the root PR event, kind 1618.
/// Updates are revisions.
/// Event id of the root PR event, kind 1618. Updates are revisions.
pr_id: EventId,
/// Input state of the comment textarea.
comment_input: Entity<TextareaState>,
/// Display name of the repository, for panels opened from here.
repo_name: SharedString,
/// Local clone the PR's git changes come from.
worktree: Option<PathBuf>,
/// Root PR's content, shown as plain text.
description: SharedString,
/// Tip commit of the PR, the latest update's `c` tag or the root's.
/// Tip commit of the PR, from the latest update's `c` tag or the root.
current_commit: Option<SharedString>,
/// Commits of the patch series, in patch order, oldest first.
commits: Vec<FileCommit>,
@@ -82,17 +73,13 @@ pub struct PullRequestDetailView {
bound: Option<PrBinding>,
/// Generation of the in-flight diff load. Stale results are discarded.
load_generation: u64,
/// Active header tab, 0 = Discussion, 1 = Files, 2 = Commits.
/// 0 = Discussion, 1 = Files, 2 = Commits.
active_tab: usize,
/// Changed-files explorer and per-file diff, like the commit and compare views.
pane: Entity<DiffPane>,
/// Per-row heights of the commits tab's virtual list, built when the patch series loads.
commit_item_sizes: Rc<Vec<Size<Pixels>>>,
/// Virtual list state of the commits tab.
commit_scroll_handle: VirtualListScrollHandle,
/// Re-render when the store's first pass or a later refresh lands.
/// Item panels are cached by the dock, so without this observer a panel
/// opened before the store loaded would stay on its placeholder.
/// The dock caches item panels, so without this observer a panel opened
/// before the store loaded would stay on its placeholder.
_subscription: Subscription,
}
@@ -523,9 +510,6 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Full-height Commits tab.
///
/// Every commit of the patch series, or a status message while loading or empty.
fn render_commits_tab(&self, cx: &mut Context<Self>) -> AnyElement {
if self.loading {
return v_flex()
@@ -573,9 +557,6 @@ impl PullRequestDetailView {
.into_any_element()
}
/// One row of the commits tab, id, summary, author and time.
///
/// Clicking a row opens the commit's diff in the bottom dock.
fn render_commit_row(
&self,
ix: usize,
@@ -628,7 +609,6 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Always-visible header with a status badge and title, like the issue panel.
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
let current_commit = self.current_commit.clone();
let (title, status, branch, author) = {
@@ -730,7 +710,6 @@ impl PullRequestDetailView {
}
}
/// Open the update pull request dialog.
fn open_update_pull_request_dialog(
store: Entity<RepoStore>,
root: Event,
@@ -791,7 +770,6 @@ fn open_update_pull_request_dialog(
});
}
/// The `c` tag of a PR event, the commit the proposal points at.
/// One-line commit metadata for the commits list.
///
/// Author and relative time, whichever is available.
@@ -849,26 +827,3 @@ impl Render for PullRequestDetailView {
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const COMMIT_HEX: &str = "1111111111111111111111111111111111111111";
#[test]
fn commit_meta_combines_author_and_time() {
let commit = |author: &str, time: i64| FileCommit {
id: COMMIT_HEX.into(),
summary: "summary".into(),
description: None,
author: author.into(),
time,
};
assert_eq!(commit_meta(&commit("Alice", 0)), "Alice");
assert_eq!(commit_meta(&commit("", 0)), "");
assert!(!commit_meta(&commit("", 1_000_000)).is_empty());
assert!(!commit_meta(&commit("Alice", 1_000_000)).is_empty());
}
}
@@ -27,26 +27,18 @@ use self::new::open_new_pull_panel;
use super::send_patch::open_send_patch_panel;
use crate::views::repo::RepoAction;
/// Height of one pull request row in the virtual list.
const ROW_HEIGHT: f32 = 73.;
/// Status filter of the pull request list, chosen via the header's filter buttons.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PullRequestFilter {
/// Every pull request, regardless of status.
All,
/// Pull requests whose resolved status is [`RepoStatus::Open`].
Open,
/// Pull requests whose resolved status is [`RepoStatus::Closed`].
Closed,
/// Pull requests whose resolved status is [`RepoStatus::Draft`].
Draft,
/// Pull requests whose resolved status is [`RepoStatus::Applied`].
Merged,
}
impl PullRequestFilter {
/// Whether a pull request with `status` is included by this filter.
fn matches(self, status: RepoStatus) -> bool {
match self {
Self::All => true,
@@ -60,28 +52,19 @@ impl PullRequestFilter {
pub struct PullRequestsView {
focus_handle: FocusHandle,
/// Dock area the detail panels are added to.
dock_area: WeakEntity<DockArea>,
/// Repo store holding the pull requests and their statuses.
store: Entity<RepoStore>,
/// Display name of the repository, for the panel title.
repo_name: SharedString,
/// Filter selected in the header filter buttons.
filter: PullRequestFilter,
/// Per-row heights of the virtual list.
item_sizes: Rc<Vec<Size<Pixels>>>,
/// Indices into the store's `pull_requests` matching [`Self::filter`].
visible_prs: Vec<usize>,
/// Header counts `(total, open, closed, draft, merged)`.
counts: (usize, usize, usize, usize, usize),
/// Filter [`Self::visible_prs`] was last rebuilt for.
///
/// A filter change notifies even when the visible rows are unchanged,
/// e.g. switching between two empty filters.
// A filter change notifies even when the visible rows are unchanged,
// e.g. switching between two empty filters.
synced_filter: PullRequestFilter,
/// Virtual list state of the pull requests list.
scroll_handle: VirtualListScrollHandle,
/// Rebuild the rows and re-render when the store's data changes.
_subscription: Subscription,
}
@@ -117,7 +100,6 @@ impl PullRequestsView {
}
}
/// Rebuild the visible rows, header counts and virtual-list sizes.
fn rebuild(&mut self, cx: &mut Context<Self>) {
let filter = self.filter;
@@ -165,7 +147,6 @@ impl PullRequestsView {
cx.notify();
}
/// Open the detail panel of `pr_id` in the dock area.
fn open_pull_request_detail(
&mut self,
pr_id: EventId,
@@ -191,9 +172,6 @@ impl PullRequestsView {
});
}
/// Render one row of the pull request list.
///
/// `ix` is the row index, `pr_ix` the index in the store's `pull_requests`.
fn render_row(&self, ix: usize, pr_ix: usize, cx: &mut Context<Self>) -> AnyElement {
let pr = &self.store.read(cx).pull_requests[pr_ix];
let pr_id = pr.id;
@@ -258,7 +236,6 @@ impl PullRequestsView {
}
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
// Counts of the last list rebuild.
let (total, open, closed, draft, merged) = self.counts;
h_flex()
@@ -397,8 +374,6 @@ impl Render for PullRequestsView {
let scroll_handle = self.scroll_handle.clone();
let view = cx.entity().clone();
// Non-fatal warnings and errors of the last action, like creating or updating a PR.
// Shown as dismissible banners above the list.
let (last_error, last_warning) = {
let store = self.store.read(cx);
(store.last_error.clone(), store.last_warning.clone())
@@ -32,20 +32,15 @@ use signed_ui::{CountBadge, placeholder};
use crate::views::commit_diff::{CommitDiffView, DiffPane};
use crate::views::repo::helpers::{COMMIT_ROW_HEIGHT, commit_row, ref_selector_trigger};
/// The new pull request panel of a repository.
pub struct NewPullRequestView {
focus_handle: FocusHandle,
/// Dock area the panel lives in, commit diffs are opened there.
dock_area: WeakEntity<DockArea>,
/// Store of the target repository, source of the announced HEAD default.
store: Entity<RepoStore>,
/// Display name of the repository, for the panel title.
repo_name: SharedString,
/// The user's local checkout.
repo_path: Option<PathBuf>,
/// Branches of the checkout, backing both selectors in checkout mode.
/// Backs both selectors in checkout mode.
branches: Vec<SharedString>,
/// Fork-backed compare state.
fork: Option<ForkCompare>,
/// Selected base branch, the PR target, stored as a short name.
base: SharedString,
@@ -53,33 +48,27 @@ pub struct NewPullRequestView {
compare: SharedString,
base_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
compare_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
/// Title input, required.
subject: Entity<InputState>,
/// Description input, optional.
description: Entity<TextareaState>,
/// Merge base of the selected branches, `None` until the compare loads.
merge_base: Option<String>,
/// Commits in `merge_base..compare`, newest first.
commits: Option<Vec<signed_git::FileCommit>>,
/// The compare is being computed.
loading: bool,
/// Error of the last compare or submit attempt.
error: Option<SharedString>,
/// A submit, patch generation and publish, is in flight.
submitting: bool,
/// Bumped on every branch switch, stale compare results are discarded.
compare_generation: u64,
/// Active tab, 0 = Files and 1 = Commits.
/// 0 = Files, 1 = Commits.
active_tab: usize,
/// The compare diff, the Files tab body.
pane: Entity<DiffPane>,
/// Virtual list state of the Commits tab.
scroll_handle: VirtualListScrollHandle,
item_sizes: Rc<Vec<Size<Pixels>>>,
_subscriptions: Vec<Subscription>,
}
/// A fork-backed compare.
struct ForkCompare {
/// Fork announcement the compare branch is imported from.
announcement: Announcement,
@@ -90,20 +79,15 @@ struct ForkCompare {
}
impl ForkCompare {
/// The full ref of the base branch `name` in the mirror.
fn base_ref(name: &str) -> String {
format!("refs/remotes/origin/{name}")
}
/// The full ref of the compare branch `name` in the mirror.
fn compare_ref(&self, name: &str) -> String {
format!("refs/fork/{}/{}", self.namespace, name)
}
}
/// The display name of an announcement.
///
/// Its human-readable name, falling back to the repository id.
fn fork_display_name(announcement: &Announcement) -> SharedString {
announcement
.name
@@ -112,7 +96,6 @@ fn fork_display_name(announcement: &Announcement) -> SharedString {
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
}
/// A short label of a fork's owner for the source picker, a hex prefix.
fn shorten_owner(owner: &PublicKey) -> String {
let hex = owner.to_hex();
hex.chars().take(10).collect()
@@ -131,9 +114,6 @@ fn truncate_label(label: &str) -> SharedString {
SharedString::from(label)
}
/// The compare-source menu entry of one local checkout folder.
///
/// Applies the folder directly, no picker.
fn checkout_source_item(
view: WeakEntity<NewPullRequestView>,
path: PathBuf,
@@ -162,7 +142,6 @@ fn checkout_source_item(
})
}
/// The compare-source menu entry prompting for an arbitrary folder.
fn choose_folder_source_item(view: WeakEntity<NewPullRequestView>) -> PopupMenuItem {
PopupMenuItem::element(move |_window, cx| {
source_row(
@@ -179,9 +158,6 @@ fn choose_folder_source_item(view: WeakEntity<NewPullRequestView>) -> PopupMenuI
})
}
/// The compare-source menu entry of one announced fork.
///
/// Imports its branches into the target's mirror and switches the panel to fork mode.
fn fork_source_item(
view: WeakEntity<NewPullRequestView>,
announcement: Announcement,
@@ -207,7 +183,6 @@ fn fork_source_item(
})
}
/// One row of the compare-source menu, icon, title and a muted subtitle.
fn source_row<T>(icon: impl Into<Icon>, title: T, subtitle: T, cx: &App) -> AnyElement
where
T: Into<SharedString>,
@@ -350,7 +325,6 @@ impl NewPullRequestView {
}
}
/// Whether a compare source, a checkout or a fork, is applied.
fn has_source(&self) -> bool {
self.repo_path.is_some() || self.fork.is_some()
}
@@ -387,7 +361,6 @@ impl NewPullRequestView {
}
}
/// Prompt for a local checkout.
fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
@@ -418,9 +391,7 @@ impl NewPullRequestView {
task.detach();
}
/// Apply `path` as the local checkout, no picker.
///
/// Branches and current branch are read off the UI thread, then applied.
/// Branches and the current branch are read off the UI thread, then applied.
fn apply_folder_path(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) {
let path = path.to_string_lossy().to_string();
@@ -449,7 +420,6 @@ impl NewPullRequestView {
task.detach();
}
/// Apply a picked checkout, filling the selectors and loading the compare.
fn apply_checkout(
&mut self,
path: String,
@@ -527,8 +497,6 @@ impl NewPullRequestView {
self.reload_compare(window, cx);
}
/// The base repository of the panel, its address and announced EUC.
///
/// Used to find fork candidates. `None` while the repository is not announced.
fn base_repo(&self, cx: &App) -> Option<(RepoAddr, Option<String>)> {
let store = self.store.read(cx);
@@ -552,7 +520,6 @@ impl NewPullRequestView {
.collect()
}
/// Compare against an announced fork.
fn choose_fork(
&mut self,
announcement: Announcement,
@@ -607,9 +574,6 @@ impl NewPullRequestView {
let clone_urls = clone_urls.clone();
let mirror_path = mirror_path.clone();
async move {
// The fork and base must share history for a merge-base to exist.
// The target's mirror is the object store both sides land in.
// `ensure_clone` fetches `origin` when the mirror already exists.
cache.ensure_clone(&base, &base_clone_urls)?;
// Prune stale imports of any fork.
@@ -678,7 +642,6 @@ impl NewPullRequestView {
task.detach();
}
/// Apply an imported fork, filling the selectors and loading the compare.
#[allow(clippy::too_many_arguments)]
fn apply_fork(
&mut self,
@@ -783,7 +746,6 @@ impl NewPullRequestView {
self.reload_compare(window, cx);
}
/// Recompute `merge_base..compare` of the selected branches on a background task.
fn reload_compare(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(repo_path) = self.work_path() else {
return;
@@ -877,7 +839,6 @@ impl NewPullRequestView {
task.detach();
}
/// Publish the pull request.
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.submitting || self.loading {
return;
@@ -959,7 +920,6 @@ impl NewPullRequestView {
task.detach();
}
/// Open the diff of `commit_id`, from the Commits tab, in a new panel.
fn open_commit_diff(&mut self, commit_id: &str, window: &mut Window, cx: &mut Context<Self>) {
let Some(repo_path) = self.work_path() else {
return;
@@ -1117,7 +1077,6 @@ impl NewPullRequestView {
.into_any_element()
}
/// The source picker's trigger, a truncated label of the applied source.
fn source_trigger(&self) -> SharedString {
match &self.fork {
Some(fork) => truncate_label(&fork_display_name(&fork.announcement)),
@@ -1128,7 +1087,6 @@ impl NewPullRequestView {
}
}
/// Build the compare-source menu.
fn source_menu(
&self,
cx: &Context<Self>,
@@ -1185,7 +1143,6 @@ impl NewPullRequestView {
}
}
/// The title and description inputs.
fn render_inputs(&self, _cx: &mut Context<Self>) -> AnyElement {
v_flex()
.px_4()
@@ -1196,7 +1153,6 @@ impl NewPullRequestView {
.into_any_element()
}
/// The Files/Commits tab bar, mirroring the repository panel's.
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());
@@ -1268,7 +1224,6 @@ impl NewPullRequestView {
.into_any_element()
}
/// The active tab's body.
fn render_content(&self, cx: &mut Context<Self>) -> AnyElement {
if self.loading {
return v_flex()
@@ -1354,7 +1309,6 @@ impl NewPullRequestView {
}
}
/// Open the new pull request panel in the center dock.
pub(crate) fn open_new_pull_panel(
dock_area: WeakEntity<DockArea>,
store: Entity<RepoStore>,
-12
View File
@@ -7,7 +7,6 @@ use signed_core::Announcement;
use signed_state::ProfileStore;
use signed_ui::{UserAvatar, middle_truncate};
/// Open the About dialog showing every field of the announcement event.
pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, cx: &mut App) {
window.open_dialog(cx, move |dialog, _window, cx| {
let announcement = announcement.clone();
@@ -21,7 +20,6 @@ pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window,
});
}
/// The announcement's fields as labeled rows.
fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
let mut rows: Vec<AnyElement> = Vec::new();
@@ -116,7 +114,6 @@ fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
v_flex().gap_3().w_full().children(rows).into_any_element()
}
/// One info row with a small muted label above the value.
fn row(label: &'static str, value: AnyElement, cx: &App) -> AnyElement {
v_flex()
.gap_1()
@@ -132,7 +129,6 @@ fn row(label: &'static str, value: AnyElement, cx: &App) -> AnyElement {
.into_any_element()
}
/// Plain text value, wrapping within the dialog.
fn text<T>(value: T) -> AnyElement
where
T: Into<SharedString>,
@@ -147,7 +143,6 @@ where
.into_any_element()
}
/// A mono-spaced value with a copy button, for hex identifiers.
fn copy_value(id: &'static str, value: String, cx: &App) -> AnyElement {
h_flex()
.gap_2()
@@ -164,10 +159,6 @@ fn copy_value(id: &'static str, value: String, cx: &App) -> AnyElement {
.into_any_element()
}
/// One row per maintainer with avatar and display name.
/// The display name falls back to a shortened npub.
///
/// A copy button copies the full pubkey.
fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement {
let profile_store = ProfileStore::global(cx);
v_flex()
@@ -197,9 +188,6 @@ fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement {
.into_any_element()
}
/// One row per item of a multi-value tag.
///
/// The value is truncated to a single line, with a copy button for the full value.
fn list(id: &'static str, items: impl IntoIterator<Item = String>, cx: &App) -> AnyElement {
v_flex()
.gap_2()
+1 -14
View File
@@ -18,7 +18,6 @@ use crate::views::pull_requests::detail::PullRequestDetailView;
use crate::views::repo::init_dialog;
impl RepoDetailView {
/// Re-push the repository's refs to its announced grasp servers.
pub(super) fn push_repository(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.error = None;
cx.notify();
@@ -28,7 +27,6 @@ impl RepoDetailView {
.detach();
}
/// Push the unpushed commits of the local checkout at `path`.
pub(super) fn push_unpushed_checkout(
&mut self,
path: PathBuf,
@@ -71,7 +69,6 @@ impl RepoDetailView {
.detach();
}
/// Open the issues list panel in the dock area.
pub(super) fn open_issue_detail(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.store.read(cx).addr().is_none() {
return;
@@ -89,7 +86,6 @@ impl RepoDetailView {
});
}
/// Open the pull requests list panel in the dock area.
pub(super) fn open_pull_request_detail(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.store.read(cx).addr().is_none() {
return;
@@ -124,7 +120,7 @@ impl RepoDetailView {
open_repo_panel(&self.dock_area, &addr, None, window, &mut *cx);
}
/// Open the dialog guiding the user through publishing the local repository to NIP-34.
/// Open the dialog that publishes the local repository to NIP-34.
pub(super) 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;
@@ -176,17 +172,8 @@ pub(crate) enum RepoItem {
Patch,
}
/// Open the detail panel of `item` in `addr`'s repository, in the dock's center.
///
/// The repository store is built here, not taken from a `RepoDetailView`, so the
/// item panel is the only panel docked.
///
/// `hint` is an announcement already in hand for `addr`, e.g. the inbox row the
/// item was clicked from. The store resolves the repository from the local
/// database on its own, so an entry point with only the address works too.
///
/// A patch opens nothing: patches are only consumed inside a pull request's
/// detail panel, and have no panel of their own.
pub(crate) fn open_repo_item(
dock_area: &WeakEntity<DockArea>,
addr: &RepoAddr,
@@ -67,9 +67,6 @@ impl RepoDetailView {
})
}
/// The ready-to-push banner of an owned repository.
///
/// A local checkout has unpushed commits, with a Push action and a dismiss control.
pub(super) fn render_push_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
let status = self.push_suggestion(cx)?;
let key = (status.path.clone(), status.branch.clone());
@@ -155,7 +152,6 @@ impl RepoDetailView {
)
}
/// Warning after a push that only some grasp servers accepted.
pub(super) fn render_push_warning_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
let store = self.store.read(cx);
let warning = store.last_push_warning.clone()?;
@@ -220,7 +216,6 @@ impl RepoDetailView {
)
}
/// The ready-to-contribute banner of the repository panel.
pub(super) fn render_ready_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
let status = self.ready_suggestion(cx)?;
let key = (status.path.clone(), status.branch.clone());
+1 -27
View File
@@ -15,38 +15,28 @@ use signed_ui::{placeholder, tree_row};
use super::RepoDetailView;
use crate::views::repo::helpers::{code_language, is_markdown_path};
/// Width of the file explorer column.
const TREE_WIDTH: f32 = 240.;
/// Files larger than this are not previewed.
pub(super) const MAX_PREVIEW_BYTES: usize = 1024 * 1024;
/// Preview cache caps, a file count and a text byte count.
///
/// The oldest previews are evicted beyond the caps.
pub(super) const MAX_PREVIEWED_FILES: usize = 32;
pub(super) const MAX_PREVIEW_CACHE_BYTES: usize = 8 * 1024 * 1024;
/// Preview state of a browsed file.
pub(super) enum FileContent {
/// Decodable text content.
Text(String),
/// Not valid UTF-8.
Binary,
/// Bigger than [`MAX_PREVIEW_BYTES`].
TooLarge,
/// Reading failed.
Failed(String),
}
/// A markdown document loaded into a persistent [`TextViewState`].
pub(super) struct MarkdownView {
/// Source path, `None` means the repository README.
/// `None` means the repository README.
pub(super) path: Option<SharedString>,
pub(super) state: Entity<TextViewState>,
/// Hash of the source, so the same document is not re-parsed on a refresh.
source_hash: u64,
}
/// A code file loaded into a persistent [`InputState`].
pub(super) struct CodeView {
/// Source path, relative to the worktree root.
pub(super) path: SharedString,
@@ -55,8 +45,6 @@ pub(super) struct CodeView {
source_hash: u64,
}
/// Hash of a preview's source text.
///
/// Two loads of the same document produce the same hash, so the persistent
/// markdown/editor state can be kept instead of rebuilt, which would re-parse
/// and flash the pane.
@@ -68,7 +56,6 @@ fn source_hash(text: &str) -> u64 {
hasher.finish()
}
/// Spinner shown while a document is being loaded/parsed.
fn preview_spinner() -> AnyElement {
v_flex()
.size_full()
@@ -79,7 +66,6 @@ fn preview_spinner() -> AnyElement {
}
impl RepoDetailView {
/// One row of the file tree with icon and name, indented by depth.
fn render_tree_item(
ix: usize,
entry: &TreeEntry,
@@ -96,7 +82,6 @@ impl RepoDetailView {
})
}
/// Left column showing the file tree.
pub(super) fn render_tree_column(
tree_state: Entity<TreeState>,
view: WeakEntity<Self>,
@@ -117,7 +102,6 @@ impl RepoDetailView {
)))
}
/// Right column, README, selected file preview or status text.
pub(super) fn render_content_column(
&self,
pane_title: SharedString,
@@ -230,7 +214,6 @@ impl RepoDetailView {
.child(div().id("repo-content").flex_1().min_h_0().child(body))
}
/// Load `text` into the persistent markdown TextView state.
pub(super) fn set_markdown(
&mut self,
path: Option<SharedString>,
@@ -254,9 +237,6 @@ impl RepoDetailView {
});
}
/// The persistent markdown TextView for `path`, where `None` is the README.
///
/// Shows a spinner while the document is being loaded or parsed.
fn markdown_element(&self, path: Option<&str>, _cx: &mut Context<Self>) -> AnyElement {
let Some(md) = &self.md else {
return preview_spinner();
@@ -279,9 +259,6 @@ impl RepoDetailView {
.into_any_element()
}
/// Load `text` into the persistent code editor state for `path`.
///
/// Code editor mode makes the Input render it read-only and highlighted.
pub(super) fn set_code(
&mut self,
path: SharedString,
@@ -312,7 +289,6 @@ impl RepoDetailView {
});
}
/// The persistent code editor for `path`, or a spinner while the file loads or parses.
fn code_element(&self, path: &str, _cx: &mut Context<Self>) -> AnyElement {
let Some(code) = &self.code else {
return preview_spinner();
@@ -332,7 +308,6 @@ impl RepoDetailView {
}
impl RepoDetailView {
/// Preview the file at `path`, relative to the worktree root.
fn open_file(&mut self, path: &str, window: &mut Window, cx: &mut Context<Self>) {
self.selected_file = Some(path.into());
@@ -455,7 +430,6 @@ impl RepoDetailView {
task.detach();
}
/// Drop the cached preview, editor and commit state of `path`.
pub(super) fn drop_preview_of(&mut self, path: &str) {
if let Some(FileContent::Text(text)) = self.files.remove(path) {
self.preview_bytes -= text.len();
-11
View File
@@ -24,8 +24,6 @@ use crate::views::repo::helpers::{ShareTargets, ref_selector_trigger};
use crate::views::send_patch::open_send_patch_panel;
impl RepoDetailView {
/// The NIP-34 header, actions and issues/PR counts.
/// Or the local header with an Init button for an unpublished repository.
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);
@@ -411,8 +409,6 @@ impl RepoDetailView {
.into_any_element()
}
/// Header for a local, not yet published, repository.
/// The directory name and path with an Init button instead of the NIP-34 actions.
fn render_local_header(&self, cx: &mut Context<Self>) -> AnyElement {
let name = self.display_name(cx);
let path = self
@@ -476,8 +472,6 @@ impl RepoDetailView {
.into_any_element()
}
/// The tab row shared by both header variants.
/// Files and Commits tabs, the HEAD commit button and the branch/tag selectors.
fn render_header_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
let commits_count = self.all_commits.as_ref().map(|list| list.total);
let worktree_empty = self.switching_ref || self.worktree.is_none();
@@ -658,7 +652,6 @@ impl RepoDetailView {
}
}
/// The `nostr://...` clone URL of an announcement, NIP-34.
fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedString {
let owner = announcement.owner;
let user = nip05
@@ -676,10 +669,6 @@ fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedSt
SharedString::from(url)
}
/// The forked-from row of the detail header.
///
/// Clickable link to the upstream repository when the `u` tag references a NIP-34 repo.
/// Plain text when it only carries a git URL.
fn fork_row(announcement: &Announcement, cx: &mut Context<RepoDetailView>) -> Option<AnyElement> {
let upstream = announcement.upstream.as_ref()?;
+6 -74
View File
@@ -23,7 +23,6 @@ use utils::{relative_time, relative_time_secs};
pub(crate) struct TreeItemSeed {
/// Path of the node, relative to the worktree root.
id: String,
/// File or directory name.
label: String,
children: Vec<TreeItemSeed>,
}
@@ -200,9 +199,6 @@ impl ShareTargets {
}
}
/// The share dropdown menu, one row per target.
///
/// Each shows a compact label, the copy button and row click copy the full value.
pub(crate) fn menu(&self, menu: PopupMenu) -> PopupMenu {
menu.min_w(px(340.))
.item(menu_copy_row(
@@ -232,7 +228,6 @@ impl ShareTargets {
}
}
/// Shorten an naddr link to `<url>/naddr1...[last tail chars]`.
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();
@@ -243,14 +238,9 @@ fn truncate_naddr_link(url: &str, tail: usize) -> String {
format!("{}...{}", &url[..end], &url[url.len() - tail..])
}
/// Width of one line-number gutter in a diff row.
pub(crate) const GUTTER_WIDTH: f32 = 44.;
/// Height of one row in a virtual diff list.
pub(crate) const DIFF_ROW_HEIGHT: f32 = 20.;
/// One row of a virtual diff list, a hunk header or a line of a hunk.
///
/// Shared by the commit diff and pull request diff viewers.
#[derive(Clone, Copy)]
pub(crate) enum DiffRow {
Hunk {
@@ -259,11 +249,12 @@ pub(crate) enum DiffRow {
new_start: u32,
new_lines: u32,
},
/// Line `line` of hunk `hunk` of the selected file's diff.
Line { hunk: usize, line: usize },
Line {
hunk: usize,
line: usize,
},
}
/// The rows of `file`'s diff, one header row per hunk then its lines.
pub(crate) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
let mut rows = Vec::new();
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
@@ -281,7 +272,6 @@ pub(crate) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
rows
}
/// One row of the virtual diff list, a hunk header or a single line.
pub(crate) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> AnyElement {
match row {
DiffRow::Hunk {
@@ -308,9 +298,6 @@ pub(crate) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> Any
}
}
/// One diff line, old and new line numbers in the gutters.
///
/// The content is tinted by kind, addition, deletion or context.
pub(crate) fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
let bg = match line.kind {
DiffLineKind::Addition => Some(cx.theme().success.opacity(0.2)),
@@ -358,7 +345,6 @@ pub(crate) fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
.into_any_element()
}
/// Find a tree item by id, searching into nested children.
pub(crate) fn find_item<'a>(items: &'a [TreeItem], id: Option<&str>) -> Option<&'a TreeItem> {
let id = id?;
items.iter().find_map(|item| {
@@ -370,21 +356,16 @@ pub(crate) fn find_item<'a>(items: &'a [TreeItem], id: Option<&str>) -> Option<&
})
}
/// The root issue events of a repo store, for the shared detail sections.
pub(crate) fn issue_roots(store: &RepoStore) -> &[Event] {
&store.issues
}
/// The root pull request events of a repo store, for the shared detail sections.
pub(crate) fn pr_roots(store: &RepoStore) -> &[Event] {
&store.pull_requests
}
/// The trigger body of the branch/tag selectors.
///
/// The kind icon, the selection or placeholder, and the caret.
/// `Combobox` replaces its default trigger entirely,
/// the only way to show an icon inside it.
/// The kind icon, the selection or placeholder, and the caret. `Combobox`
/// replaces its default trigger entirely, the only way to show an icon inside it.
pub(crate) fn ref_selector_trigger(
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
icon: CustomIconName,
@@ -418,7 +399,6 @@ pub(crate) fn ref_selector_trigger(
.into_any_element()
}
/// Section heading of a detail sidebar, shared by the issue and PR panels.
pub(crate) fn sidebar_title(text: &str, cx: &App) -> AnyElement {
div()
.text_xs()
@@ -428,7 +408,6 @@ pub(crate) fn sidebar_title(text: &str, cx: &App) -> AnyElement {
.into_any_element()
}
/// Right sidebar with participants and labels of a root event, issue or PR.
pub(crate) fn sidebar_section(
store: &Entity<RepoStore>,
id: EventId,
@@ -511,7 +490,6 @@ pub(crate) fn sidebar_section(
.into_any_element()
}
/// The comments on a root event, issue or PR, one card per comment.
pub(crate) fn comments_section(store: &Entity<RepoStore>, root: EventId, cx: &App) -> AnyElement {
let store = store.read(cx);
let comments: Vec<&Event> = store.comments_of(&root).collect();
@@ -559,8 +537,6 @@ pub(crate) fn comments_section(store: &Entity<RepoStore>, root: EventId, cx: &Ap
.into_any_element()
}
/// The comment form posting to an issue or PR root event.
///
/// `roots` selects the root's list within the store, issues or pull requests.
pub(crate) fn comment_form(
store: &Entity<RepoStore>,
@@ -621,11 +597,8 @@ pub(crate) fn comment_form(
.into_any_element()
}
/// Height of one commit row in a commit virtual list.
pub(crate) const COMMIT_ROW_HEIGHT: f32 = 56.;
/// One commit row of a virtual list, shared by the commits tab and the
/// new-pull-request commit picker.
pub(crate) fn commit_row(
ix: usize,
commit: &FileCommit,
@@ -745,45 +718,4 @@ mod tests {
assert_eq!(items[0].children.len(), 2);
assert_eq!(items[1].label, "b");
}
#[test]
fn tree_seeds_convert_to_tree_items() {
let entries = vec![
PathBuf::from("src"),
PathBuf::from("src/main.rs"),
PathBuf::from("README.md"),
];
let items: Vec<TreeItem> = tree_items(build_tree_items(&entries), false);
assert_eq!(items.len(), 2);
assert_eq!(items[0].label, "src");
assert_eq!(items[0].children.len(), 1);
assert_eq!(items[0].children[0].label, "main.rs");
}
#[test]
fn code_language_maps_extensions_and_names() {
assert_eq!(code_language("src/main.rs"), Some("rust"));
assert_eq!(code_language("Cargo.toml"), Some("toml"));
assert_eq!(code_language("app.js"), Some("javascript"));
assert_eq!(code_language("index.tsx"), Some("tsx"));
assert_eq!(code_language("Makefile"), Some("make"));
assert_eq!(code_language("CMakeLists.txt"), Some("cmake"));
assert_eq!(code_language("data.csv"), None);
assert_eq!(code_language("LICENSE"), None);
assert_eq!(code_language("README.md"), None);
}
#[test]
fn naddr_link_keeps_url_and_tail() {
assert_eq!(
truncate_naddr_link("https://gitworkshop.dev/naddr1qqqxyzabc1234", 4),
"https://gitworkshop.dev/naddr1...1234"
);
// Without the naddr1 prefix, unchanged.
assert_eq!(
truncate_naddr_link("https://example.com/x", 4),
"https://example.com/x"
);
}
}
@@ -105,8 +105,6 @@ impl RepoDetailView {
}
impl RepoDetailView {
/// Queue `path` for the per-file commit query.
/// Requests are batched into one history walk, see [`Self::load_commits`].
pub(super) fn load_commit(&mut self, path: &str, cx: &mut Context<Self>) {
if self.commits.contains_key(path) || self.pending_commits.iter().any(|p| p == path) {
return;
@@ -117,10 +115,6 @@ impl RepoDetailView {
}
}
/// Walk history once for every queued path on a background task.
/// Cache the latest commit touching each path in [`Self::commits`].
/// That feeds the file header in the content column.
/// Batching shares one walk across paths queued while the previous walk ran.
fn load_commits(&mut self, cx: &mut Context<Self>) {
if self.pending_commits.is_empty() || self.loading_commits {
return;
@@ -167,9 +161,6 @@ impl RepoDetailView {
task.detach();
}
/// Walk all commits reachable from HEAD on a background task.
/// For the Commits tab and its total-count badge.
/// [`CommitList`] caps the list, only the newest commits are materialized.
pub(super) fn load_all_commits(&mut self, cx: &mut Context<Self>) {
if self.loading_all_commits || self.all_commits.is_some() {
return;
@@ -209,7 +200,6 @@ impl RepoDetailView {
task.detach();
}
/// Open a new panel showing the diff of `commit_id`.
pub(super) fn open_commit_diff(
&mut self,
commit_id: &str,
@@ -19,10 +19,8 @@ use crate::views::sidebar::grasp_servers::{
GraspServersState, grasp_servers_field, load_user_grasp_servers,
};
/// Shared state for the Init dialog, so async results can be rendered.
pub type InitRepoState = DialogProgress;
/// Open the Init dialog for the local repository at `local_path`.
pub fn open(
local_path: PathBuf,
view: WeakEntity<RepoDetailView>,
@@ -51,7 +49,6 @@ pub fn open(
.placeholder("Short description")
});
// Load the user's grasp servers.
load_user_grasp_servers(grasp_state.clone(), window, cx);
window.open_dialog(cx, move |dialog, _window, _cx| {
@@ -146,9 +143,6 @@ pub fn open(
});
}
/// Run the init flow.
///
/// Closes the dialog and switches the repository into NIP-34 mode on success.
fn init_repository(
local_path: PathBuf,
inputs: (Entity<InputState>, Entity<TextareaState>),
+2 -11
View File
@@ -15,7 +15,6 @@ use crate::views::repo::helpers::{
TreeItemSeed, build_tree_items, sorted_worktree_paths, tree_items,
};
/// Everything loaded from the local clone for the explorer.
struct RepoData {
tree: Vec<TreeItemSeed>,
/// Relative paths of the worktree entries, for [`RepoDetailView::worktree_paths`].
@@ -32,8 +31,8 @@ struct RepoData {
impl RepoDetailView {
/// Load the repository and populate the file explorer.
///
/// A local, not yet published, repository opens straight from disk.
/// An announced repository's clone, if any, loads first without touching the network.
/// An announced repository's clone, if any, loads first without touching
/// the network.
pub(super) fn load_repo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.loading = true;
self.error = None;
@@ -110,7 +109,6 @@ impl RepoDetailView {
let disk = disk.await;
let had_clone = matches!(&disk, Ok(Some(_)));
// No local clone yet, so clone from the network then load.
let data = match disk {
Ok(Some(data)) => Ok(data),
Ok(None) => {
@@ -247,7 +245,6 @@ impl RepoDetailView {
task.detach();
}
/// Apply the loaded repository data.
fn apply_repo_data(&mut self, data: RepoData, window: &mut Window, cx: &mut Context<Self>) {
log::debug!("repo detail: apply_repo_data");
let RepoData {
@@ -273,8 +270,6 @@ impl RepoDetailView {
state.set_items(tree_items(tree, false), cx);
});
// Populate the branch/tag selectors with the local refs.
// Select the branch HEAD points to.
let branches: Vec<SharedString> = branches.into_iter().map(Into::into).collect();
let tags: Vec<SharedString> = tags.into_iter().map(Into::into).collect();
@@ -338,7 +333,6 @@ impl RepoDetailView {
true
}
/// Clone the repository into a user-chosen folder outside the cache.
pub(super) fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let store = self.store.clone();
@@ -404,9 +398,6 @@ impl RepoDetailView {
}
}
/// Read the worktree state of `repo`, no network.
///
/// Entries, README, refs and HEAD commit.
fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
let entries = signed_git::worktree_entries(repo)?;
let tree = build_tree_items(&entries);
+17 -62
View File
@@ -33,121 +33,84 @@ pub(crate) use actions::{RepoItem, open_repo_item, open_repo_panel};
use self::files::{CodeView, FileContent, MarkdownView};
/// What kind of ref the header selectors switch to.
#[derive(Clone, Copy, PartialEq, Eq)]
enum RefKind {
/// A local branch `refs/heads/*`, HEAD stays attached.
/// HEAD stays attached.
Branch,
/// A tag `refs/tags/*`, HEAD becomes detached.
/// HEAD becomes detached.
Tag,
}
/// Header actions dispatched by the dropdown menus of the header buttons.
/// `pub(crate)` because the pull-request list panel shares this action set.
/// It offers the New-PR and Send-patch actions in its own dropdown.
/// 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)]
#[action(namespace = repo, no_json)]
pub(crate) enum RepoAction {
/// Open the new issue dialog.
NewIssue,
/// Open the new pull request dialog.
NewPR,
/// Open the send patch panel.
SendPatch,
/// Open the about dialog.
About,
/// Re-push the repository to its grasp servers.
Push,
/// Delete the repository from nostr, owner only.
Delete,
}
/// Detail view of a repository, header, stats and metadata.
///
/// A file explorer with README preview, cloned from the announcement's `clone` URLs.
pub struct RepoDetailView {
focus_handle: FocusHandle,
/// Dock area the detail view lives in.
///
/// New panels, commit diffs, are added there.
dock_area: WeakEntity<DockArea>,
/// Per-repository store, holding the local path and the announcement,
/// issues, PRs and statuses. The single identity of both modes.
store: Entity<RepoStore>,
/// The initial explorer load has been started.
///
/// A repository opened by address alone starts without an announcement; the
/// store observer starts the load once the first one lands.
repo_started: bool,
/// File explorer state, the worktree of the local clone.
tree_state: Entity<TreeState>,
/// Root of the local clone, for reading files on demand.
worktree: Option<PathBuf>,
/// Sorted relative paths of the tree currently shown.
///
/// A background refresh that did not change the tree skips rebuilding it,
/// see [`Self::catch_up_worktree`], so a fetch that learned nothing new
/// does not flash the explorer.
worktree_paths: Vec<String>,
/// Markdown document currently in the preview pane, README or a file.
md: Option<MarkdownView>,
/// Code file currently in the preview pane.
code: Option<CodeView>,
readme_name: Option<SharedString>,
/// Currently previewed file, a relative path, and its contents.
selected_file: Option<SharedString>,
files: HashMap<String, FileContent>,
/// Paths of cached previews, oldest first.
/// Feeds the eviction caps in [`Self::evict_previews`].
/// Paths of cached previews, oldest first. Feeds the eviction caps in
/// [`Self::evict_previews`].
file_order: VecDeque<String>,
/// Total text bytes held by [`Self::files`].
preview_bytes: usize,
/// Reads in flight, to avoid duplicate loads.
loading_files: HashSet<String>,
/// Latest commit touching a previewed file or the README, keyed by path.
commits: HashMap<String, FileCommit>,
/// Paths queued for the next batched commit query, see [`Self::load_commits`].
pending_commits: Vec<String>,
/// A batched commit query is in flight.
loading_commits: bool,
/// Active header tab, 0 = Files tree, 1 = Commits.
/// 0 = Files tree, 1 = Commits.
active_tab: usize,
/// Commits reachable from HEAD, newest first.
/// `None` until the walk finishes or fails.
/// [`CommitList`] caps the list, `total` feeds the tab badge.
/// Commits reachable from HEAD, newest first. `None` until the walk
/// finishes or fails. `total` feeds the tab badge.
all_commits: Option<CommitList>,
/// Commit walk in flight.
loading_all_commits: bool,
/// Virtual list state of the Commits tab.
scroll_handle: VirtualListScrollHandle,
item_sizes: Rc<Vec<Size<Pixels>>>,
/// A clone/fetch is in flight.
loading: bool,
error: Option<SharedString>,
/// Commit HEAD currently points to, shown in the header button.
head_commit: Option<FileCommit>,
/// Branch selector in the header, local branches, searchable.
branch_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
/// Tag selector in the header, tags, searchable.
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>,
/// A branch/tag switch is in flight, checkout plus explorer reload.
switching_ref: bool,
/// Bumped on every branch/tag switch.
/// In-flight loads with an older generation are discarded when they complete.
ref_generation: u64,
/// Subscriptions keeping the selectors' confirm events alive.
_subscriptions: Vec<Subscription>,
/// `(path, branch)` ready-suggestions dismissed by the user, per panel.
banner_dismissed: HashSet<(PathBuf, String)>,
/// The announced HEAD the ready statuses were last requested with.
/// Whether they were requested at all.
/// Re-requested only when the HEAD, the base default, changes.
/// e.g. when the store's first refresh lands.
/// 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.
@@ -155,14 +118,10 @@ pub struct RepoDetailView {
/// The store notifies on any recompute pass; the observer re-renders this
/// panel only when these slices changed.
ready_statuses: Vec<CheckoutStatus>,
/// The global checkouts store's ready-to-push statuses of this repository,
/// last seen when they drove a render.
push_statuses: Vec<CheckoutStatus>,
}
impl RepoDetailView {
/// Open a repository by address.
///
/// `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.
@@ -177,7 +136,6 @@ impl RepoDetailView {
Self::new_common(dock_area, store, window, cx)
}
/// Open a local repository discovered by the scan.
pub fn new_local(
dock_area: WeakEntity<DockArea>,
local_path: PathBuf,
@@ -188,9 +146,6 @@ impl RepoDetailView {
Self::new_common(dock_area, store, window, cx)
}
/// Shared construction.
///
/// File explorer state, ref selectors and the deferred repository load.
fn new_common(
dock_area: WeakEntity<DockArea>,
store: Entity<RepoStore>,
@@ -306,8 +261,8 @@ impl RepoDetailView {
self.store.read(cx).announcement.as_ref()
}
/// Display name, the announcement's name or ID for announced repositories.
/// The directory name for local ones.
/// The announcement's name or ID for announced repositories, the directory
/// name for local ones.
fn display_name(&self, cx: &App) -> SharedString {
let store = self.store.read(cx);
-7
View File
@@ -10,8 +10,6 @@ use super::{RefKind, RepoDetailView};
use crate::views::repo::helpers::{build_tree_items, sorted_worktree_paths, tree_items};
impl RepoDetailView {
/// Check out `name`, a branch or tag picked in the header.
/// Refresh the explorer once the switch completes.
pub(super) fn switch_ref(
&mut self,
kind: RefKind,
@@ -81,7 +79,6 @@ impl RepoDetailView {
task.detach();
}
/// Restore a selector to `previous`, or clear it after a failed switch.
fn restore_selection(
&self,
select: &Entity<ComboboxState<SearchableVec<SharedString>>>,
@@ -95,10 +92,6 @@ impl RepoDetailView {
});
}
/// Refresh the file explorer, preview pane and commit list after a successful switch.
/// The selectors were already updated by [`Self::switch_ref`].
/// [`Self::switching_ref`] stays set until this reload finishes.
/// A second switch cannot interleave.
fn reload_worktree(&mut self, cx: &mut Context<Self>) {
let Some(worktree) = self.worktree.clone() else {
return;
-3
View File
@@ -26,8 +26,6 @@ impl RepoDetailView {
cx.notify();
}
/// Observe the repository's store and re-render on its refreshes.
/// Start the explorer once the store has an announcement.
pub(super) fn attach_store(
&mut self,
store: &Entity<RepoStore>,
@@ -82,7 +80,6 @@ impl RepoDetailView {
});
}
/// The ready-to-contribute and ready-to-push statuses of this repository.
pub(super) fn refresh_statuses(&mut self, cx: &mut Context<Self>) -> bool {
let Some(addr) = self.store.read(cx).addr().cloned() else {
return false;
-14
View File
@@ -24,18 +24,13 @@ use super::open_repo_panel;
const COLUMNS: usize = 2;
const CARD_HEIGHT: f32 = 40. + 64. + 48. + 2. + 6.;
/// How many of the newest repositories the `Recent` sort shows.
const RECENT_COUNT: usize = 10;
/// Sort of the explore list, chosen via the header's filter buttons.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum RepoFilter {
/// Every repository in the store's default order, newest first.
All,
#[default]
/// Repositories ranked by total issues + pull requests + commits.
Popular,
/// The [`RECENT_COUNT`] newest repositories.
Recent,
}
@@ -100,23 +95,18 @@ impl RepoFilter {
}
}
/// Browse all announced repositories.
pub struct RepoListView {
store: Entity<RepoListStore>,
dock_area: WeakEntity<DockArea>,
focus_handle: FocusHandle,
scroll_handle: VirtualListScrollHandle,
/// Sort selected in the header filter buttons.
filter: RepoFilter,
/// Per-row heights of the virtual list.
item_sizes: Rc<Vec<Size<Pixels>>>,
/// Number of rows [`Self::item_sizes`] was built for, the filtered repo count.
repo_len: usize,
/// Indices matching [`Self::filter`] into the store's `announcements`.
visible: Vec<usize>,
/// Search box filtering repositories by name.
search: Entity<InputState>,
/// Rebuilds the visible slice as the search text changes.
_search_subscription: Subscription,
_subscription: Subscription,
}
@@ -129,7 +119,6 @@ impl RepoListView {
) -> Self {
let store = RepoListStore::global(cx);
// Live search over repository names
let search = cx.new(|cx| InputState::new(window, cx).placeholder("Search..."));
let search_subscription = cx.subscribe(&search, |this, _search, event, cx| {
if matches!(event, InputEvent::Change) {
@@ -162,9 +151,6 @@ impl RepoListView {
}
}
/// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the store.
///
/// Uses the store contents, [`Self::filter`] and the search query.
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
let filter = self.filter;
let query = self.search.read(cx).value();
-8
View File
@@ -13,19 +13,12 @@ use signed_state::RepoStore;
pub struct SendPatchView {
focus_handle: FocusHandle,
/// Dock area the panel lives in.
dock_area: WeakEntity<DockArea>,
/// Store of the target repository.
store: Entity<RepoStore>,
/// Display name of the repository, for the panel title.
repo_name: SharedString,
/// Title input, required.
subject: Entity<InputState>,
/// Description input, optional.
description: Entity<TextareaState>,
/// The pasted `git format-patch` output, required.
patch: Entity<TextareaState>,
/// A submit is in flight.
submitting: bool,
/// Error of the last submit attempt, it keeps the panel open.
error: Option<SharedString>,
@@ -61,7 +54,6 @@ impl SendPatchView {
}
}
/// Publish the pull request from the pasted patch.
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.submitting {
return;
@@ -17,10 +17,9 @@ use super::super::open_repo_panel;
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
use crate::views::dialog_state::{DialogProgress, error_row};
/// Shared state for the Create Repository dialog, so async results can be rendered.
/// Progress of the create-repository flow, so async results can be rendered.
pub type CreateRepoState = DialogProgress;
/// Open the Create Repository dialog.
pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
let settings = SettingsStore::global(cx);
let default_folder = settings
@@ -151,7 +150,6 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
});
}
/// Pick the repository's storage folder with the platform's native folder picker.
fn choose_folder(folder_input: &Entity<InputState>, window: &mut Window, cx: &mut App) {
let handle = window.window_handle();
let folder_input = folder_input.clone();
@@ -186,8 +184,6 @@ fn choose_folder(folder_input: &Entity<InputState>, window: &mut Window, cx: &mu
.detach();
}
/// Run the create-repository flow.
///
/// Opens the new working copy and the repository panel on success.
#[allow(clippy::too_many_arguments)]
fn create_repository(
@@ -250,7 +246,6 @@ fn create_repository(
.detach();
}
/// Open the newly created repository in the dock's center.
fn open_repo(
dock_area: WeakEntity<DockArea>,
announcement: Announcement,
@@ -11,17 +11,16 @@ use signed_state::Backend;
/// State of the grasp-server section of a publish dialog, so async results can be rendered.
#[derive(Default)]
pub struct GraspServersState {
/// The user's grasp list of kind `10317` is being loaded.
/// Set while the user's kind `10317` grasp list loads.
pub loading_servers: bool,
pub grasp_servers: Vec<RelayUrl>,
/// Whether the grasp server section is shown. Defaults to shown.
pub servers_enabled: bool,
/// Error of the last grasp-server edit, an invalid relay URL for example.
/// Error from the last grasp-server edit, such as an invalid relay URL.
pub error: Option<SharedString>,
}
impl GraspServersState {
/// Defaults used until the user's grasp list loads, which replaces them when non-empty.
/// Defaults used until the user's grasp list loads and replaces them.
///
/// Persisted settings supply the defaults, an empty list falls back to the built-ins.
pub fn new_default(settings: &GraspServersSettings) -> Self {
@@ -137,7 +136,6 @@ pub fn grasp_servers_field(
}))
}
/// One grasp server row, the host in a tag plus a remove button.
fn render_server_row(
ix: usize,
relay: &RelayUrl,
@@ -176,7 +174,7 @@ fn render_server_row(
)
}
/// The bare host of a grasp server, defaults are entered without a scheme.
/// Shows only the host, since grasp servers are entered without a scheme.
fn display_server(relay: &RelayUrl) -> SharedString {
relay
.domain()
@@ -184,7 +182,7 @@ fn display_server(relay: &RelayUrl) -> SharedString {
.unwrap_or_else(|| SharedString::from(relay.to_string()))
}
/// Parse the relay input, accepting a bare host, and append it to the list.
/// Accepts a bare host as well as a full URL.
fn add_relay(
state: &Entity<GraspServersState>,
input: &Entity<InputState>,
@@ -220,9 +218,9 @@ fn add_relay(
}
}
/// Load the user's grasp list of kind `10317` from the local database.
/// Loads the user's kind `10317` grasp list from the local database.
///
/// It replaces the defaults when it lists any servers.
/// Replaces the defaults when the list is non-empty.
pub fn load_user_grasp_servers(
state: Entity<GraspServersState>,
window: &mut Window,
@@ -1,7 +1,6 @@
use gpui::{App, Window, px};
use gpui_component::WindowExt;
/// Open the Import Identity dialog.
pub fn open(window: &mut Window, cx: &mut App) {
window.open_dialog(cx, move |dialog, _window, _cx| {
dialog.title("Import identity").width(px(400.))
+8 -26
View File
@@ -39,15 +39,13 @@ pub struct SidebarPanel {
dock_area: WeakEntity<DockArea>,
inbox: Option<WeakEntity<InboxView>>,
explore: Option<WeakEntity<RepoListView>>,
/// Artwork for the sign-in screen.
banner: SharedString,
/// The signed-in user's announced repositories, newest first.
announcements: Arc<Vec<Announcement>>,
/// Local repositories found by the scan that are not announced yet.
local_repos: Arc<Vec<PathBuf>>,
/// A local scan is currently running.
scanning: bool,
/// Unpushed local commits per announced repository, the row badge counts.
/// Unpushed commit counts per announced repository, shown as row badges.
unpushed: HashMap<RepoAddr, usize>,
_subscriptions: Vec<Subscription>,
}
@@ -80,21 +78,19 @@ impl SidebarPanel {
}
}));
// The merged list re-derives when announcements or the local scan change.
subscriptions.push(cx.observe(&repos, |this, _repos, cx| {
if this.refresh(cx) {
cx.notify();
}
}));
// The local scan re-derives when announcements or the local scan change.
subscriptions.push(cx.observe(&local, |this, _local, cx| {
if this.refresh(cx) {
cx.notify();
}
}));
// Push statuses are recomputed in the background; only the badge counts change.
// Push statuses are recomputed in the background, so only the badge counts change.
subscriptions.push(cx.observe(&checkouts, |this, _checkouts, cx| {
if this.refresh_unpushed(cx) {
cx.notify();
@@ -125,8 +121,7 @@ impl SidebarPanel {
.map(|user| repo_list.read(cx).announcements_of(user))
.unwrap_or_default();
// A scanned repository is dropped from the local list
// once the user announces it, so it is not listed twice.
// Drop a scanned repository once the user announces it, so it is not listed twice.
let local = LocalReposStore::global(cx);
let scanning = local.read(cx).scanning;
@@ -162,7 +157,6 @@ impl SidebarPanel {
announcements_changed || local_changed || scanning_changed
}
/// Recompute the badge counts from the global checkouts store's ready-to-push statuses
fn refresh_unpushed(&mut self, cx: &mut Context<Self>) -> bool {
let checkouts = CheckoutsStore::global(cx);
let mut unpushed = HashMap::with_capacity(self.announcements.len());
@@ -183,7 +177,6 @@ impl SidebarPanel {
true
}
/// Keep the `ready to push` statuses of the announced repositories current.
fn request_push_watches(&self, cx: &mut Context<Self>) {
let checkouts = CheckoutsStore::global(cx);
checkouts.update(cx, |checkouts, cx| {
@@ -193,7 +186,6 @@ impl SidebarPanel {
});
}
/// Open the inbox home panel in the dock area's center.
pub fn open_inbox(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.inbox.as_ref().and_then(WeakEntity::upgrade).is_some() {
return;
@@ -209,7 +201,6 @@ impl SidebarPanel {
.ok();
}
/// Open the Explore repository list panel in the dock area's center.
pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self
.explore
@@ -230,7 +221,6 @@ impl SidebarPanel {
.ok();
}
/// Show the Onboarding dialog.
fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Enter desired name"));
let pass_input = cx.new(|cx| {
@@ -248,12 +238,10 @@ impl SidebarPanel {
onboarding_dialog::open(name_input, pass_input, repass_input, state, window, cx);
}
/// Show the Create Repository dialog.
fn open_create_repo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
create_repo_dialog::open(self.dock_area.clone(), window, cx);
}
/// Open a repository's detail view in the dock's center.
fn open_repo(
&mut self,
announcement: &Announcement,
@@ -269,8 +257,6 @@ impl SidebarPanel {
);
}
/// Open a local repository's detail view in the dock's center.
///
/// The detail view offers to publish it to NIP-34.
fn open_local_repo(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) {
let detail =
@@ -334,7 +320,7 @@ impl SidebarPanel {
),
)
.map(|this| {
// Merged list, the user's NIP-34 repositories and local repositories discovered.
// The merged list: NIP-34 repositories first, then discovered local repositories.
let total = announcements.len() + local_repos.len();
if total == 0 {
@@ -374,7 +360,7 @@ impl SidebarPanel {
})
}
/// One row of the merged sidebar list, a NIP-34 or a local repository.
/// Renders row `ix` of the merged list: an announced repository or a local one.
fn render_repo_at(
&self,
announcements: &[Announcement],
@@ -403,7 +389,6 @@ impl SidebarPanel {
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
let announcement = announcement.clone();
// Badge with the unpushed commit count of the repository's local checkouts.
let unpushed = self
.unpushed
.get(&announcement.addr())
@@ -433,9 +418,7 @@ impl SidebarPanel {
)
}
/// One local repository row.
///
/// The directory name and a warning suffix, the repo is not yet set up for NIP-34.
/// A local repository that is not yet set up for NIP-34, marked with a warning.
fn render_local_row(&self, path: &Path, cx: &mut Context<Self>) -> impl IntoElement {
let name = path
.file_name()
@@ -455,12 +438,11 @@ impl SidebarPanel {
}))
}
/// Show the Import Identity dialog.
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
import_dialog::open(window, cx);
}
/// Render the user avatar and name in the sidebar, inside the titlebar drag area.
/// The user avatar and name, wired into the titlebar drag area.
fn render_user(
&self,
profile: &Profile,
@@ -490,7 +472,7 @@ impl SidebarPanel {
)
}
/// Sign-in placeholder shown while logged out.
/// Shown while no identity is signed in.
fn render_sign_in(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
v_flex()
.size_full()
@@ -9,10 +9,9 @@ use signed_state::Backend;
use crate::views::dialog_state::{DialogProgress, error_row};
/// Shared state for the Onboarding dialog, so async results can be rendered.
/// Progress of the onboarding flow, so async results can be rendered.
pub type OnboardingState = DialogProgress;
/// Open the Onboarding dialog for creating a new identity.
pub fn open(
name_input: Entity<InputState>,
pass_input: Entity<InputState>,
@@ -10,16 +10,14 @@ use signed_state::Backend;
use crate::views::dialog_state::{DialogProgress, error_row};
/// Shared state for the passphrase dialog, so async results can be rendered.
/// State of the passphrase dialog, so async results can be rendered.
#[derive(Default)]
pub struct PassphraseState {
/// Progress of the unlock flow.
pub progress: DialogProgress,
/// Keeps the Enter-to-submit subscription alive while the dialog is open.
_enter_subscription: Option<Subscription>,
}
/// Open the dialog asking for the passphrase that protects the stored identity.
pub fn open(window: &mut Window, cx: &mut App) {
let pass_input = cx.new(|cx| {
InputState::new(window, cx)
@@ -30,7 +28,7 @@ pub fn open(window: &mut Window, cx: &mut App) {
let handle = window.window_handle();
let state = cx.new(|_| PassphraseState::default());
// Enter in the passphrase field submits, same as the Unlock button.
// Enter in the passphrase field submits, like the Unlock button.
let enter_pass_input = pass_input.clone();
let enter_state = state.clone();
let enter_subscription = cx.subscribe(&pass_input, move |_input, event, cx| {
@@ -95,7 +93,6 @@ pub fn open(window: &mut Window, cx: &mut App) {
});
}
/// Submit the passphrase to the backend.
fn unlock(
pass_input: &Entity<InputState>,
state: &Entity<PassphraseState>,
@@ -22,7 +22,7 @@ use nostr::prelude::RelayUrl;
use settings::{AppearanceMode, Settings, SettingsStore};
use signed_ui::{SelectOption, setting_block, setting_row};
/// The index of `value` in `options`, for seeding a [`SelectState`].
/// Looks up the option index used to seed a [`SelectState`].
fn selected_index(options: &[SelectOption], value: &str) -> Option<IndexPath> {
options
.iter()
@@ -30,7 +30,7 @@ fn selected_index(options: &[SelectOption], value: &str) -> Option<IndexPath> {
.map(|row| IndexPath::default().row(row))
}
/// The light and dark themes registered in the theme registry.
/// Registered themes split into light and dark options, light first.
fn theme_options(cx: &App) -> (Vec<SelectOption>, Vec<SelectOption>) {
let registry = ThemeRegistry::global(cx);
let mut light = Vec::new();
@@ -48,7 +48,7 @@ fn theme_options(cx: &App) -> (Vec<SelectOption>, Vec<SelectOption>) {
(light, dark)
}
/// Stateful controls of the settings dialog, created once when it opens.
/// Created once when the dialog opens, so control state survives re-renders.
struct SettingsControls {
appearance: Entity<SelectState<Vec<SelectOption>>>,
light_theme: Entity<SelectState<Vec<SelectOption>>>,
@@ -58,9 +58,9 @@ struct SettingsControls {
radius: Entity<InputState>,
radius_lg: Entity<InputState>,
grasp_server_input: Entity<InputState>,
/// The effective default create-repository folder, shown in the disabled input.
/// The effective create-repository folder, shown in a disabled input.
default_folder: Entity<InputState>,
/// Keeps the control subscriptions alive for the dialog's lifetime.
/// Keeps the control subscriptions alive while the dialog is open.
_subscriptions: Vec<Subscription>,
}
@@ -264,7 +264,6 @@ impl SettingsControls {
}
}
/// Open the Settings dialog.
pub fn open(window: &mut Window, cx: &mut App) {
let controls = Rc::new(SettingsControls::new(window, cx));
@@ -278,8 +277,6 @@ pub fn open(window: &mut Window, cx: &mut App) {
});
}
/// The settings content, one section per related setting.
/// Sections are divided by horizontal separator lines.
fn settings_view(controls: &SettingsControls, cx: &mut App) -> impl IntoElement {
let store = SettingsStore::global(cx);
let settings = store.read(cx).settings().clone();
@@ -297,7 +294,6 @@ fn settings_view(controls: &SettingsControls, cx: &mut App) -> impl IntoElement
.child(repositories_section(&settings, controls, cx))
}
/// How the app picks its appearance.
fn appearance_section(controls: &SettingsControls, cx: &App) -> impl IntoElement {
v_flex().w_full().gap_3().child(setting_row(
cx,
@@ -307,7 +303,6 @@ fn appearance_section(controls: &SettingsControls, cx: &App) -> impl IntoElement
))
}
/// Theme configuration, the registry theme names plus tweaks the app customizes at startup.
fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) -> impl IntoElement {
v_flex()
.gap_3()
@@ -378,7 +373,7 @@ fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) ->
))
}
/// Default grasp servers offered until the user publishes a kind `10317` grasp list.
/// Default grasp servers, used until the user's kind `10317` grasp list loads.
fn grasp_servers_section(
settings: &Settings,
controls: &SettingsControls,
@@ -394,7 +389,6 @@ fn grasp_servers_section(
))
}
/// The editable list of default grasp servers plus an add-relay input.
/// Styled like the grasp-server section of the publish dialogs.
fn grasp_server_editor(
servers: &[String],
@@ -459,7 +453,7 @@ fn grasp_server_editor(
)
}
/// The bare host of a grasp server, defaults are entered without a scheme.
/// Shows only the host, since grasp servers are entered without a scheme.
/// Matches how the publish dialogs display servers.
fn display_server(server: &str) -> SharedString {
RelayUrl::parse(server)
@@ -469,7 +463,6 @@ fn display_server(server: &str) -> SharedString {
.unwrap_or_else(|| SharedString::from(server.to_owned()))
}
/// Local repository scanning and the create-repository dialog default folder.
fn repositories_section(
settings: &Settings,
controls: &SettingsControls,
@@ -494,7 +487,6 @@ fn repositories_section(
))
}
/// The editable list of scan directories plus an add-directory button.
/// Styled like the grasp-server list.
fn scan_paths_editor(scan_paths: &[PathBuf], cx: &App) -> impl IntoElement {
v_flex()
@@ -547,7 +539,6 @@ fn scan_paths_editor(scan_paths: &[PathBuf], cx: &App) -> impl IntoElement {
)
}
/// The default-folder selector, a disabled input plus a picker button.
/// Matches the create-repository dialog.
fn folder_selector(controls: &SettingsControls) -> impl IntoElement {
let default_folder = controls.default_folder.clone();
@@ -571,8 +562,7 @@ fn folder_selector(controls: &SettingsControls) -> impl IntoElement {
)
}
/// Parse the server input and append it to the default grasp servers.
/// A bare host is accepted.
/// Accepts a bare host as well as a full URL.
fn add_server(input: &Entity<InputState>, window: &mut Window, cx: &mut App) {
let value = input.read(cx).value().trim().to_owned();
if value.is_empty() {
@@ -604,7 +594,6 @@ fn add_server(input: &Entity<InputState>, window: &mut Window, cx: &mut App) {
input.update(cx, |input, cx| input.set_value("", window, cx));
}
/// Prompt for directories to add to the local-repository scan.
fn add_scan_path(cx: &mut App) {
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
@@ -641,8 +630,7 @@ fn add_scan_path(cx: &mut App) {
.detach();
}
/// Prompt for the Create Repository dialog's default folder.
/// Remember it in the settings and show it in the disabled input.
/// Persists the choice and reflects it in the disabled input.
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();
@@ -676,9 +664,7 @@ fn choose_default_folder(default_folder: &Entity<InputState>, window: &mut Windo
.detach();
}
/// Wire a number input to the settings.
/// Step actions clamp and persist the value.
/// Typed changes parse, clamp and persist.
/// Step actions clamp and persist the value; typed changes parse, clamp and persist.
fn wire_number_input(
state: &Entity<InputState>,
subscriptions: &mut Vec<Subscription>,
@@ -751,7 +737,6 @@ fn wire_number_input(
}));
}
/// Apply the persisted appearance to the live theme.
fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
match appearance {
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
@@ -760,7 +745,6 @@ fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
}
}
/// Re-apply the persisted theme configuration to the live theme.
fn apply_theme(cx: &mut App) {
let store = SettingsStore::global(cx);
let settings = store.read(cx).settings().theme.clone();
-5
View File
@@ -29,7 +29,6 @@ impl Workspace {
let mut subscriptions = vec![];
// Sync the system appearance if the appearance mode is set to system.
if settings.read(cx).settings().appearance == AppearanceMode::System {
subscriptions.push(cx.observe_window_appearance(window, |_this, window, cx| {
Theme::sync_system_appearance(Some(window), cx);
@@ -77,7 +76,6 @@ impl Workspace {
passphrase_dialog::open(window, cx);
}
// Open the sidebar and explore panel.
this.dock.update(cx, |dock_area, cx| {
dock_area.set_dock(
DockPlacement::Left,
@@ -88,7 +86,6 @@ impl Workspace {
dock_area.set_dock_size(DockPlacement::Left, px(240.), window, cx);
});
// Open the explore panel.
weak_sidebar
.update(cx, |this, cx| {
this.open_explore(window, cx);
@@ -114,9 +111,7 @@ impl Render for Workspace {
.size_full()
.relative()
.child(self.dock.clone())
// Notifications
.children(notification_layer)
// Modals
.children(dialog_layer)
}
}