This commit is contained in:
2026-09-03 16:38:52 +07:00
parent 018395d0c5
commit 212f35d6bb
69 changed files with 2130 additions and 2373 deletions
@@ -7,8 +7,8 @@ use signed_core::Announcement;
use signed_state::ProfileStore;
use signed_ui::{UserAvatar, middle_truncate};
/// Open the "About" dialog: every field of the repository's announcement
/// event (NIP-34, kind 30617), as parsed into [`Announcement`].
/// Open the About dialog showing every field of the announcement event.
/// The event is NIP-34 kind 30617, parsed into [`Announcement`].
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();
@@ -22,8 +22,8 @@ pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window,
});
}
/// The announcement's fields as labeled rows; hex identifiers carry a copy
/// button, multi-value tags one line per value.
/// The announcement's fields as labeled rows.
/// Hex identifiers carry a copy button, multi-value tags one line per value.
fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
let mut rows: Vec<AnyElement> = Vec::new();
@@ -116,7 +116,7 @@ fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
v_flex().gap_3().w_full().children(rows).into_any_element()
}
/// One info row: a small muted label above the value.
/// 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()
@@ -159,8 +159,9 @@ fn copy_value(id: &'static str, value: String, cx: &App) -> AnyElement {
.into_any_element()
}
/// One row per maintainer: avatar and display name (falling back to a
/// shortened npub), with a copy button for the full pubkey.
/// 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()
@@ -190,8 +191,8 @@ 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 that copies the full value.
/// 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()
@@ -16,8 +16,8 @@ use super::helpers::{code_language, is_markdown_path};
const TREE_WIDTH: f32 = 240.;
/// Files larger than this are not previewed.
pub(super) const MAX_PREVIEW_BYTES: usize = 1024 * 1024;
/// Preview cache caps: at most this many files (or this many text bytes)
/// are kept in memory at once; the oldest previews are evicted beyond that.
/// 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;
@@ -34,19 +34,19 @@ pub(super) enum FileContent {
}
/// A markdown document loaded into a persistent [`TextViewState`].
///
/// The state is owned by the view rather than created per render: GPUI
/// drops keyed element state after one absent frame, which would re-parse
/// the whole document on every pane switch (README / file / spinner).
/// The state lives in the view rather than being created per render.
/// GPUI drops keyed element state after one absent frame.
/// A per-render state would re-parse the whole document on every pane switch.
pub(super) struct MarkdownView {
/// Source path; `None` means the repository README.
/// Source path, `None` means the repository README.
pub(super) path: Option<SharedString>,
pub(super) state: Entity<TextViewState>,
}
/// A code file loaded into a persistent [`InputState`], rendered as a
/// disabled (read-only) code editor with syntax highlighting, line numbers
/// and search. Persistent for the same reason as [`MarkdownView`].
/// A code file loaded into a persistent [`InputState`].
/// It renders as a disabled, read-only code editor.
/// Syntax highlighting, line numbers and search are included.
/// Persistent for the same reason as [`MarkdownView`].
pub(super) struct CodeView {
/// Source path, relative to the worktree root.
pub(super) path: SharedString,
@@ -64,7 +64,7 @@ fn preview_spinner() -> AnyElement {
}
impl RepoDetailView {
/// One row of the file tree: icon + name, indented by depth.
/// One row of the file tree with icon and name, indented by depth.
fn render_tree_item(
ix: usize,
entry: &TreeEntry,
@@ -81,7 +81,7 @@ impl RepoDetailView {
})
}
/// Left column: the file tree.
/// Left column showing the file tree.
pub(super) fn render_tree_column(
tree_state: Entity<TreeState>,
view: WeakEntity<Self>,
@@ -102,7 +102,7 @@ impl RepoDetailView {
)))
}
/// Right column: README, selected file preview, or status text.
/// Right column, README, selected file preview or status text.
pub(super) fn render_content_column(
&self,
pane_title: SharedString,
@@ -159,9 +159,8 @@ impl RepoDetailView {
placeholder("No README found", cx)
};
// Latest commit for the current pane: the selected file, or the README
// while nothing is selected. Computed after the body above, which
// needs `&mut self`.
// Latest commit for the current pane, the selected file or the README.
// Computed after the body above, which needs `&mut self`.
let commit = match &self.selected_file {
Some(path) => self.commits.get(path.as_ref()),
None => self
@@ -217,9 +216,8 @@ impl RepoDetailView {
}
/// Load `text` into the persistent markdown TextView state.
///
/// The state is created empty and fed via `push_str`, which parses on a
/// background task, so switching files never blocks the main thread.
/// The state is created empty and fed via `push_str`, which parses on a background task.
/// Switching files never blocks the main thread.
pub(super) fn set_markdown(
&mut self,
path: Option<SharedString>,
@@ -231,8 +229,8 @@ impl RepoDetailView {
self.md = Some(MarkdownView { path, state });
}
/// The persistent markdown TextView for `path` (`None` = README), or a
/// spinner while the document is being loaded/parsed.
/// 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();
@@ -254,10 +252,8 @@ impl RepoDetailView {
}
/// Load `text` into the persistent code editor state for `path`.
///
/// The state is created in code editor mode so the Input renders it as
/// a syntax-highlighted, read-only editor; the tree-sitter parse runs
/// on a background task like [`set_markdown`]'s.
/// Code editor mode makes the Input render it read-only and highlighted.
/// The tree-sitter parse runs on a background task like [`set_markdown`]'s.
pub(super) fn set_code(
&mut self,
path: SharedString,
@@ -276,8 +272,7 @@ impl RepoDetailView {
self.code = Some(CodeView { path, state });
}
/// The persistent code editor for `path`, or a spinner while the file is
/// being loaded/parsed.
/// 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();
@@ -70,8 +70,8 @@ pub(super) fn commit_row(
}
impl RepoDetailView {
/// Full-height body of the Commits tab: all commits in a virtual
/// list, or a status message while loading / when there are none.
/// Full-height body of the Commits tab.
/// All commits in a virtual list, or a status message while loading or empty.
pub(super) fn render_commits_tab(&self, cx: &mut Context<Self>) -> AnyElement {
let Some(list) = self.all_commits.as_ref() else {
return if self.loading_all_commits {
@@ -90,9 +90,9 @@ impl RepoDetailView {
return placeholder("No commits found", cx);
}
// Copy only the values the element tree needs; the list itself is
// borrowed inside the renderer below instead of being cloned per
// frame (a full history can be tens of thousands of commits).
// Copy only the values the element tree needs.
// The list is borrowed by the renderer below instead of cloned per frame.
// A full history can be tens of thousands of commits.
let view = cx.entity().clone();
let sizes = self.item_sizes.clone();
let scroll_handle = self.scroll_handle.clone();
@@ -137,7 +137,8 @@ impl RepoDetailView {
.size_full(),
)
.when(shown < total, |this| {
// The history is capped; tell the user the list is truncated.
// The history is capped.
// Tell the user the list is truncated.
this.child(
div()
.py_2()
+26 -28
View File
@@ -28,19 +28,18 @@ use super::helpers::{
/// Width of the changed-files column.
const TREE_WIDTH: f32 = 260.;
/// The tree + per-file diff body shared by the commit diff panel and the
/// compare view of the new-pull-request panel. Owns the changed-files
/// explorer and the virtual list of the selected file's hunks; the host
/// feeds it a [`CommitDiff`] via [`DiffPane::set_diff`].
/// Tree and per-file diff body, shared by the commit diff and compare views.
/// Owns the changed-files explorer and the virtual list of the selected file's hunks.
/// The host feeds it a [`CommitDiff`] via [`DiffPane::set_diff`].
pub struct DiffPane {
/// Loaded diff; `None` until [`Self::set_diff`] is called.
/// 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 + lines), backing the
/// virtual list in the detail column.
/// Rows of the selected file's diff, hunk headers and lines.
/// Backing the virtual list in the detail column.
rows: Vec<DiffRow>,
/// Per-row heights of [`Self::rows`].
item_sizes: Rc<Vec<Size<Pixels>>>,
@@ -90,8 +89,8 @@ impl DiffPane {
}
}
/// Forget the diff (e.g. when the compared branches changed): clear the
/// tree, the selection and the diff rows.
/// 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;
@@ -102,15 +101,14 @@ impl DiffPane {
});
}
/// Show the diff of the file at `path` (selected in the tree).
/// 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 the file at `path` and scroll back
/// to the top.
/// 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;
@@ -123,7 +121,7 @@ impl DiffPane {
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
}
/// One row of the changed-files tree: icon + name, indented by depth.
/// One row of the changed-files tree, icon and name, indented by depth.
fn render_tree_item(
ix: usize,
entry: &TreeEntry,
@@ -140,7 +138,7 @@ impl DiffPane {
})
}
/// Left column: the changed-files tree.
/// 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();
@@ -170,7 +168,7 @@ impl DiffPane {
.into_any_element()
}
/// Right column: header of the selected file plus its diff.
/// 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);
@@ -188,8 +186,9 @@ impl DiffPane {
self.render_file_diff(file, cx.entity(), cx)
}
/// The diff of one file: a header with status and stats, then the hunks
/// in a virtual list (a large diff is never materialized per frame).
/// The diff of one file, with a header showing status and stats.
/// The hunks render in a virtual list.
/// A large diff is never materialized per frame.
fn render_file_diff(&self, file: &FileDiff, view: Entity<Self>, cx: &App) -> AnyElement {
let status_label = match file.status {
DiffStatus::Added => "A",
@@ -316,25 +315,24 @@ impl Render for DiffPane {
}
}
/// Detail panel showing the diff of one commit: a metadata header plus the
/// shared [`DiffPane`] body.
/// Detail panel showing the diff of one commit.
/// A metadata header plus the shared [`DiffPane`] body.
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 (header and tab title). Starts as an id-only
/// stub; [`Self::load`] replaces it with the full metadata, which the
/// history list intentionally omits.
/// The commit being shown in the header and tab title.
/// Starts as an id-only stub, the history list omits the full metadata.
/// [`Self::load`] replaces the stub with the full metadata.
commit: FileCommit,
/// The diff is being computed on a background task.
loading: bool,
error: Option<SharedString>,
/// Changed-files explorer and per-file diff, shared with the compare
/// view of the new-pull-request panel.
/// Changed-files explorer and per-file diff, also used by the new PR panel's compare view.
pane: Entity<DiffPane>,
/// In-flight tasks; pruned on every push (see [`helpers::track`]).
/// In-flight tasks, pruned on every push, see [`helpers::track`].
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
}
@@ -371,8 +369,8 @@ impl CommitDiffView {
}
}
/// Load the commit diff (and the full commit metadata) on a background
/// task and populate the tree.
/// Load the commit diff and the full commit metadata on a background task.
/// Then populate the tree.
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.loading = true;
self.error = None;
@@ -419,7 +417,7 @@ impl CommitDiffView {
self.tasks.push(task);
}
/// Header: commit id, summary, author/time and overall change stats.
/// 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| {
@@ -11,9 +11,9 @@ use signed_core::Announcement;
use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileDiff};
use signed_ui::{menu_copy_row, middle_truncate};
/// A `Send` file-tree node: the tree is built on a background thread and
/// converted into [`TreeItem`]s (which hold `Rc` state,
/// so they cannot cross threads) on the main thread.
/// A `Send` file-tree node, the build runs on a background thread.
/// The main thread converts the seeds into [`TreeItem`]s.
/// [`TreeItem`]s hold `Rc` state and cannot cross threads.
pub(super) struct TreeItemSeed {
/// Path of the node, relative to the worktree root.
id: String,
@@ -22,12 +22,10 @@ pub(super) struct TreeItemSeed {
children: Vec<TreeItemSeed>,
}
/// Convert tree seeds into [`TreeItem`]s, expanding every folder
/// when `expand_folders` is set.
///
/// The commit diff explorer shows only changed files,
/// which is typically a handful of paths, so its folders start expanded;
/// the worktree explorer starts collapsed instead.
/// Convert tree seeds into [`TreeItem`]s.
/// Every folder is expanded when `expand_folders` is set.
/// The commit diff explorer shows only changed files, typically a handful.
/// Its folders start expanded, the worktree explorer's folders collapsed.
pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<TreeItem> {
fn convert(seed: TreeItemSeed, expand_folders: bool) -> TreeItem {
let mut item = TreeItem::new(seed.id, seed.label);
@@ -48,14 +46,13 @@ pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<
.collect()
}
/// Build nested tree items from a flat, sorted (dirs-first) entry list.
///
/// Returns [`TreeItemSeed`]s so the build can run off the main thread; a
/// worktree walk can yield tens of thousands of entries. Nodes live in an
/// arena and parents are found via a path -> index map, which keeps the
/// build linear in the number of path components.
/// Build nested tree items from a flat entry list sorted dirs-first.
/// Returns [`TreeItemSeed`]s so the build can run off the main thread.
/// A worktree walk can yield tens of thousands of entries.
/// Nodes live in an arena, parents are found via a path-to-index map.
/// That keeps the build linear in the number of path components.
pub(super) fn build_tree_items(entries: &[PathBuf]) -> Vec<TreeItemSeed> {
// Node indices by full path, for O(1) parent lookup while inserting.
// Node indices by full path, so parents resolve in constant time while inserting.
let mut index: HashMap<String, usize> = HashMap::new();
let mut nodes: Vec<(String, String, Vec<usize>)> = Vec::new();
let mut roots: Vec<usize> = Vec::new();
@@ -99,9 +96,8 @@ pub(super) fn build_tree_items(entries: &[PathBuf]) -> Vec<TreeItemSeed> {
}
/// The markdown fence language for a file path, or `None` for plain text.
///
/// Names are chosen so `gpui_component`'s highlighter can resolve them
/// (`highlighter::Language::from_name` accepts short aliases such as `rs` and `js`).
/// Names resolve in `gpui_component`'s highlighter.
/// `highlighter::Language::from_name` accepts short aliases like `rs` and `js`.
pub(super) fn code_language(path: &str) -> Option<&'static str> {
let name = Path::new(path)
.file_name()
@@ -166,7 +162,7 @@ pub(super) fn is_markdown_path(path: &str) -> bool {
}
pub(super) struct ShareTargets {
/// NIP-19 `naddr1...` of the announcement (with its announced relays).
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
pub(super) naddr: String,
/// Hex ID of the announcement event itself.
pub(super) event_id: String,
@@ -195,8 +191,8 @@ impl ShareTargets {
}
}
/// The share dropdown menu: one row per target, each showing a compact
/// label while the copy button (and row click) copy the full value.
/// The share dropdown menu, one row per target.
/// Each shows a compact label, the copy button and row click copy the full value.
pub(super) fn menu(&self, menu: PopupMenu) -> PopupMenu {
menu.min_w(px(340.))
.item(menu_copy_row(
@@ -226,9 +222,9 @@ impl ShareTargets {
}
}
/// Shorten an naddr link to `<url>/naddr1...[last tail chars]`, e.g.
/// `https://gitworkshop.dev/naddr1...abcd`. Only the label is shortened;
/// the value to be copied stays the full URL.
/// Shorten an naddr link to `<url>/naddr1...[last tail chars]`.
/// `https://gitworkshop.dev/naddr1...abcd` is an example.
/// Only the label is shortened, the copied value stays the full URL.
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();
@@ -244,7 +240,7 @@ pub(super) const GUTTER_WIDTH: f32 = 44.;
/// Height of one row in a virtual diff list.
pub(super) const DIFF_ROW_HEIGHT: f32 = 20.;
/// One row of a virtual diff list: a hunk header, or a line of a hunk.
/// 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(super) enum DiffRow {
@@ -258,7 +254,7 @@ pub(super) enum DiffRow {
Line { hunk: usize, line: usize },
}
/// The rows of `file`'s diff: one header row per hunk, then its lines.
/// The rows of `file`'s diff, one header row per hunk then its lines.
pub(super) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
let mut rows = Vec::new();
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
@@ -276,7 +272,7 @@ pub(super) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
rows
}
/// One row of the virtual diff list: a hunk header or a single line.
/// One row of the virtual diff list, a hunk header or a single line.
pub(super) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> AnyElement {
match row {
DiffRow::Hunk {
@@ -303,8 +299,8 @@ pub(super) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> Any
}
}
/// One diff line: old and new line numbers in gutters, then the content,
/// tinted by kind (addition / deletion / context).
/// One diff line, old and new line numbers in the gutters.
/// The content is tinted by kind, addition, deletion or context.
pub(super) fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
let bg = match line.kind {
DiffLineKind::Addition => Some(cx.theme().success.opacity(0.2)),
@@ -313,8 +309,8 @@ pub(super) fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
};
let gutter = cx.theme().muted_foreground;
// Fixed height and nowrap: the virtual list assumes every row has
// the same height, so long lines are clipped instead of wrapped.
// Fixed height and nowrap, the virtual list assumes every row has the same height.
// Long lines are clipped instead of wrapped.
h_flex()
.w_full()
.h(px(DIFF_ROW_HEIGHT))
@@ -379,7 +375,7 @@ mod tests {
let items = build_tree_items(&entries);
// Input order is preserved (dirs-first, as produced by worktree_entries).
// Input order is preserved, dirs-first as produced by worktree_entries.
assert_eq!(items.len(), 3);
assert_eq!(items[0].label, "src");
assert_eq!(items[0].id, "src");
@@ -411,9 +407,9 @@ mod tests {
#[test]
fn tree_builder_merges_shared_prefixes() {
// File children of a directory arrive after other directories'
// entries (the worktree list is dirs-first globally); the shared
// prefix must still resolve to one node.
// File children of a directory arrive after other directories' entries.
// The worktree list is dirs-first globally.
// The shared prefix must still resolve to one node.
let entries = vec![
PathBuf::from("a/x.txt"),
PathBuf::from("b/y.txt"),
@@ -461,7 +457,7 @@ mod tests {
truncate_naddr_link("https://gitworkshop.dev/naddr1qqqxyzabc1234", 4),
"https://gitworkshop.dev/naddr1...1234"
);
// No naddr1 prefix: unchanged.
// Without the naddr1 prefix, unchanged.
assert_eq!(
truncate_naddr_link("https://example.com/x", 4),
"https://example.com/x"
@@ -26,10 +26,9 @@ pub struct InitRepoState {
}
/// Open the Init dialog for the local repository at `local_path`.
///
/// The dialog loads the user's default grasp servers (kind `10317` grasp
/// list) and falls back to the shared defaults when none are set. On
/// success the dialog closes and `view` switches into NIP-34 mode.
/// The dialog loads the user's default grasp servers, a kind `10317` grasp list.
/// It falls back to the shared defaults when the user has none set.
/// On success the dialog closes and `view` switches into NIP-34 mode.
pub fn open(
local_path: PathBuf,
view: WeakEntity<RepoDetailView>,
@@ -153,8 +152,8 @@ pub fn open(
});
}
/// Run the init flow; closes the dialog and switches the repository into
/// its NIP-34 mode on success.
/// 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>),
@@ -25,7 +25,7 @@ pub struct IssueDetailView {
store: Entity<RepoStore>,
issue_id: EventId,
contents: HashMap<EventId, SharedString>,
/// Input state of the "leave a comment" textarea.
/// Input state of the comment textarea.
comment_input: Entity<TextareaState>,
focus_handle: FocusHandle,
}
@@ -58,7 +58,7 @@ impl IssueDetailView {
return div().into_any_element();
};
// Participants: the issue author plus everyone who commented.
// Participants, the issue author plus everyone who commented.
let mut participants: Vec<PublicKey> = vec![issue.pubkey];
participants.extend(store.comments_of(&issue.id).map(|comment| comment.pubkey));
participants.sort_by_key(PublicKey::to_hex);
@@ -138,8 +138,7 @@ impl IssueDetailView {
let author = profile.name();
let picture = profile.picture();
let age = relative_time(comment.created_at);
// Comment bodies are cloned into shared strings once per
// comment, not on every render.
// Comment bodies become shared strings once per comment, not per render.
let content = self
.contents
.entry(comment.id)
@@ -24,8 +24,9 @@ use utils::relative_time;
use super::issue_detail::IssueDetailView;
/// Height of one issue row in the virtual list: `py_2` padding, a 32px
/// title line (`h_8`), a 24px meta line (`h_6`) and the 1px bottom border.
/// Height of one issue row in the virtual list.
/// `py_2` padding, a 32px `h_8` title line and a 24px `h_6` meta line.
/// Plus the 1px bottom border.
const ISSUE_ROW_HEIGHT: f32 = 73.;
/// Status filter of the issues list, chosen via the header's filter buttons.
@@ -35,8 +36,8 @@ enum IssueFilter {
All,
/// Issues whose resolved status is [`RepoStatus::Open`].
Open,
/// Issues whose resolved status is [`RepoStatus::Closed`] or
/// [`RepoStatus::Applied`] (both are "done" states).
/// Issues whose resolved status is [`RepoStatus::Closed`].
/// [`RepoStatus::Applied`] counts too, both are done states.
Closed,
}
@@ -63,14 +64,14 @@ pub struct IssuesView {
filter: IssueFilter,
/// Per-row heights of the virtual list.
item_sizes: Rc<Vec<Size<Pixels>>>,
/// Number of rows [`Self::item_sizes`] was built for (the filtered issue count).
/// The filtered issue count [`Self::item_sizes`] was built for.
issue_len: usize,
/// Indices into the store's `issues` matching [`Self::filter`]; the
/// virtual list renders this slice. Rebuilt only when the store
/// version or the filter changes, keyed by [`Self::cache_key`].
/// Indices into the store's `issues` matching [`Self::filter`].
/// The virtual list renders this slice.
/// Rebuilt only when the store version or the filter changes.
/// Keyed by [`Self::cache_key`].
visible_issues: Vec<usize>,
/// Header counts `(total, open, closed)`, rebuilt with
/// [`Self::visible_issues`].
/// Header counts `(total, open, closed)`, rebuilt with [`Self::visible_issues`].
counts: (usize, usize, usize),
/// Store version and filter the cached rows/counts were built from.
cache_key: Option<(u64, IssueFilter)>,
@@ -102,7 +103,7 @@ impl IssuesView {
}
}
/// Open the detail panel of `issue_id` at the bottom of the dock area.
/// Open the detail panel of `issue_id` in the dock area.
fn open_issue_detail(
&mut self,
issue_id: EventId,
@@ -120,8 +121,8 @@ impl IssuesView {
});
}
/// Render one row of the issue list; `ix` is the row index and
/// `issue_ix` the index of the issue in the store's `issues`.
/// Render one row of the issue list.
/// `ix` is the row index, `issue_ix` the index in the store's `issues`.
fn render_row(&self, ix: usize, issue_ix: usize, cx: &mut Context<Self>) -> AnyElement {
let issue = &self.store.read(cx).issues[issue_ix];
let title = activity_subject(issue);
@@ -184,8 +185,8 @@ impl IssuesView {
}
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
// Counts of the last list rebuild (`render` rebuilds first when the
// store version or filter changed, so this is never stale).
// Counts of the last list rebuild.
// `render` rebuilds first when the store version or filter changed, so never stale.
let (total, open, closed) = self.counts;
h_flex()
@@ -243,8 +244,8 @@ impl IssuesView {
}
}
/// Open the "new issue" dialog: a title and a content input that submit
/// through [`RepoStore::open_issue`] when confirmed.
/// Open the new issue dialog, a title and a content input.
/// Confirming submits through [`RepoStore::open_issue`].
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..."));
@@ -333,8 +334,8 @@ impl Render for IssuesView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let filter = self.filter;
// Rebuild the filtered rows and header counts only when the store
// refreshed or the filter changed; other renders reuse the cache.
// Rows and counts are rebuilt only when the store refreshed or filter changed.
// Other renders reuse the cache.
let version = self.store.read(cx).version();
if self.cache_key != Some((version, filter)) {
let store = self.store.read(cx);
@@ -360,8 +361,8 @@ impl Render for IssuesView {
let count = self.visible_issues.len();
// The virtual list's item count comes from `item_sizes`; rebuild it
// whenever the filtered issue count changes.
// The virtual list's item count comes from `item_sizes`.
// Rebuild it whenever the filtered issue count changes.
if count != self.issue_len {
self.issue_len = count;
self.item_sizes = Rc::new(vec![size(px(0.), px(ISSUE_ROW_HEIGHT)); count]);
+241 -243
View File
@@ -66,35 +66,36 @@ use crate::views::repo_detail::new_pull_request::open_new_pull_panel;
/// What kind of ref the header selectors switch to.
#[derive(Clone, Copy, PartialEq, Eq)]
enum RefKind {
/// A local branch (`refs/heads/*`); HEAD stays attached.
/// A local branch `refs/heads/*`, HEAD stays attached.
Branch,
/// A tag (`refs/tags/*`); HEAD becomes detached.
/// A tag `refs/tags/*`, HEAD becomes detached.
Tag,
}
/// Header actions dispatched by the dropdown menus of the header buttons.
/// `pub(super)`: the pull-request list panel offers the same New-PR / Send-
/// patch actions in its own dropdown.
/// `pub(super)` because the pull-request list panel shares this action set.
/// It offers the New-PR and Send-patch actions in its own dropdown.
#[derive(Clone, Action, PartialEq, Eq)]
#[action(namespace = repo_detail, no_json)]
pub(super) enum RepoAction {
/// Open the "new issue" dialog.
/// Open the new issue dialog.
NewIssue,
/// Open the "new pull request" dialog.
/// Open the new pull request dialog.
NewPR,
/// Open the "send patch" panel.
/// 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 the repository from nostr, owner only.
Delete,
}
/// Everything loaded from the local clone for the explorer: the tree seeds,
/// README, refs and HEAD commit. Computed on a background thread (see
/// [`load_repo_data`]) and applied on the main thread.
/// Everything loaded from the local clone for the explorer.
/// The tree seeds, README, refs and HEAD commit.
/// Computed on a background thread, see [`load_repo_data`].
/// Applied on the main thread.
struct RepoData {
tree: Vec<TreeItemSeed>,
readme_path: Option<PathBuf>,
@@ -106,12 +107,13 @@ struct RepoData {
head_commit: Option<FileCommit>,
}
/// Derived NIP-34 header data, cached so renders don't re-encode bech32
/// share targets and rebuild clone command strings on every frame.
/// Derived NIP-34 header data.
/// Renders avoid re-encoding bech32 share targets per frame.
/// They also avoid rebuilding the clone command strings.
struct HeaderCache {
/// Announcement event ID and owner NIP-05 this cache was built from;
/// rebuilt when either changes (a new announcement version, or the
/// owner's profile arriving with a NIP-05 identifier).
/// Announcement event ID and owner NIP-05 this cache was built from.
/// Rebuilt when either changes.
/// A new announcement version, or the owner's profile arriving with a NIP-05 identifier.
key: (EventId, Option<String>),
announcement: Rc<Announcement>,
share: Rc<ShareTargets>,
@@ -120,54 +122,54 @@ struct HeaderCache {
git_commands: Rc<Vec<SharedString>>,
}
/// Detail view of a repository: header, stats, a file explorer with README
/// preview (cloned from the announcement's `clone` URLs), and metadata.
/// 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 the detail view lives in.
/// New panels, commit diffs, are added there.
dock_area: WeakEntity<DockArea>,
/// Snapshot taken at open time, shown until the store's first refresh
/// completes (and as a fallback while the store has no announcement).
/// Snapshot taken at open time.
/// Shown until the store's first refresh completes.
/// Also a fallback while the store has no announcement.
/// `None` for local repositories that haven't been published yet.
initial: Option<Announcement>,
/// Per-repository nostr store (announcement, issues, PRs, statuses).
/// `None` until a local repository is initialized (published) to
/// NIP-34.
/// Per-repository nostr store, holding announcement, issues, PRs and statuses.
/// `None` until a local repository is initialized to NIP-34.
store: Option<Entity<RepoStore>>,
/// Path of the local repository when opened from the scan; `None` once
/// it has been initialized to NIP-34 (or for announced repositories).
/// Path of the local repository when opened from the scan.
/// `None` once it is initialized to NIP-34, or for announced repositories.
local_path: Option<PathBuf>,
/// File explorer state (worktree of the local clone).
/// 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>,
/// Markdown document currently in the preview pane (README or a file).
/// 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 (relative path) and its contents.
/// 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.
/// 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`]).
/// 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.
/// Active header tab, 0 = Files tree, 1 = Commits.
active_tab: usize,
/// Commits reachable from HEAD, newest first; `None` until the walk
/// finishes (or fails). `commits` may be capped by
/// [`CommitList`]; `total` feeds the tab badge.
/// Commits reachable from HEAD, newest first.
/// `None` until the walk finishes or fails.
/// [`CommitList`] caps the list, `total` feeds the tab badge.
all_commits: Option<CommitList>,
/// Commit walk in flight.
loading_all_commits: bool,
@@ -183,52 +185,51 @@ pub struct RepoDetailView {
error: Option<SharedString>,
/// Commit HEAD currently points to, shown in the header button.
head_commit: Option<FileCommit>,
/// Branch selector (header): local branches, searchable.
/// Branch selector in the header, local branches, searchable.
branch_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
/// Tag selector (header): tags, searchable.
/// Tag selector in the header, tags, searchable.
tag_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
/// A branch/tag switch is in flight (checkout plus explorer reload).
/// A branch/tag switch is in flight, checkout plus explorer reload.
switching_ref: bool,
/// Bumped on every branch/tag switch; in-flight loads tagged with an
/// older generation are discarded when they complete.
/// Bumped on every branch/tag switch.
/// In-flight loads with an older generation are discarded when they complete.
ref_generation: u64,
/// Derived NIP-34 header data (share targets, clone commands),
/// rebuilt only when the announcement or the owner's NIP-05 changes
/// instead of on every render.
/// Derived NIP-34 header data, share targets and clone commands.
/// Rebuilt only when the announcement or the owner's NIP-05 changes.
/// Not on every render.
header_cache: Option<HeaderCache>,
/// In-flight tasks; finished tasks are pruned on every push, so the vec
/// stays bounded by the number of concurrent loads.
/// In-flight tasks, finished tasks are pruned on every push.
/// The vec stays bounded by the number of concurrent loads.
tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions keeping the selectors' confirm events alive.
_subscriptions: Vec<Subscription>,
/// Observes the checkouts store, whose statuses feed the "ready to
/// contribute" banner of the repository panel.
/// Observes the checkouts store.
/// Its statuses feed the ready-to-contribute banner of the repository panel.
_checkouts_subscription: 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, and
/// 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).
/// 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.
ready_requested: bool,
ready_head: Option<String>,
/// Upstream repository (from this fork's `u` tag) the user asked to
/// open, while its announcement is still being fetched.
/// Upstream repository, from this fork's `u` tag, the user asked to open.
/// Its announcement is still being fetched.
pending_upstream: Option<RepoAddr>,
}
impl RepoDetailView {
/// Open a repository announced on NIP-34: the store connects to the
/// announcement's relays and loads issues, PRs and statuses.
/// Open a repository announced on NIP-34.
/// The store connects to the announcement's relays and loads issues, PRs and statuses.
pub fn new(
dock_area: WeakEntity<DockArea>,
initial: Announcement,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
// The announcement we opened from already carries the repository's
// NIP-34 `relays` tag, so the store can connect to those relays
// immediately instead of waiting for the bootstrap fetch.
// The announcement we opened from already carries the NIP-34 `relays` tag.
// The store connects to those relays immediately, no bootstrap fetch wait.
let addr = initial.addr();
let relays = initial.relays.clone();
let store = cx.new(|cx| RepoStore::new(addr, relays, cx));
@@ -245,10 +246,9 @@ impl RepoDetailView {
view
}
/// Open a local repository discovered by the scan. There is no
/// announcement and no nostr store until the user initializes
/// (publishes) it to NIP-34, so the header shows an Init button
/// instead of the NIP-34 actions.
/// Open a local repository discovered by the scan.
/// There is no announcement and no nostr store until the user publishes it to NIP-34.
/// The header shows an Init button instead of the NIP-34 actions.
pub fn new_local(
dock_area: WeakEntity<DockArea>,
local_path: PathBuf,
@@ -258,8 +258,8 @@ impl RepoDetailView {
Self::new_common(dock_area, None, None, Some(local_path), window, cx)
}
/// Shared construction: file explorer state, ref selectors and the
/// deferred repository load.
/// Shared construction.
/// File explorer state, ref selectors and the deferred repository load.
fn new_common(
dock_area: WeakEntity<DockArea>,
initial: Option<Announcement>,
@@ -270,7 +270,7 @@ impl RepoDetailView {
) -> Self {
let tree_state = cx.new(|cx| TreeState::new(cx));
// Empty until the clone completes; populated with the local refs.
// Empty until the clone completes, then filled with the local refs.
let branch_select: Entity<ComboboxState<SearchableVec<SharedString>>> = cx.new(|cx| {
ComboboxState::new(
SearchableVec::new(Vec::<SharedString>::new()),
@@ -292,9 +292,9 @@ impl RepoDetailView {
let subscriptions = vec![
cx.subscribe_in(&branch_select, window, |this, _state, event, window, cx| {
// `Change` fires only when the selection actually changed
// (picking the already-selected branch emits nothing), so a
// confirmed value always means a switch.
// `Change` fires only when the selection actually changed.
// Picking the already-selected branch emits nothing.
// A confirmed value always means a switch.
if let ComboboxEvent::Change(values) = event
&& let Some(name) = values.first()
{
@@ -360,18 +360,18 @@ impl RepoDetailView {
}
}
/// Load the repository and populate the file explorer. A local
/// (not yet published) repository is opened straight from disk. An
/// announced repository's local clone (if any) is loaded first without
/// touching the network, so an unreachable server can't block the
/// panel; a background fetch then refreshes the refs and commit list.
/// 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 unreachable server can't block the panel.
/// A background fetch then refreshes the refs and commit list.
fn load_repo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.loading = true;
self.error = None;
cx.notify();
// Local repositories live on disk at their scan path; there is no
// clone to ensure and no network refresh.
// Local repositories live on disk at their scan path.
// No clone step or network refresh applies here.
if let Some(local_path) = self.local_path.clone() {
let task = cx.spawn_in(window, async move |this, cx| {
let data = cx
@@ -401,8 +401,8 @@ impl RepoDetailView {
let cache = GitStore::global(cx).cache().clone();
let addr = initial.addr();
let clone_urls: Vec<String> = initial.clone.iter().map(ToString::to_string).collect();
// Captured before the loads start: a branch/tag switch bumps it, and
// the refresh below is discarded when that happens.
// Captured before the loads start.
// A branch/tag switch bumps the generation, discarding the refresh below.
let refresh_generation = self.ref_generation;
let disk = {
@@ -420,7 +420,7 @@ impl RepoDetailView {
let disk = disk.await;
let had_clone = matches!(&disk, Ok(Some(_)));
// No local clone yet: clone from the network (blocking), then load.
// No local clone yet, so clone from the network then load.
let data = match disk {
Ok(Some(data)) => Ok(data),
Ok(None) => {
@@ -445,9 +445,9 @@ impl RepoDetailView {
cx.notify();
})?;
// Refresh the clone from the network in the background; when it
// completes, update the refs and commit list. Loads started
// before a branch/tag switch are discarded via the generation.
// Refresh the clone from the network in the background.
// When it completes, update the refs and commit list.
// Loads started before a branch/tag switch are discarded via the generation.
if !had_clone {
return Ok(());
}
@@ -458,16 +458,15 @@ impl RepoDetailView {
let Some(repo) = cache.open(&addr)? else {
return Ok::<_, Error>(None);
};
// Best-effort: a failed fetch (e.g. offline) keeps the
// cached state, which is already shown.
// Best-effort, a fetch failure, e.g. offline, keeps the cached state.
// The state is already shown.
signed_git::fetch_all(&repo).ok();
let worktree = repo.workdir().map(Path::to_path_buf);
// A fetch never moves a mirror's local branches, so a
// push landing on the grasp servers (own repo pushed
// from a checkout, or an update fetched here) would
// never show up. Fast-forward them from the remote,
// like `git pull --ff-only` on every branch; only the
// checked-out branch's worktree can change on disk.
// A fetch never moves a mirror's local branches.
// A push landing on the grasp servers would never show up.
// That covers own repo pushes from a checkout and updates fetched here.
// Fast-forward branches from the remote, like `git pull --ff-only`.
// Only the checked-out branch's worktree can change on disk.
let moved = match &worktree {
Some(worktree) => {
signed_git::fast_forward_branches(worktree).unwrap_or(false)
@@ -494,10 +493,9 @@ impl RepoDetailView {
}
if let Ok(Some((moved, branches, tags, current_branch, head_commit))) = refresh {
if moved {
// The mirror caught up with the remote (e.g. the
// push of an owned checkout just landed): rebuild
// the explorer, previews and commit list from the
// updated worktree.
// The mirror caught up with the remote.
// E.g. the push of an owned checkout just landed.
// Rebuild the explorer, previews and commit list from the worktree.
this.reload_worktree(cx);
cx.notify();
return;
@@ -539,8 +537,9 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Apply the loaded repository data: explorer tree, README preview,
/// ref selectors and HEAD commit, then start the commit-list walk.
/// Apply the loaded repository data.
/// Sets the explorer tree, README preview, ref selectors and HEAD commit.
/// Then starts the commit-list walk.
fn apply_repo_data(&mut self, data: RepoData, window: &mut Window, cx: &mut Context<Self>) {
let RepoData {
tree,
@@ -563,8 +562,8 @@ impl RepoDetailView {
state.set_items(tree_items(tree, false), cx);
});
// Populate the branch/tag selectors with the local refs,
// selecting the branch HEAD points to.
// 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();
@@ -591,8 +590,8 @@ impl RepoDetailView {
}
}
/// Clone the repository into a folder chosen by the user (outside the cache),
/// then open the new clone in the system file manager.
/// Clone the repository into a user-chosen folder outside the cache.
/// Then open the new clone in the system file manager.
fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.cloning {
return;
@@ -605,8 +604,8 @@ impl RepoDetailView {
let addr = announcement.addr();
let clone_urls: Vec<String> =
announcement.clone.iter().map(ToString::to_string).collect();
// Directory name: the display name, falling back to the repo id;
// both sanitized to a safe single path component.
// Directory name, the display name falling back to the repo id.
// Both are sanitized to a safe single path component.
let name = announcement
.name
.as_ref()
@@ -633,8 +632,8 @@ impl RepoDetailView {
});
let task = cx.spawn_in(window, async move |this, cx| {
// `Ok(Ok(Some(paths)))` means the user picked a folder; a
// cancel (or a picker failure) resolves to anything else.
// `Ok(Ok(Some(paths)))` means the user picked a folder.
// A cancel or picker failure resolves to anything else.
let picked = match prompt.await {
Ok(Ok(Some(mut paths))) => paths.pop(),
_ => None,
@@ -659,8 +658,8 @@ impl RepoDetailView {
match result {
Ok(_) => {
cx.open_with_system(&destination_for_open);
// Remember the clone as a checkout of this
// repository, so the New PR panel pre-fills it.
// Remember the clone as a checkout of this repository.
// The New PR panel pre-fills it.
let checkouts = CheckoutsStore::global(cx);
checkouts.update(cx, |store, cx| {
store.record(destination, addr, cx);
@@ -679,15 +678,14 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Preview the file at `path` (relative to the worktree root).
/// 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());
if self.files.contains_key(path) {
// The file is cached, but the persistent markdown/code state may
// still hold a different file; re-point it at this one (the parse
// runs on a background task either way). Without this, the pane
// would show a spinner forever.
// The file is cached, but the markdown or code state may hold a different file.
// Re-point it at this one, the parse runs on a background task either way.
// Without this, the pane would show a spinner forever.
if let Some(FileContent::Text(text)) = self.files.get(path) {
let text = text.clone();
if is_markdown_path(path) {
@@ -706,8 +704,8 @@ impl RepoDetailView {
return;
}
// Paths come from our own tree walk, but never trust them: refuse
// anything that could escape the worktree.
// Paths come from our own tree walk, but never trust them.
// Refuse anything that could escape the worktree.
let rel = Path::new(path);
let unsafe_path = rel.is_absolute()
|| rel.components().any(|c| {
@@ -735,9 +733,9 @@ impl RepoDetailView {
let content = cx
.background_spawn(async move {
let full = worktree.join(&path_for_read);
// Refuse oversized files before reading them: reading a
// multi-gigabyte file just to classify it as too large
// would waste the disk and memory bandwidth.
// Refuse oversized files before reading them.
// Reading a multi-gigabyte file just to classify it is wasteful.
// It would burn disk and memory bandwidth.
let metadata = match std::fs::metadata(&full) {
Ok(metadata) => metadata,
Err(error) => return Err(anyhow::anyhow!("{}", error)),
@@ -757,10 +755,10 @@ impl RepoDetailView {
.await;
this.update_in(cx, |this, window, cx| {
// The worktree was switched while this file was reading;
// the result belongs to the previous branch. Clear the
// in-flight marker either way, or the path could never be
// loaded again.
// The worktree was switched while this file was reading.
// The result belongs to the previous branch.
// Clear the in-flight marker either way.
// Otherwise the path could never be loaded again.
if generation != this.ref_generation {
this.loading_files.remove(&path);
return;
@@ -802,8 +800,8 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Queue `path` for the per-file commit query; requests are batched into
/// one history walk (see [`Self::load_commits`]).
/// Queue `path` for the per-file commit query.
/// Requests are batched into one history walk, see [`Self::load_commits`].
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;
@@ -814,10 +812,10 @@ impl RepoDetailView {
}
}
/// Walk history once for every queued path on a background task, and
/// cache the latest commit touching each of them in [`Self::commits`]
/// (for the file header in the content column). Batching shares one
/// walk across all paths queued while the previous walk was in flight.
/// 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;
@@ -849,10 +847,9 @@ impl RepoDetailView {
.insert(path.to_string_lossy().into_owned(), commit);
}
}
// Paths queued while the walk was in flight start the next
// batch. A stale walk (branch switched mid-flight) must not
// strand them, so this runs under the current generation
// regardless of whether the result was applied.
// Paths queued while the walk was in flight start the next batch.
// A stale walk, branch switched mid-flight, must not strand them.
// This runs under the current generation regardless of the result.
if !this.pending_commits.is_empty() {
this.load_commits(cx);
}
@@ -865,9 +862,9 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Walk all commits reachable from HEAD on a background task, for the
/// Commits tab and its total-count badge. The list is capped by
/// [`CommitList`]; only the newest commits are materialized.
/// 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.
fn load_all_commits(&mut self, cx: &mut Context<Self>) {
if self.loading_all_commits || self.all_commits.is_some() {
return;
@@ -886,8 +883,8 @@ impl RepoDetailView {
.await;
this.update(cx, |this, cx| {
// A stale walk (branch switched mid-flight) must not leave
// the flag set, or the Commits tab would spin forever.
// A stale walk, branch switched mid-flight, must not leave the flag set.
// Otherwise the Commits tab would spin forever.
if generation != this.ref_generation {
this.loading_all_commits = false;
return;
@@ -907,9 +904,9 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Open a new panel showing the diff of `commit_id` (all files it
/// changed, with the line diff of each). Called from the Commits tab
/// rows and the latest-commit button in the header.
/// Open a new panel showing the diff of `commit_id`.
/// All files it changed, with the line diff of each.
/// Called from the Commits tab rows and the latest-commit button.
fn open_commit_diff(&mut self, commit_id: &str, window: &mut Window, cx: &mut Context<Self>) {
let Some(worktree) = self.worktree.clone() else {
return;
@@ -930,9 +927,9 @@ impl RepoDetailView {
});
}
/// Re-push the repository's refs to its announced grasp servers; the
/// menu trigger shows a spinner while the push is in flight, failures
/// appear in the panel's error banner.
/// Re-push the repository's refs to its announced grasp servers.
/// The menu trigger shows a spinner while the push is in flight.
/// Failures appear in the panel's error banner.
fn push_repository(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.pushing {
return;
@@ -960,10 +957,10 @@ impl RepoDetailView {
}));
}
/// Push the unpushed commits of the local checkout at
/// `path` (an owned repository's working copy) to the announced grasp servers,
/// failures appear in the panel's error banner,
/// and on success the push statuses are recomputed so the banner clears.
/// Push the unpushed commits of the local checkout at `path`.
/// The checkout is an owned repository's working copy.
/// Failures appear in the panel's error banner.
/// On success the push statuses are recomputed so the banner clears.
fn push_unpushed_checkout(
&mut self,
path: PathBuf,
@@ -978,8 +975,8 @@ impl RepoDetailView {
return;
};
// Keep the repository's announced default branch as the state
// event's `HEAD` when the checkout is on a side branch.
// The state event's `HEAD` stays the announced default branch.
// The checkout may be on a side branch.
let head = self
.store
.as_ref()
@@ -1003,10 +1000,10 @@ impl RepoDetailView {
this.update_in(cx, |this, window, cx| {
match result {
Ok(()) => {
// The remote moved; recompute the push statuses so
// the banner disappears, and refresh the mirror so
// the pushed commits appear in the panel right away
// (fetch + fast-forward + explorer reload).
// The remote moved, so recompute the push statuses.
// The banner disappears.
// Refresh the mirror, fetch, fast-forward and an explorer reload.
// The pushed commits then show in the panel.
checkout.update(cx, |store, cx| {
store.request_push_statuses(&addr, cx);
});
@@ -1024,9 +1021,9 @@ impl RepoDetailView {
}));
}
/// Delete the repository from nostr (announcement, state and activity);
/// only offered to the repository owner. The sidebar list updates when
/// the deletion events arrive.
/// Delete the repository from nostr, announcement, state and activity.
/// Only offered to the repository owner.
/// The sidebar list updates when the deletion events arrive.
fn delete_repository(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(announcement) = self.announcement(cx).cloned() else {
return;
@@ -1048,7 +1045,7 @@ impl RepoDetailView {
}));
}
/// Open the issues panel at the bottom of the dock area.
/// Open the issues list panel in the dock area.
fn open_issue_detail(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(store) = self.store.clone() else {
return;
@@ -1064,7 +1061,7 @@ impl RepoDetailView {
});
}
/// Open the pull requests panel at the bottom of the dock area.
/// Open the pull requests list panel in the dock area.
fn open_pull_request_detail(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(store) = self.store.clone() else {
return;
@@ -1080,9 +1077,9 @@ impl RepoDetailView {
});
}
/// Open the upstream repository (the `u` tag of this fork's announcement).
/// When the upstream announcement is not in the local database yet,
/// subscribe for it and open the panel as soon as it lands.
/// Open the upstream repository, the `u` tag of this fork's announcement.
/// The upstream announcement may not be in the local database yet.
/// Subscribe for it and open the panel as soon as it lands.
fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.pending_upstream.is_some() {
return;
@@ -1151,8 +1148,8 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Check out `name` (a branch or tag picked in the header) and refresh
/// the explorer once the switch completes.
/// Check out `name`, a branch or tag picked in the header.
/// Refresh the explorer once the switch completes.
fn switch_ref(
&mut self,
kind: RefKind,
@@ -1167,9 +1164,9 @@ impl RepoDetailView {
return;
};
// Branches and tags are mutually exclusive states of HEAD: selecting
// one clears the other selector. Remember the previous selections so
// they can be restored if the checkout fails.
// Branches and tags are mutually exclusive states of HEAD.
// Selecting one clears the other selector.
// Remember the previous selections to restore them if the checkout fails.
let previous_branch = self.branch_select.read(cx).selected_value();
let previous_tag = self.tag_select.read(cx).selected_value();
@@ -1184,8 +1181,7 @@ impl RepoDetailView {
}
}
self.switching_ref = true;
// In-flight loads of the previous branch are discarded when they
// complete.
// In-flight loads of the previous branch are discarded when they complete.
self.ref_generation += 1;
cx.notify();
@@ -1223,7 +1219,7 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Restore a selector to `previous`, or clear it (after a failed switch).
/// Restore a selector to `previous`, or clear it after a failed switch.
fn restore_selection(
&self,
select: &Entity<ComboboxState<SearchableVec<SharedString>>>,
@@ -1237,9 +1233,10 @@ impl RepoDetailView {
});
}
/// Trigger body for the branch/tag selectors: the kind icon, the
/// selection (or placeholder) and the caret. `Combobox` replaces its
/// default trigger entirely, the only way to show an icon inside it.
/// Trigger body for the branch/tag selectors.
/// The kind icon, the selection or placeholder, and the caret.
/// `Combobox` replaces its default trigger entirely.
/// That is the only way to show an icon inside it.
fn render_ref_trigger(
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
icon: CustomIconName,
@@ -1273,10 +1270,10 @@ impl RepoDetailView {
.into_any_element()
}
/// Refresh the file explorer, preview pane and commit list after a
/// successful branch or tag switch. The selectors were already updated
/// by [`Self::switch_ref`]; [`Self::switching_ref`] stays set until this
/// reload finishes, so a second switch cannot interleave.
/// 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;
@@ -1297,9 +1294,9 @@ impl RepoDetailView {
match result {
Ok((snapshot, tree)) => {
this.head_commit = snapshot.head_commit;
// Rebuild the tree from scratch: entries of the
// previous branch are gone, and with them the
// expansion state.
// Rebuild the tree from scratch.
// Entries of the previous branch are gone.
// The expansion state goes with them.
this.tree_state.update(cx, |state, cx| {
state.set_items(tree_items(tree, false), cx);
});
@@ -1346,9 +1343,10 @@ impl RepoDetailView {
self.tasks.push(task);
}
/// Drop the oldest previews beyond the cache caps, keeping the currently
/// selected file. The parsed editor state of an evicted file is dropped
/// along with its entry, so re-opening it re-parses on a background task.
/// Drop the oldest previews beyond the cache caps.
/// Keep the currently selected file.
/// An evicted file's parsed editor state drops with its entry.
/// Re-opening it re-parses on a background task.
fn evict_previews(&mut self) {
while (self.files.len() > MAX_PREVIEWED_FILES
|| self.preview_bytes > MAX_PREVIEW_CACHE_BYTES)
@@ -1376,7 +1374,7 @@ impl RepoDetailView {
}
}
/// The latest announcement from the store, or the open-time snapshot;
/// The latest announcement from the store or the open-time snapshot.
/// `None` for local repositories that haven't been published yet.
fn announcement<'a>(&'a self, cx: &'a App) -> Option<&'a Announcement> {
let store = self.store.as_ref()?;
@@ -1387,8 +1385,8 @@ impl RepoDetailView {
.or(self.initial.as_ref())
}
/// Display name: the announcement's name (or ID) for announced
/// repositories, the directory name for local ones.
/// Display name, the announcement's name or ID for announced repositories.
/// The directory name for local ones.
fn display_name(&self, cx: &App) -> SharedString {
if let Some(path) = &self.local_path {
return SharedString::from(
@@ -1407,9 +1405,8 @@ impl RepoDetailView {
.unwrap_or_default()
}
/// The NIP-34 header (actions, issues/PR counts) or, for a local
/// repository that hasn't been published yet, the local header with an
/// Init button.
/// The NIP-34 header, actions and issues/PR counts.
/// Or the local header with an Init button for an unpublished repository.
fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
if self.local_path.is_some() {
return self.render_local_header(cx);
@@ -1426,9 +1423,9 @@ impl RepoDetailView {
return div().into_any_element();
};
// The header derives bech32 share targets and clone command strings
// from the announcement; rebuild them only when the announcement or
// the owner's NIP-05 changes, not on every render.
// The header derives bech32 share targets and clone commands.
// Rebuild them only when the announcement or the owner's NIP-05 changes.
// Not on every render.
let nip05 = ProfileStore::global(cx)
.read(cx)
.get(&source.owner)
@@ -1815,9 +1812,8 @@ 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
/// (issues, pull requests, share, info, clone).
/// 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
@@ -1879,8 +1875,7 @@ impl RepoDetailView {
.into_any_element()
}
/// Open the dialog guiding the user through publishing the local
/// repository to NIP-34.
/// Open the dialog guiding the user through publishing the local repository to NIP-34.
fn open_init_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(local_path) = self.local_path.clone() else {
return;
@@ -1889,47 +1884,47 @@ impl RepoDetailView {
init_dialog::open(local_path, view, window, cx);
}
/// Switch the repository into its NIP-34 mode after a successful init:
/// create the nostr store for the announced repository and drop the
/// local (scan) identity. The worktree is unchanged, so the file
/// explorer keeps its loaded content.
/// Switch the repository into its NIP-34 mode after a successful init.
/// Creates the nostr store for the announced repository.
/// Drops the local scan identity.
/// The worktree is unchanged, so the explorer keeps its loaded content.
pub(crate) fn apply_announcement(
&mut self,
announcement: Announcement,
cx: &mut Context<Self>,
) {
// The repository is no longer a bare local repo: drop it from the
// scan results so it leaves the sidebar's local section immediately.
// The repository is no longer a bare local repo.
// Drop it from the scan results so it leaves the sidebar's local section.
if let Some(path) = self.local_path.take() {
LocalReposStore::global(cx).update(cx, |store, cx| store.remove(&path, cx));
}
let store =
cx.new(|cx| RepoStore::new(announcement.addr(), announcement.relays.clone(), cx));
// Re-render on store refreshes (issues, PRs, statuses) and keep the
// "ready to contribute" statuses of this repository requested.
// Re-render on store refreshes, issues, PRs and statuses.
// Keep the ready-to-contribute statuses of this repository requested.
self.attach_store(&store, cx);
self.store = Some(store);
self.initial = Some(announcement);
cx.notify();
}
/// Observe the repository's store (re-render on refreshes) and request
/// the "ready to contribute" statuses for it.
/// Observe the repository's store, re-render on refreshes.
/// Request the ready-to-contribute statuses for it.
fn attach_store(&mut self, store: &Entity<RepoStore>, cx: &mut Context<Self>) {
self._subscriptions
.push(cx.observe(store, |this, _store, cx| {
cx.notify();
// The first refresh fills the announced HEAD, which defaults
// the banner's base branch; re-request when it changes.
// The first refresh fills the announced HEAD.
// It defaults the banner's base branch, re-request when it changes.
this.refresh_ready_statuses(cx);
}));
self.refresh_ready_statuses(cx);
}
/// (Re)request the statuses of this repository when the announced
/// HEAD - the base the checkouts are compared against, changed since the last request.
/// Repositories the user owns are watched for unpushed commits,
/// other repositories for ready-to-contribute checkouts.
/// Request the statuses of this repository again when the announced HEAD changes.
/// The HEAD is the base the checkouts are compared against.
/// Owned repositories are watched for unpushed commits.
/// Other repositories for ready-to-contribute checkouts.
fn refresh_ready_statuses(&mut self, cx: &mut Context<Self>) {
let Some(entity) = self.store.clone() else {
return;
@@ -1954,8 +1949,8 @@ impl RepoDetailView {
.is_some_and(|user| entity.read(cx).is_author(&user));
checkout.update(cx, |store, cx| {
// The ready statuses also keep the fast poll running while the
// panel is open (the sidebar's push watch alone polls slower).
// The ready statuses keep the fast poll running while the panel is open.
// The sidebar's push watch alone polls slower.
store.request_statuses(&addr, head, cx);
if owned {
@@ -1964,10 +1959,11 @@ impl RepoDetailView {
});
}
/// The first checkout ready for a pull request on this repository,
/// not covered by an open PR of the signed-in user and not dismissed in
/// this panel. The repository's own checkouts are not suggested here:
/// their work is pushed (see [`Self::push_suggestion`]).
/// The first checkout ready for a pull request on this repository.
/// Not covered by an open PR of the signed-in user.
/// Not dismissed in this panel.
/// The repository's own checkouts are not suggested here.
/// Their work is pushed, see [`Self::push_suggestion`].
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
let store = self.store.as_ref()?;
let addr = store.read(cx).addr().clone();
@@ -1998,8 +1994,8 @@ impl RepoDetailView {
None
}
/// The first checkout of this owned repository with unpushed commits,
/// not dismissed in this panel.
/// The first checkout of this owned repository with unpushed commits.
/// Not dismissed in this panel.
fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
let entity = self.store.as_ref()?;
let user = Backend::global(cx).read(cx).current_user()?;
@@ -2015,8 +2011,8 @@ 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.
/// The ready-to-push banner of an owned repository.
/// A local checkout has unpushed commits, with a Push action and a dismiss control.
fn render_push_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
let status = self.push_suggestion(cx)?;
let commits = if status.ahead == 1 {
@@ -2067,9 +2063,9 @@ impl RepoDetailView {
)
}
/// The "ready to contribute" banner of the repository panel: message,
/// a Create action opening the prefilled New PR panel, and a dismiss
/// control.
/// The ready-to-contribute banner of the repository panel.
/// A message, a Create action opening the prefilled New PR panel.
/// Plus a dismiss control.
fn render_ready_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
let status = self.ready_suggestion(cx)?;
let commits = if status.ahead == 1 {
@@ -2119,8 +2115,8 @@ impl RepoDetailView {
)
}
/// The tab row shared by both header variants: Files/Commits tabs, the
/// HEAD commit button and the branch/tag selectors.
/// 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();
@@ -2379,8 +2375,8 @@ impl Render for RepoDetailView {
}
}
/// Read the worktree state of `repo` (no network): entries, README, refs
/// and HEAD commit.
/// 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);
@@ -2390,8 +2386,9 @@ fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
None => None,
};
let worktree = repo.workdir().map(Path::to_path_buf);
// Ref listing is auxiliary UI: a broken ref must not prevent the
// explorer from loading, so failures degrade to empty selectors.
// Ref listing is auxiliary UI.
// A broken ref must not prevent the explorer from loading.
// Failures degrade to empty selectors.
let (branches, tags, current_branch) = match &worktree {
Some(_) => (
signed_git::repo_branches(repo).unwrap_or_default(),
@@ -2414,10 +2411,10 @@ fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
})
}
/// The `nostr://...` clone URL of an announcement (NIP-34): the owner as a
/// NIP-05 identifier when known (npub otherwise), the first announced relay
/// as a hint, and the repository identifier. `nip05` is the owner's
/// NIP-05 identifier from the profile store, already blank-filtered.
/// The `nostr://...` clone URL of an announcement, NIP-34.
/// The owner as a NIP-05 identifier when known, npub otherwise.
/// The first announced relay is a hint, plus the repository identifier.
/// `nip05` is the owner's NIP-05 identifier from the profile store, already blank-filtered.
fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedString {
let owner = announcement.owner;
let user = nip05
@@ -2435,16 +2432,16 @@ fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedSt
SharedString::from(url)
}
/// The "Forked from …" row of the detail header: a clickable link to the
/// upstream repository when the `u` tag references a NIP-34 repo,
/// plain text when it only carries a git 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()?;
let (label, clickable) = match &upstream.addr {
Some(addr) => {
// Prefer the upstream's display name when its announcement
// is already known locally fall back to its repository id.
// Prefer the upstream's display name when its announcement is known locally.
// Fall back to its repository id otherwise.
let name = RepoListStore::global(cx)
.read(cx)
.announcements
@@ -2481,9 +2478,10 @@ fn fork_row(announcement: &Announcement, cx: &mut Context<RepoDetailView>) -> Op
})
}
/// Open `announcement` as a repository panel in the dock's center, returning
/// the new detail view. Shared by the explore list, the sidebar and fork
/// links so every entry point opens repositories identically.
/// Open `announcement` as a repository panel in the dock's center.
/// Returns the new detail view.
/// Shared by the explore list, the sidebar and fork links.
/// Every entry point opens repositories identically.
pub(crate) fn open_repo_panel(
dock_area: &WeakEntity<DockArea>,
announcement: &Announcement,
@@ -1,9 +1,3 @@
//! The "new pull request" panel: pick a compare source — a local checkout
//! or an announced fork of the repository — a base and a compare branch
//! (GitHub-style), review the diff and the commit list, then publish the PR
//! with only a title and an optional description. The patch series is
//! generated at submit time; there is no patch input.
use std::path::{Path, PathBuf};
use std::rc::Rc;
@@ -41,47 +35,44 @@ use signed_ui::placeholder;
use super::commits::{COMMIT_ROW_HEIGHT, commit_row};
use super::diff::{CommitDiffView, DiffPane};
/// The "new pull request" panel of a repository.
///
/// The compare side of the PR comes from one of two sources:
///
/// - **Local checkout**: both branch selectors list a user-picked local
/// checkout's branches; git ops and the tip push run in the checkout.
/// - **Announced fork**: the fork's branches are fetched into the target
/// repository's GitCache mirror under `refs/fork/<owner>/<id>/*`, the base
/// selector lists the mirror's `refs/remotes/origin/*` branches, and all
/// git ops run in the mirror.
///
/// The compare view (Files/Commits tabs) is built from `merge-base..compare`
/// of the chosen refs, and the patch series published with the PR is
/// generated from the same range at submit time.
/// The new pull request panel of a repository.
/// The compare side comes from a local checkout or an announced fork.
/// A local checkout lists its own branches in both selectors.
/// Git ops and the tip push run in the checkout.
/// An announced fork imports its branches into the target's GitCache mirror.
/// The base selector lists the mirror's `refs/remotes/origin/*` branches.
/// All git ops run in the mirror.
/// The Files and Commits tabs are built from `merge-base..compare` of the chosen refs.
/// The patch series published with the PR comes from the same range at submit time.
pub struct NewPullRequestView {
focus_handle: FocusHandle,
/// Dock area the panel lives in; commit diffs are opened there.
/// Dock area the panel lives in, commit diffs are opened there.
dock_area: WeakEntity<DockArea>,
/// Store of the target repository (for the announced HEAD default).
/// 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: where both branches live in checkout mode
/// and where the tip is pushed from. `None` until a folder is picked.
/// The user's local checkout.
/// Both branches live there in checkout mode and the tip is pushed from there.
/// `None` until a folder is picked.
repo_path: Option<PathBuf>,
/// Branches of the checkout, backing both selectors in checkout mode.
branches: Vec<SharedString>,
/// Fork-backed compare state; `Some` switches the panel into fork mode
/// (the checkout above is kept so the user can switch back).
/// Fork-backed compare state.
/// `Some` switches the panel into fork mode.
/// The checkout above is kept so the user can switch back.
fork: Option<ForkCompare>,
/// Selected base branch (the target of the PR), short name.
/// Selected base branch, the PR target, stored as a short name.
base: SharedString,
/// Selected compare branch (the source of the PR), short name.
/// Selected compare branch, the PR source, stored as a short name.
compare: SharedString,
base_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
compare_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
/// Title input (required).
/// Title input, required.
subject: Entity<InputState>,
/// Description input (optional).
/// Description input, optional.
description: Entity<TextareaState>,
/// Merge base of the selected branches; `None` until the compare loads.
/// 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>>,
@@ -89,13 +80,13 @@ pub struct NewPullRequestView {
loading: bool,
/// Error of the last compare or submit attempt.
error: Option<SharedString>,
/// A submit (patch generation + publish) is in flight.
/// A submit, patch generation and publish, is in flight.
submitting: bool,
/// Bumped on every branch switch; stale compare results are discarded.
/// Bumped on every branch switch, stale compare results are discarded.
compare_generation: u64,
/// Active tab: 0 = Files, 1 = Commits.
/// Active tab, 0 = Files and 1 = Commits.
active_tab: usize,
/// The compare diff (Files tab).
/// The compare diff, the Files tab body.
pane: Entity<DiffPane>,
/// Virtual list state of the Commits tab.
scroll_handle: VirtualListScrollHandle,
@@ -104,13 +95,13 @@ pub struct NewPullRequestView {
tasks: Vec<Task<Result<(), anyhow::Error>>>,
}
/// A fork-backed compare: the fork's heads are imported into the target
/// repository's GitCache mirror under `refs/fork/<namespace>/*`, and the
/// mirror's own `refs/remotes/origin/*` track the base branches.
/// A fork-backed compare.
/// The fork's heads are imported into the target mirror under `refs/fork/<namespace>/*`.
/// The mirror's own `refs/remotes/origin/*` refs track the base branches.
struct ForkCompare {
/// Fork announcement the compare branch is imported from.
announcement: Announcement,
/// Import namespace: `<owner-hex>/<sanitized-id>`.
/// Import namespace of the form `<owner-hex>/<sanitized-id>`.
namespace: String,
/// Path of the target repository's GitCache mirror.
mirror_path: PathBuf,
@@ -137,11 +128,11 @@ fn fork_namespace(announcement: &Announcement) -> String {
)
}
/// The announced forks of `base` a New PR compare can be built from:
/// announcements related by `u` tag or shared EUC, excluding the base
/// itself and announcements without `clone` URLs (unfetchable). Own forks
/// (announced by `user`) come first; the input order (newest first, as
/// `RepoListStore` keeps it) is preserved within each group.
/// The announced forks of `base` a New PR compare can be built from.
/// Related by `u` tag or shared EUC, excluding the base itself.
/// Announcements without `clone` URLs are unfetchable and excluded.
/// Own forks, announced by `user`, come first.
/// Newest first as `RepoListStore` keeps them, order is preserved within each group.
fn fork_candidates<'a>(
announcements: &'a [Announcement],
base: &RepoAddr,
@@ -162,8 +153,8 @@ fn fork_candidates<'a>(
own.into_iter().chain(others).collect()
}
/// The display name of an announcement: its human-readable name, falling
/// back to the repository id.
/// 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
@@ -171,7 +162,7 @@ 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 (hex prefix).
/// 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()
@@ -190,8 +181,8 @@ 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).
/// 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,
@@ -237,8 +228,8 @@ 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.
/// 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,
@@ -264,7 +255,7 @@ fn fork_source_item(
})
}
/// One row of the compare-source menu: icon, title and a muted subtitle.
/// 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>,
@@ -340,8 +331,7 @@ impl NewPullRequestView {
});
let subscriptions = vec![
// Re-evaluate the Create button's enabled state as the title
// changes.
// Re-evaluate the Create button's enabled state as the title changes.
cx.subscribe(&subject, |_this, _state, _event: &InputEvent, cx| {
cx.notify();
}),
@@ -395,8 +385,7 @@ impl NewPullRequestView {
tasks: Vec::new(),
};
// Prefill: when the store knows an associated checkout of this repository,
// apply the freshest one right away (no folder dialog).
// Prefill with the store's freshest associated checkout, no folder dialog.
let addr = view.store.read(cx).addr().clone();
if let Some(path) = CheckoutsStore::global(cx)
.read(cx)
@@ -410,13 +399,13 @@ impl NewPullRequestView {
view
}
/// Whether a compare source (a checkout or a fork) is applied.
/// 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()
}
/// The path git ops run against: the target's mirror in fork mode, the
/// user's checkout otherwise.
/// The path git ops run against.
/// The target's mirror in fork mode, the user's checkout otherwise.
fn work_path(&self) -> Option<PathBuf> {
match &self.fork {
Some(fork) => Some(fork.mirror_path.clone()),
@@ -424,9 +413,9 @@ impl NewPullRequestView {
}
}
/// The full ref the selected base branch resolves to: the mirror's
/// remote-tracking ref in fork mode, the plain branch name in checkout
/// mode (where git resolves it through `refs/heads`).
/// The full ref the selected base branch resolves to.
/// The mirror's remote-tracking ref in fork mode.
/// The plain branch name in checkout mode, git resolves it through `refs/heads`.
fn base_ref(&self) -> String {
match &self.fork {
Some(_) => ForkCompare::base_ref(&self.base),
@@ -434,9 +423,9 @@ impl NewPullRequestView {
}
}
/// The full ref the selected compare branch resolves to: the imported
/// `refs/fork/<namespace>` ref in fork mode, the plain branch name in
/// checkout mode.
/// The full ref the selected compare branch resolves to.
/// The imported `refs/fork/<namespace>` ref in fork mode.
/// The plain branch name in checkout mode.
fn compare_ref(&self) -> String {
match &self.fork {
Some(fork) => fork.compare_ref(&self.compare),
@@ -444,9 +433,10 @@ impl NewPullRequestView {
}
}
/// Prompt for a local checkout; on success populate the branch selectors
/// (defaults: the announced HEAD branch for the base, the checkout's
/// current branch for the compare) and load the compare.
/// Prompt for a local checkout.
/// On success populate the branch selectors and load the compare.
/// Defaults are the announced HEAD branch for the base.
/// The checkout's current branch is the default for the compare.
fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
@@ -456,8 +446,8 @@ impl NewPullRequestView {
});
let task = cx.spawn_in(window, async move |this, cx| {
// `Ok(Ok(Some(paths)))` means the user picked a folder; a
// cancel (or a picker failure) resolves to anything else.
// `Ok(Ok(Some(paths)))` means the user picked a folder.
// A cancel or picker failure resolves to anything else.
let picked = match prompt.await {
Ok(Ok(Some(mut paths))) => paths.pop(),
_ => None,
@@ -475,8 +465,8 @@ impl NewPullRequestView {
self.tasks.push(task);
}
/// Apply `path` as the local checkout (no picker): read its branches
/// and current branch off the UI thread, then apply.
/// Apply `path` as the local checkout, no picker.
/// Branches and 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();
@@ -504,9 +494,10 @@ impl NewPullRequestView {
self.tasks.push(task);
}
/// Apply a picked checkout: fill the selectors and load the compare.
/// Leaves fork mode; a fork applied earlier keeps its import in the
/// mirror (harmless) but the panel switches back to the checkout.
/// Apply a picked checkout, filling the selectors and loading the compare.
/// Leaves fork mode.
/// A fork applied earlier keeps its import in the mirror, harmless.
/// The panel switches back to the checkout.
fn apply_checkout(
&mut self,
path: String,
@@ -533,9 +524,9 @@ impl NewPullRequestView {
return;
}
// Defaults: the announced HEAD branch when the checkout has it
// (falling back to `main`, then the first branch); the checkout's
// current branch for the compare side.
// Defaults, the announced HEAD branch when the checkout has it.
// Falling back to `main`, then the first branch.
// The checkout's current branch is the compare side default.
let announced = self.store.read(cx).head.clone();
let base = announced
.as_ref()
@@ -551,8 +542,8 @@ impl NewPullRequestView {
self.error = None;
self.branches = branches.into_iter().map(SharedString::from).collect();
// Learning: remember this folder as a checkout of the target
// repository, so the next panel pre-fills it.
// Remember this folder as a checkout of the target repository.
// The next panel pre-fills it.
let addr = self.store.read(cx).addr().clone();
CheckoutsStore::global(cx).update(cx, |store, cx| {
store.record(PathBuf::from(&path), addr, cx);
@@ -575,16 +566,16 @@ impl NewPullRequestView {
self.reload_compare(window, cx);
}
/// The base repository of the panel: its address and announced EUC,
/// used to find fork candidates.
/// The base repository of the panel, its address and announced EUC.
/// Used to find fork candidates.
fn base_repo(&self, cx: &App) -> (RepoAddr, Option<String>) {
let store = self.store.read(cx);
let euc = store.announcement.as_ref().and_then(|a| a.euc.clone());
(store.addr().clone(), euc)
}
/// Announced forks of the target repository the compare can be built
/// from (own forks first), re-read whenever the picker opens.
/// Announced forks of the target repository a compare can use, own first.
/// Re-read whenever the picker opens.
fn fork_candidates(&self, cx: &App) -> Vec<Announcement> {
let (base, euc) = self.base_repo(cx);
let user = Backend::global(cx).read(cx).current_user();
@@ -595,11 +586,12 @@ impl NewPullRequestView {
.collect()
}
/// Compare against an announced fork: ensure the target's GitCache
/// mirror, import the fork's heads under `refs/fork/…`, then fill the
/// selectors (base from `refs/remotes/origin/*`, compare from the
/// import) and load the compare. Picking the fork already applied
/// refreshes it instead (re-import + reload), keeping the branch selection.
/// Compare against an announced fork.
/// The target's GitCache mirror is ensured, then the fork's heads land under `refs/fork/…`.
/// The base selector lists `refs/remotes/origin/*`, the compare the import.
/// Then the compare loads.
/// Picking the fork already applied refreshes it, re-import and reload.
/// The branch selection is kept.
fn choose_fork(
&mut self,
announcement: Announcement,
@@ -625,13 +617,13 @@ impl NewPullRequestView {
.map(|a| a.clone.iter().map(ToString::to_string).collect())
.unwrap_or_default();
// The default compare branch of the fork, if the refreshed fork is
// the one applied and its branch still exists.
// Keep the current compare and base when the fork is already applied.
// apply_fork drops them when the branch no longer exists.
let keep_compare = refresh.then(|| self.compare.clone());
let keep_base = refresh.then(|| self.base.clone());
// The fork applied when the fetch started; if the user switches the
// source mid-flight, the result must not clobber the newer state.
// The fork applied when the fetch started.
// A source switch mid-flight must not let the stale result clobber the newer state.
let expected_fork = self.fork.as_ref().map(|fork| fork.announcement.addr());
self.loading = true;
@@ -639,9 +631,9 @@ impl NewPullRequestView {
cx.notify();
let task = cx.spawn_in(window, async move |this, cx| {
// The fork and the base must share history for a merge-base to exist,
// so the target's mirror is the object store both sides land in.
// `ensure_clone` fetches `origin` when the mirror exists already.
// 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.
let result = cx
.background_spawn({
let cache = cache.clone();
@@ -651,13 +643,13 @@ impl NewPullRequestView {
let clone_urls = clone_urls.clone();
let mirror_path = mirror_path.clone();
async move {
// The fork and the base must share history for a
// merge-base to exist, so the target's mirror is the
// object store both sides land in. `ensure_clone`
// fetches `origin` when the mirror exists already.
// 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, then import this fork's heads under its namespace.
// Prune stale imports of any fork.
// Then import this fork's heads under its namespace.
delete_refs_with_prefix(&mirror_path, "refs/fork")?;
fetch_repo_refs(
@@ -666,7 +658,7 @@ impl NewPullRequestView {
&format!("+refs/heads/*:refs/fork/{namespace}/*"),
)?;
// Both branch lists are short names, kept sorted like the checkout's.
// Both branch lists are short names, sorted like the checkout's.
let strip = |refs: Vec<String>, prefix: &str| {
let mut names: Vec<String> = refs
.into_iter()
@@ -696,7 +688,8 @@ impl NewPullRequestView {
.await;
this.update_in(cx, |this, window, cx| {
// A source switch mid-flight (e.g. the user picked a folder while the fork was fetching) discards the stale result.
// A source switch mid-flight discards the stale result.
// E.g. the user picked a folder while the fork was fetching.
let applied = this.fork.as_ref().map(|fork| fork.announcement.addr());
if applied != expected_fork {
this.loading = false;
@@ -721,7 +714,7 @@ impl NewPullRequestView {
self.tasks.push(task);
}
/// Apply an imported fork: fill the selectors and load the compare.
/// Apply an imported fork, filling the selectors and loading the compare.
#[allow(clippy::too_many_arguments)]
fn apply_fork(
&mut self,
@@ -738,8 +731,8 @@ impl NewPullRequestView {
let (base_branches, compare_branches) = match result {
Ok(branches) => branches,
Err(error) => {
// Keep the previous source (if any); the error is shown
// inline next to the compare bar.
// Keep the previous source, if any.
// The error shows inline next to the compare bar.
self.error = Some(format!("Could not compare against the fork: {error}").into());
cx.notify();
return;
@@ -764,11 +757,10 @@ impl NewPullRequestView {
.map(SharedString::from)
.collect();
// Defaults: the announced HEAD branch when the mirror has it
// (falling back to `main`, then the first branch); the fork's
// `main` for the compare side (falling back to the first branch).
// A refresh keeps the previous selection when the branch still
// exists.
// Base defaults to the announced HEAD branch when the mirror has it.
// Otherwise `main`, then the first branch.
// The fork's `main` is the compare default, else the first branch.
// A refresh keeps the previous selection when the branch still exists.
let announced = self.store.read(cx).head.clone();
let contains =
|name: &str, list: &[SharedString]| list.iter().any(|branch| branch.as_ref() == name);
@@ -817,18 +809,17 @@ impl NewPullRequestView {
self.reload_compare(window, cx);
}
/// (Re)compute `merge_base..compare` of the selected branches on a
/// background task: the merge base, the commit list and the diff. Runs
/// against the work path (the checkout, or the mirror in fork mode)
/// using the full refs of both branches, so base `main` and fork `main`
/// stay distinct.
/// Recompute `merge_base..compare` of the selected branches on a background task.
/// Computes the merge base, the commit list and the diff.
/// Runs against the work path, the checkout or the mirror in fork mode.
/// Full refs keep base `main` and fork `main` distinct.
fn reload_compare(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(repo_path) = self.work_path() else {
return;
};
let base = self.base_ref();
let compare = self.compare_ref();
// Short names for the error copy; the full refs go to git.
// Short names for the error copy, the full refs go to git.
let base_name = self.base.to_string();
let compare_name = self.compare.to_string();
@@ -879,8 +870,8 @@ impl NewPullRequestView {
.await;
this.update_in(cx, |this, _window, cx| {
// A stale result (the branches changed mid-flight) must not
// clobber a newer compare; the newer task clears the flag.
// A stale result, branches changed mid-flight, must not clobber a newer compare.
// The newer task clears the flag.
if generation != this.compare_generation {
return;
}
@@ -908,9 +899,10 @@ impl NewPullRequestView {
self.tasks.push(task);
}
/// Publish the pull request: generate the patch series from the checkout
/// on a background task, hand it to the store, and close the panel once
/// the publish is underway (errors surface in the pull request list).
/// Publish the pull request.
/// Generate the patch series on a background task and hand it to the store.
/// Close the panel once the publish is underway.
/// Errors surface in the pull request list.
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.submitting || self.loading {
return;
@@ -930,8 +922,8 @@ impl NewPullRequestView {
// The published `branch-name` is the compare branch's short name.
let branch_name = self.compare.to_string();
// The patch is generated from the compare ref: a plain branch name
// in checkout mode, the imported `refs/fork/…` ref in fork mode.
// The patch comes from the compare ref.
// Plain branch name in checkout mode, imported `refs/fork/…` ref in fork mode.
let compare_ref = self.compare_ref();
let store = self.store.clone();
let dock_area = self.dock_area.clone();
@@ -942,7 +934,8 @@ impl NewPullRequestView {
cx.notify();
let task = cx.spawn_in(window, async move |this, cx| {
// Regenerate the series at submit time so the published patch covers the current tip of the compare branch.
// Regenerate the series at submit time.
// The published patch covers the current tip of the compare branch.
let patch = cx
.background_spawn({
let repo_path = repo_path.clone();
@@ -1009,7 +1002,7 @@ impl NewPullRequestView {
self.tasks.push(task);
}
/// Open the diff of `commit_id` (from the Commits tab) in a new panel.
/// 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;
@@ -1033,8 +1026,8 @@ impl NewPullRequestView {
});
}
/// The compare bar: base/compare selectors, the source picker (local
/// checkout / announced fork) and the Create button.
/// The compare bar, base and compare selectors.
/// Plus the source picker, local checkout or announced fork, and the Create button.
fn render_compare_bar(&self, cx: &mut Context<Self>) -> AnyElement {
let has_source = self.has_source();
let can_submit = has_source
@@ -1047,8 +1040,8 @@ impl NewPullRequestView {
.is_some_and(|commits| !commits.is_empty())
&& !self.subject.read(cx).value().is_empty();
// Source-picker data, snapshotted when the menu is built
// (each open rebuilds the items from the live announcements).
// Source-picker data snapshotted when the menu is built.
// Each open rebuilds the items from the live announcements.
let source_menu = self.source_menu(cx);
let source_label = self.source_trigger();
let source_tooltip = match &self.fork {
@@ -1169,7 +1162,7 @@ impl NewPullRequestView {
.into_any_element()
}
/// The source picker's trigger: truncated label of the applied source.
/// 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)),
@@ -1180,18 +1173,18 @@ impl NewPullRequestView {
}
}
/// Build the compare-source menu: switching back to the local checkout,
/// then the announced forks of the target repository (own forks first).
/// Picking the fork already applied re-fetches it. Rebuilt every time
/// the menu opens, so the candidates are always current.
/// Build the compare-source menu.
/// The local checkout entries first, then the announced forks, own forks first.
/// Picking the fork already applied re-fetches it.
/// Rebuilt every time the menu opens, so the candidates stay current.
fn source_menu(
&self,
cx: &Context<Self>,
) -> impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static {
let view = cx.entity().downgrade();
// Associated local checkouts of the target repository, freshest
// first; the applied one is checked. The picker prompt stays
// available underneath for arbitrary folders.
// Associated local checkouts of the target repository, freshest first.
// The applied one is checked.
// The picker prompt stays available underneath for arbitrary folders.
let addr = self.store.read(cx).addr().clone();
let associated = CheckoutsStore::global(cx).read(cx).associations_of(&addr);
let active_path = (self.fork.is_none())
@@ -1345,8 +1338,8 @@ impl NewPullRequestView {
}
}
/// The Commits tab: `merge_base..compare` in a virtual list; clicking a
/// row opens the commit's diff in a new panel.
/// The Commits tab, `merge_base..compare` in a virtual list.
/// Clicking a row opens the commit's diff in a new panel.
fn render_commits_tab(&self, cx: &mut Context<Self>) -> AnyElement {
let Some(commits) = self.commits.as_ref() else {
return placeholder("No commits", cx);
@@ -1423,8 +1416,9 @@ fn count_badge(count: usize, cx: &App) -> impl IntoElement {
.child(SharedString::from(count.to_string()))
}
/// The trigger of a branch selector: icon + current selection (or
/// placeholder) + caret. `Combobox` replaces its default trigger entirely.
/// The trigger of a branch selector.
/// Shows the icon, the current selection or placeholder, and the caret.
/// `Combobox` replaces its default trigger entirely.
fn render_ref_trigger(
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
icon: CustomIconName,
@@ -1458,7 +1452,7 @@ fn render_ref_trigger(
.into_any_element()
}
/// Open the "new pull request" panel in the center dock.
/// Open the new pull request panel in the center dock.
pub(super) fn open_new_pull_panel(
dock_area: WeakEntity<DockArea>,
store: Entity<RepoStore>,
@@ -1570,8 +1564,8 @@ mod tests {
PublicKey::from_hex(OWNER_KEYS[0]).expect("pubkey"),
"upstream",
);
// Newest first, as RepoListStore keeps them: an unrelated repo, the
// user's own fork (shared EUC), someone else's fork (u tag).
// Newest first, as RepoListStore keeps them.
// Unrelated repo, the user's fork with the shared EUC, another fork with a `u` tag.
let all = vec![
announcements(
2,
@@ -41,60 +41,61 @@ use super::helpers::{
/// Width of the changed-files column.
const TREE_WIDTH: f32 = 260.;
/// Height of one commit row in the commits tab's virtual list: a single
/// text line plus the 1px bottom border.
/// Height of one commit row in the commits tab's virtual list.
/// A single text line plus the 1px bottom border.
const PR_COMMIT_ROW_HEIGHT: f32 = 37.;
/// Detail panel of a single pull request.
pub struct PullRequestDetailView {
focus_handle: FocusHandle,
/// Dock area new panels (commit diffs) are added to.
/// 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 "leave a comment" textarea.
/// 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; `None` while the diff is
/// parsed from the nostr patch set (no commit diff viewer then).
/// Local clone the PR's git changes come from.
/// `None` when the diff is parsed from the nostr patch set.
/// No commit diff viewer in that case.
worktree: Option<PathBuf>,
/// Root PR's content, shown as plain text.
description: SharedString,
/// Tip commit of the PR: the latest update's `c` tag, else the root's.
/// Tip commit of the PR, the latest update's `c` tag or the root's.
current_commit: Option<SharedString>,
/// Commits of the patch series, in patch order (oldest first).
/// Commits of the patch series, in patch order, oldest first.
commits: Vec<FileCommit>,
/// Parsed file changes of the patch; `None` while loading or on failure.
/// Parsed file changes of the patch, `None` while loading or on failure.
diff: Option<CommitDiff>,
/// The patch is being parsed on a background task.
loading: bool,
error: Option<SharedString>,
/// Active header tab: 0 = Discussion, 1 = Files, 2 = Commits.
/// Active header tab, 0 = Discussion, 1 = Files, 2 = Commits.
active_tab: usize,
/// 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 + lines).
/// 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,
/// Per-row heights of the commits tab's virtual list, built when the
/// patch series is loaded.
/// 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,
/// Comment bodies as shared strings, keyed by comment event ID, so
/// re-renders don't clone full contents again (events are immutable,
/// so the cache never needs invalidation).
/// Comment bodies as shared strings, keyed by comment event ID.
/// Re-renders don't clone full contents again.
/// Events are immutable, so the cache never needs invalidation.
contents: HashMap<EventId, SharedString>,
/// In-flight tasks; finished tasks are pruned on every push, so the vec
/// stays bounded by the number of concurrent loads.
/// In-flight tasks, finished tasks are pruned on every push.
/// The vec stays bounded by the number of concurrent loads.
tasks: Vec<Task<Result<(), anyhow::Error>>>,
/// Subscriptions keeping the view live as the store refreshes.
_subscriptions: Vec<Subscription>,
@@ -125,7 +126,7 @@ impl PullRequestDetailView {
})
.unwrap_or_default();
// Re-render when the store refreshes (new comments, status changes).
// Re-render when the store refreshes, new comments or status changes.
let subscriptions = vec![cx.observe(&store, |_this, _store, cx| cx.notify())];
// Defer loading until the window is ready, like the commit diff view.
@@ -161,12 +162,12 @@ impl PullRequestDetailView {
}
}
/// Snapshot the PR events from the store, then compute the file changes
/// and commit list on a background task and populate the tree.
///
/// The changes come from the PR's patch set (NIP-34 `e`-linked patch
/// events) when present; otherwise from the git repository (`c`,
/// `clone` and `merge-base` tags), diffing the `merge-base..tip` range.
/// Snapshot the PR events from the store.
/// File changes and the commit list are computed on a background task.
/// The tree is populated from the result.
/// The changes come from the PR's patch set, NIP-34 `e`-linked patch events, when present.
/// Otherwise from the git repository, `c`, `clone` and `merge-base` tags.
/// Diffing the `merge-base..tip` range.
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.loading = true;
self.error = None;
@@ -226,9 +227,8 @@ impl PullRequestDetailView {
})
.await;
// PRs without patch events (e.g. published by ngit) carry their
// changes in the git repository: fetch the clone and diff the
// `merge-base..tip` range.
// PRs without patch events, e.g. published by ngit, carry their changes in git.
// Fetch the clone and diff the `merge-base..tip` range.
let use_nostr = match &nostr_diff {
Ok(diff) => has_patch_link || !diff.files.is_empty(),
Err(_) => true,
@@ -252,8 +252,8 @@ impl PullRequestDetailView {
tip.ok_or_else(|| anyhow::anyhow!("pull request has no tip commit"))?;
let base = match base {
Some(base) => base,
// No `merge-base` tag: use the merge base of the
// tip with the default branch.
// No `merge-base` tag.
// Use the merge base of the tip and the default branch.
None => {
let head = repo
.head_id()
@@ -322,15 +322,14 @@ impl PullRequestDetailView {
self.tasks.push(task);
}
/// Show the diff of the file at `path` (selected in the tree).
/// 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 the file at `path` and scroll back
/// to the top.
/// Rebuild the virtual list state for the file at `path` and scroll back to the top.
fn set_diff_rows(&mut self, path: &str) {
let Some(diff) = self.diff.as_ref() else {
return;
@@ -370,7 +369,7 @@ impl PullRequestDetailView {
});
}
/// One row of the changed-files tree: icon + name, indented by depth.
/// One row of the changed-files tree, icon and name, indented by depth.
fn render_tree_item(
ix: usize,
entry: &TreeEntry,
@@ -387,7 +386,7 @@ impl PullRequestDetailView {
})
}
/// Left column: the changed-files tree.
/// 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();
@@ -417,7 +416,7 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Right column: header of the selected file plus its diff.
/// Right column, header of the selected file plus its diff.
fn render_detail_column(&self, cx: &mut Context<Self>) -> AnyElement {
if self.loading {
return v_flex()
@@ -446,8 +445,9 @@ impl PullRequestDetailView {
self.render_file_diff(file, cx.entity(), cx)
}
/// The diff of one file: a header with status and stats, then the hunks
/// in a virtual list (a large diff is never materialized per frame).
/// The diff of one file, with a header showing status and stats.
/// The hunks render in a virtual list.
/// A large diff is never materialized per frame.
fn render_file_diff(&self, file: &FileDiff, view: Entity<Self>, cx: &App) -> AnyElement {
let status_label = match file.status {
signed_git::DiffStatus::Added => "A",
@@ -564,7 +564,7 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Underline tab bar: Discussion, Files and Commits.
/// Underline tab bar with the Discussion, Files and Commits tabs.
fn render_tabs(&self, cx: &mut Context<Self>) -> AnyElement {
let active = self.active_tab;
let files_count = self.diff.as_ref().map(|diff| diff.files.len());
@@ -610,8 +610,8 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Discussion tab: author, description and comments like the issue
/// panel, with the comment form at the end and a sidebar on the right.
/// Discussion tab, author, description and comments like the issue panel.
/// The comment form sits at the end, a sidebar on the right.
fn render_discussion(&mut self, cx: &mut Context<Self>) -> AnyElement {
if self.loading {
return v_flex()
@@ -694,7 +694,7 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Right sidebar: participants and labels, like the issue panel.
/// Right sidebar with participants and labels, like the issue panel.
fn render_sidebar(&self, cx: &mut Context<Self>) -> AnyElement {
let profile_store = ProfileStore::global(cx);
let store = self.store.read(cx);
@@ -708,7 +708,7 @@ impl PullRequestDetailView {
return div().into_any_element();
};
// Participants: the PR author plus everyone who commented.
// Participants, the PR author plus everyone who commented.
let mut participants: Vec<PublicKey> = vec![root.pubkey];
participants.extend(store.comments_of(&root.id).map(|comment| comment.pubkey));
participants.sort_by_key(PublicKey::to_hex);
@@ -776,8 +776,8 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Files tab: the changed-files tree on the left, the diff of the
/// selected file on the right.
/// Files tab, the changed-files tree on the left.
/// The diff of the selected file on the right.
fn render_files_tab(&self, cx: &mut Context<Self>) -> AnyElement {
h_flex()
.flex_1()
@@ -789,8 +789,8 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Full-height Commits tab: every commit of the patch series, or a
/// status message while loading / when there are none.
/// 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()
@@ -838,8 +838,8 @@ 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.
/// 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,
@@ -882,8 +882,8 @@ impl PullRequestDetailView {
.child(SharedString::from(meta)),
)
})
// Commits parsed from the nostr patch set may not exist in any
// local clone; only git-backed PRs open a diff viewer.
// Commits parsed from the nostr patch set may not exist in any local clone.
// Only git-backed PRs open a diff viewer.
.when_some(self.worktree.clone(), |this, worktree| {
this.on_click(cx.listener(move |this, _event, window, cx| {
this.open_commit_diff(worktree.clone(), &id, window, cx);
@@ -892,8 +892,8 @@ impl PullRequestDetailView {
.into_any_element()
}
/// One comment card, same design as the issue panel: avatar, author,
/// "commented" and age on the header row, content below.
/// One comment card, same design as the issue panel.
/// Header row holds the avatar, author, commented and age, content below.
fn render_comments(&mut self, id: &EventId, cx: &mut Context<Self>) -> AnyElement {
let store = self.store.read(cx);
let comments: Vec<&Event> = store.comments_of(id).collect();
@@ -907,8 +907,7 @@ impl PullRequestDetailView {
let author = profile.name();
let picture = profile.picture();
let age = relative_time(comment.created_at);
// Comment bodies are cloned into shared strings once per
// comment, not on every render.
// Comment bodies become shared strings once per comment, not per render.
let content = self
.contents
.entry(comment.id)
@@ -1002,7 +1001,7 @@ impl PullRequestDetailView {
.into_any_element()
}
/// Always-visible header: status badge and title, like the issue panel.
/// 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) = {
@@ -1022,7 +1021,7 @@ impl PullRequestDetailView {
)
};
// Only the PR author may publish revisions (kind 1619, NIP-34).
// Only the PR author may publish revisions, NIP-34 kind 1619.
let backend = Backend::global(cx);
let can_update = backend.read(cx).current_user() == Some(author);
@@ -1104,8 +1103,9 @@ impl PullRequestDetailView {
}
}
/// Open the "update pull request" dialog: a patch input that submits a new
/// revision through [`RepoStore::update_pull_request`] when confirmed.
/// Open the update pull request dialog.
/// The patch input supplies the new revision.
/// Confirming calls [`RepoStore::update_pull_request`].
fn open_update_pull_request_dialog(
store: Entity<RepoStore>,
root: Event,
@@ -1115,8 +1115,8 @@ fn open_update_pull_request_dialog(
let patch = cx.new(|cx| {
TextareaState::new(window, cx).placeholder("Paste the updated `git format-patch` output...")
});
// Both the dialog body and the submit button capture the root event;
// share it instead of cloning into each closure.
// Both the dialog body and submit button capture the root event.
// Share it instead of cloning into each closure.
let root = Rc::new(root);
window.open_dialog(cx, move |dialog, _window, _cx| {
@@ -1176,7 +1176,7 @@ fn sidebar_title(text: &str, cx: &App) -> AnyElement {
.into_any_element()
}
/// The `c` tag of a PR event (tip of the proposed branch), as hex.
/// The `c` tag of a PR event, the tip of the proposed branch, as hex.
fn current_commit_of(event: &Event) -> Option<String> {
event
.tags
@@ -1187,8 +1187,8 @@ fn current_commit_of(event: &Event) -> Option<String> {
})
}
/// The `merge-base` tag of a PR event (most recent common ancestor with the
/// target branch), as hex.
/// The `merge-base` tag of a PR event, as hex.
/// The most recent common ancestor with the target branch.
fn merge_base_of(event: &Event) -> Option<String> {
event
.tags
@@ -1199,8 +1199,8 @@ fn merge_base_of(event: &Event) -> Option<String> {
})
}
/// The `clone` tag of a PR event (URLs where the proposed branch can be
/// fetched), or `None` if the PR has none.
/// The `clone` tag of a PR event.
/// URLs where the proposed branch can be fetched, or `None` if the PR has none.
fn clone_urls_of(event: &Event) -> Option<Vec<String>> {
event
.tags
@@ -1222,9 +1222,10 @@ fn branch_name_of(event: &Event) -> Option<String> {
})
}
/// The latest PR update (kind 1619) revising `root`, found via its NIP-22
/// `E` tag pointing at the root PR event. Only updates by the PR author
/// count: the tip of a PR is only mutable by its author (NIP-34).
/// The latest PR update, kind 1619, revising `root`.
/// Found via its NIP-22 `E` tag pointing at the root PR event.
/// Only updates by the PR author count.
/// The tip of a PR is only mutable by its author, NIP-34.
fn latest_update<'a>(events: impl Iterator<Item = &'a Event>, root: &Event) -> Option<&'a Event> {
let root_hex = root.id.to_hex();
events
@@ -1238,8 +1239,8 @@ fn latest_update<'a>(events: impl Iterator<Item = &'a Event>, root: &Event) -> O
.max_by_key(|e| e.created_at)
}
/// One-line commit metadata for the commits list: author and relative time,
/// whichever is available.
/// One-line commit metadata for the commits list.
/// Author and relative time, whichever is available.
fn commit_meta(commit: &FileCommit) -> String {
let author = commit.author.trim();
let time = commit.time > 0;
@@ -1357,8 +1358,7 @@ mod tests {
created_at,
)
};
// An update revising a different PR must be ignored even though it
// is newer.
// An update revising a different PR must be ignored even though it is newer.
let unrelated = signed(
Kind::GitPullRequestUpdate,
vec![Tag::parse(["E", OTHER_ROOT_HEX]).expect("valid tag")],
@@ -1386,8 +1386,8 @@ mod tests {
.finalize(&other)
.expect("signed event");
// The tip of a PR is only mutable by its author: a newer update
// from anyone else must not win.
// The tip of a PR is only mutable by its author.
// A newer update from anyone else must not win.
assert!(latest_update([&stranger, &root].into_iter(), &root).is_none());
}
@@ -25,12 +25,11 @@ use super::new_pull_request::open_new_pull_panel;
use super::pull_request_detail::PullRequestDetailView;
use super::send_patch::open_send_patch_panel;
/// Height of one pull request row in the virtual list; same layout as an
/// issue row.
/// Height of one pull request row in the virtual list.
/// Same layout as an issue row.
const PR_ROW_HEIGHT: f32 = 73.;
/// Status filter of the pull request list, chosen via the header's filter
/// buttons.
/// 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.
@@ -70,17 +69,18 @@ pub struct PullRequestsView {
filter: PullRequestFilter,
/// Per-row heights of the virtual list.
item_sizes: Rc<Vec<Size<Pixels>>>,
/// Number of rows [`Self::item_sizes`] was built for (the filtered
/// pull request count); rebuilt on change.
/// The filtered pull request count [`Self::item_sizes`] was built for.
/// Rebuilt on change.
pr_len: usize,
/// Indices into the store's `pull_requests` matching [`Self::filter`]
/// (root PR events only; updates are revisions of the root); the
/// virtual list renders this slice. Rebuilt only when the store
/// version or the filter changes, keyed by [`Self::cache_key`].
/// Indices into the store's `pull_requests` matching [`Self::filter`].
/// Root PR events only, updates are revisions of the root.
/// The virtual list renders this slice.
/// Rebuilt only when the store version or the filter changes.
/// Keyed by [`Self::cache_key`].
visible_prs: Vec<usize>,
/// Header counts `(total, open, closed, draft, merged)` of the root
/// pull requests only (revisions are not separate PRs), rebuilt with
/// [`Self::visible_prs`].
/// Header counts `(total, open, closed, draft, merged)`.
/// Root pull requests only, revisions are not separate PRs.
/// Rebuilt with [`Self::visible_prs`].
counts: (usize, usize, usize, usize, usize),
/// Store version and filter the cached rows/counts were built from.
cache_key: Option<(u64, PullRequestFilter)>,
@@ -112,7 +112,7 @@ impl PullRequestsView {
}
}
/// Open the detail panel of `pr_id` at the bottom of the dock area.
/// Open the detail panel of `pr_id` in the dock area.
fn open_pull_request_detail(
&mut self,
pr_id: EventId,
@@ -138,8 +138,8 @@ impl PullRequestsView {
});
}
/// Render one row of the pull request list; `ix` is the row index and
/// `pr_ix` the index of the pull request in the store's `pull_requests`.
/// 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;
@@ -204,8 +204,8 @@ impl PullRequestsView {
}
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
// Counts of the last list rebuild (`render` rebuilds first when the
// store version or filter changed, so this is never stale).
// Counts of the last list rebuild.
// `render` rebuilds first when the store version or filter changed, so never stale.
let (total, open, closed, draft, merged) = self.counts;
h_flex()
@@ -340,8 +340,8 @@ impl Render for PullRequestsView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let filter = self.filter;
// Rebuild the filtered rows and header counts only when the store
// refreshed or the filter changed; other renders reuse the cache.
// Rows and counts are rebuilt only when the store refreshed or filter changed.
// Other renders reuse the cache.
let version = self.store.read(cx).version();
if self.cache_key != Some((version, filter)) {
let store = self.store.read(cx);
@@ -351,10 +351,10 @@ impl Render for PullRequestsView {
.iter()
.enumerate()
.filter_map(|(ix, pr)| {
// Kind-30620 patches are revisions of a root PR (NIP-34),
// not separate pull requests: count only root events, or
// the header counts inflate with every revision (which
// also default to `Open` in `status_of`).
// Kind-30620 patches are revisions of a root PR, NIP-34.
// They are not separate pull requests.
// Count root events only, or the counts inflate with every revision.
// Revisions also default to `Open` in `status_of`.
if pr.kind != Kind::GitPullRequest {
return None;
}
@@ -375,8 +375,8 @@ impl Render for PullRequestsView {
let count = self.visible_prs.len();
// The virtual list's item count comes from `item_sizes`; rebuild it
// whenever the filtered pull request count changes.
// The virtual list's item count comes from `item_sizes`.
// Rebuild it whenever the filtered pull request count changes.
if count != self.pr_len {
self.pr_len = count;
self.item_sizes = Rc::new(vec![size(px(0.), px(PR_ROW_HEIGHT)); count]);
@@ -386,8 +386,8 @@ 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 (e.g. creating
// or updating a PR), shown as dismissible banners above the list.
// 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())
@@ -19,15 +19,15 @@ pub struct SendPatchView {
store: Entity<RepoStore>,
/// Display name of the repository, for the panel title.
repo_name: SharedString,
/// Title input (required).
/// Title input, required.
subject: Entity<InputState>,
/// Description input (optional).
/// Description input, optional.
description: Entity<TextareaState>,
/// The pasted `git format-patch` output (required).
/// The pasted `git format-patch` output, required.
patch: Entity<TextareaState>,
/// A submit is in flight.
submitting: bool,
/// Error of the last submit attempt (keeps the panel open).
/// Error of the last submit attempt, it keeps the panel open.
error: Option<SharedString>,
_subscriptions: Vec<Subscription>,
}
@@ -71,10 +71,11 @@ impl SendPatchView {
}
}
/// Publish the pull request from the pasted patch. The store validates
/// synchronously (patch shape, per-part size, sign-in); on failure the
/// panel stays open with the error inline, on success it closes — async
/// publish failures surface in the pull request list's banner.
/// Publish the pull request from the pasted patch.
/// The store validates synchronously, patch shape, per-part size and sign-in.
/// On failure the panel stays open with the error inline.
/// On success it closes.
/// Async publish failures surface in the pull request list's banner.
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.submitting {
return;
@@ -93,8 +94,8 @@ impl SendPatchView {
self.error = None;
cx.notify();
// Errors the store detects before publishing are returned
// synchronously through `last_error`.
// Errors the store detects before publishing.
// Returned synchronously through `last_error`.
let sync_error = store.update(cx, |store, cx| {
store.open_pull_request(
(!subject.is_empty()).then_some(subject),
+20 -21
View File
@@ -24,13 +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.
/// 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, newest first (the store's default order).
/// Every repository in the store's default order, newest first.
All,
#[default]
/// Repositories ranked by total issues + pull requests + commits.
@@ -40,15 +40,15 @@ enum RepoFilter {
}
impl RepoFilter {
/// Indices into the store's `announcements` included by this filter, in
/// display order, narrowed to repositories whose name (or id) contains
/// `query`; an empty query matches everything.
/// Indices into the store's `announcements` this filter includes, in display order.
/// Narrowed to repositories whose name or id contains `query`.
/// An empty query matches everything.
fn visible(self, store: &RepoListStore, query: &str) -> Vec<usize> {
let announcements = &store.announcements;
let mut indices: Vec<usize> = (0..announcements.len()).collect();
// Narrow by the search query first, so "Recent" limits the matches
// and "Popular" ranks them.
// Narrow by the search query first.
// Recent then limits the matches and Popular ranks them.
let query = query.trim().to_lowercase();
if !query.is_empty() {
indices.retain(|&ix| {
@@ -93,10 +93,10 @@ pub struct RepoListView {
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).
/// Number of rows [`Self::item_sizes`] was built for, the filtered repo count.
repo_len: usize,
/// Indices into the store's `announcements` matching [`Self::filter`],
/// in display order; the virtual list renders this slice.
/// Indices matching [`Self::filter`] into the store's `announcements`.
/// The virtual list renders this slice in display order.
visible: Vec<usize>,
/// Search box filtering repositories by name.
search: Entity<InputState>,
@@ -121,8 +121,8 @@ impl RepoListView {
}
});
// Keep the visible slice and row sizes in sync with the store,
// so newly announced repositories appear without waiting for a click.
// Keep the visible slice and row sizes in sync with the store.
// Newly announced repositories appear without waiting for a click.
let subscription = cx.observe(&store, |this, _store, cx| {
this.rebuild_rows(cx);
});
@@ -141,16 +141,16 @@ impl RepoListView {
_subscription: subscription,
};
// Seed the rows right away; the store may already hold announcements
// (it loaded before the panel opened), and the first render must not
// depend on a later store update.
// Seed the rows right away.
// The store may already hold announcements from before the panel opened.
// The first render must not depend on a later store update.
this.rebuild_rows(cx);
this
}
/// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the current
/// store contents, [`Self::filter`] and the search query.
/// 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();
@@ -201,8 +201,8 @@ impl RepoListView {
.map(|label| SharedString::from(format!("Updated {label}")))
.unwrap_or_default();
// Fork badge: the upstream's display name when its announcement is
// known locally, otherwise its repository id from the `u` tag.
// The fork badge shows the upstream name when its announcement is known locally.
// Otherwise it shows the repository id from the `u` tag.
let fork_label: Option<SharedString> =
announcement.upstream.as_ref().and_then(|upstream| {
let addr = upstream.addr.as_ref()?;
@@ -347,8 +347,7 @@ impl RepoListView {
.into_any_element()
}
/// One segmented filter button of the header, styled like the issues
/// list's status filter buttons.
/// One segmented header filter button, like the issues list's status filter buttons.
fn filter_button(
&self,
filter: RepoFilter,
@@ -24,10 +24,9 @@ pub struct CreateRepoState {
}
/// Open the Create Repository dialog.
///
/// The dialog loads the user's default grasp servers (kind `10317` grasp
/// list) and falls back to the shared defaults when none are set. On
/// success the dialog closes and the new repository opens in the dock.
/// Loads the user's default grasp servers, a kind `10317` grasp list.
/// Falls back to the shared defaults when none are set.
/// On success the dialog closes and the new repository opens in the dock.
pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
let settings = SettingsStore::global(cx);
let default_folder = settings
@@ -160,10 +159,9 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
});
}
/// Prompt the user to pick the folder the repository will be stored in, using
/// the platform's native folder picker, and show the result in the disabled
/// folder input. The picked folder is remembered in the settings so it
/// becomes the default next time.
/// Pick the repository's storage folder with the platform's native folder picker.
/// Show the result in the disabled folder input.
/// The settings remember the picked folder as the default next time.
fn choose_folder(folder_input: &Entity<InputState>, window: &mut Window, cx: &mut App) {
let handle = window.window_handle();
let folder_input = folder_input.clone();
@@ -198,8 +196,8 @@ 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.
/// Run the create-repository flow.
/// Opens the new working copy and the repository panel on success.
#[allow(clippy::too_many_arguments)]
fn create_repository(
name_input: Entity<InputState>,
@@ -247,8 +245,8 @@ fn create_repository(
Ok((announcement, local_path)) => {
cx.update_window(handle, |_, window, cx| {
window.close_dialog(cx);
// Remember the new working copy as a checkout of this
// repository, so the New PR panel pre-fills it.
// Record the new working copy as a checkout of this repository.
// The New PR panel then pre-fills it.
let checkouts = CheckoutsStore::global(cx);
checkouts.update(cx, |store, cx| {
store.record(local_path.clone(), announcement.addr(), cx);
@@ -9,24 +9,21 @@ use settings::{DEFAULT_GRASP_SERVERS, GraspServersSettings};
use signed_core::filters;
use signed_state::Backend;
/// State of the grasp-server section of a publish dialog, so async
/// results can be rendered.
/// 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 (kind `10317`) is being loaded.
/// The user's grasp list of kind `10317` is being loaded.
pub loading_servers: bool,
pub grasp_servers: Vec<RelayUrl>,
/// Whether the grasp server section is shown; defaults to shown.
/// Whether the grasp server section is shown. Defaults to shown.
pub servers_enabled: bool,
/// Error of the last grasp-server edit (e.g. an invalid relay URL).
/// Error of the last grasp-server edit, an invalid relay URL for example.
pub error: Option<SharedString>,
}
impl GraspServersState {
/// Defaults until the user's grasp list arrives; replaced by it when it lists any servers.
///
/// The servers come from the persisted settings, falling back to the
/// built-in defaults when the configured list is empty.
/// Defaults used until the user's grasp list loads, which replaces them when non-empty.
/// Persisted settings supply the defaults, an empty list falls back to the built-ins.
pub fn new_default(settings: &GraspServersSettings) -> Self {
let urls: Vec<String> = if settings.default_servers.is_empty() {
DEFAULT_GRASP_SERVERS
@@ -48,10 +45,9 @@ impl GraspServersState {
}
}
/// The "Grasp servers" form field shared by the publish dialogs: an
/// expandable toggle, the configured servers (each removable) and an
/// add-relay input, with a loading hint while the user's grasp list
/// (kind `10317`) is being fetched.
/// The Grasp servers form field shared by the publish dialogs.
/// An expandable toggle, the configured servers each removable, and an add-relay input.
/// Shows a loading hint while the user's kind `10317` grasp list is fetched.
pub fn grasp_servers_field(
state: &Entity<GraspServersState>,
relay_input: &Entity<InputState>,
@@ -143,7 +139,7 @@ pub fn grasp_servers_field(
}))
}
/// A grasp server row: the host as a tag plus a remove button.
/// One grasp server row, the host in a tag plus a remove button.
fn render_server_row(
ix: usize,
relay: &RelayUrl,
@@ -182,7 +178,7 @@ fn render_server_row(
)
}
/// The bare host of a grasp server (defaults are entered without a scheme).
/// The bare host of a grasp server, defaults are entered without a scheme.
fn display_server(relay: &RelayUrl) -> SharedString {
relay
.domain()
@@ -190,7 +186,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.
/// Parse the relay input, accepting a bare host, and append it to the list.
fn add_relay(
state: &Entity<GraspServersState>,
input: &Entity<InputState>,
@@ -226,8 +222,8 @@ fn add_relay(
}
}
/// Load the user's grasp list (kind `10317`) from the local database and
/// replace the defaults with it when it lists any servers.
/// Load the user's grasp list of kind `10317` from the local database.
/// It replaces the defaults when it lists any servers.
pub fn load_user_grasp_servers(
state: Entity<GraspServersState>,
window: &mut Window,
@@ -2,8 +2,7 @@ use gpui::{App, Window, px};
use gpui_component::WindowExt;
/// Open the Import Identity dialog.
///
/// Currently a placeholder — the dialog only shows a title for now.
/// Currently a placeholder, the dialog only shows a title.
pub fn open(window: &mut Window, cx: &mut App) {
window.open_dialog(cx, move |dialog, _window, _cx| {
dialog.title("Import identity").width(px(400.))
+36 -39
View File
@@ -32,25 +32,25 @@ mod settings_dialog;
use self::onboarding_dialog::OnboardingState;
/// Left-dock panel with navigation entries. Entries open content panels in
/// the dock area.
/// Left-dock panel with navigation entries.
/// Entries open content panels in the dock area.
pub struct SidebarPanel {
focus_handle: FocusHandle,
dock_area: WeakEntity<DockArea>,
explore: Option<WeakEntity<RepoListView>>,
logged_in: bool,
/// Repositories announced by the current user, listed under
/// "All Repositories". Recreated when the signer changes.
/// Repositories the current user announced, listed under the All Repositories heading.
/// Recreated when the signer changes.
my_repos: Option<Entity<RepoListStore>>,
/// Observes the current user's repo store so the list re-renders.
my_repos_subscription: Option<Subscription>,
/// Banner artwork shown behind the sign-in screen,
/// picked at random from the bundled `backgrounds/` assets.
/// Banner artwork behind the sign-in screen.
/// Picked at random from the bundled `backgrounds/` assets.
banner: SharedString,
/// Observes the local-repository scan so new discoveries re-render.
_local_repos_subscription: Subscription,
/// Observes the checkouts store, whose ready-to-push statuses feed the
/// badges on the user's repository rows.
/// Observes the checkouts store.
/// Its ready-to-push statuses feed the badges on the user's repo rows.
_checkouts_subscription: Subscription,
_subscription: Subscription,
}
@@ -107,8 +107,8 @@ impl SidebarPanel {
panel
}
/// (Re)create the store listing the current user's repositories,
/// and watch each of them for unpushed local work.
/// Recreate the store listing the current user's repositories.
/// Watch each repository for unpushed local work.
fn refresh_my_repos(&mut self, cx: &mut Context<Self>) {
self.my_repos_subscription = None;
@@ -119,9 +119,9 @@ impl SidebarPanel {
if let Some(store) = self.my_repos.as_ref() {
self.my_repos_subscription = Some(cx.observe(store, |_this, store, cx| {
cx.notify();
// These are the signed-in user's own repositories; request
// their ready-to-push statuses (deduplicated per repo) so
// the rows carry a badge while local work is unpushed.
// These are the signed-in user's own repositories.
// Request their ready-to-push statuses, deduplicated per repository.
// The rows carry a badge while local work is unpushed.
let addrs: Vec<_> = store
.read(cx)
.announcements
@@ -138,8 +138,8 @@ impl SidebarPanel {
}
}
/// Open the Explore (repository list) panel in the center of the dock
/// area. No-op if it's already open.
/// Open the Explore repository list panel in the dock area's center.
/// No-op if it is already open.
pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self
.explore
@@ -191,8 +191,8 @@ impl SidebarPanel {
open_repo_panel(&self.dock_area, announcement, window, &mut *cx);
}
/// Open a local repository's detail view in the dock's center; the
/// detail view offers to publish it to NIP-34.
/// 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 =
cx.new(|cx| RepoDetailView::new_local(self.dock_area.clone(), path, window, cx));
@@ -208,10 +208,10 @@ impl SidebarPanel {
});
}
/// The "All Repositories" section: header with the create button and
/// the current user's repositories below it, lazily rendered through a
/// [`uniform_list`], followed by the local git repositories discovered
/// by the startup scan.
/// The All Repositories section of the sidebar.
/// A header with the create button above the current user's repositories.
/// Rendered lazily through a [`uniform_list`].
/// Followed by local git repositories from the startup scan.
fn render_my_repos(&self, cx: &mut Context<Self>) -> impl IntoElement {
let store = self.my_repos.as_ref();
let local = LocalReposStore::global(cx);
@@ -265,11 +265,10 @@ impl SidebarPanel {
)
.when_some(store, |builder, store| {
let announcements = store.read(cx).announcements.clone();
// Local repositories that have already been published to
// NIP-34 are listed among the user's repositories above;
// hide them from the local section (matched by the
// identifier derived from the directory name, like the
// init dialog's default name).
// Local repositories already published to NIP-34 appear above.
// Hide them from the local section here.
// Matched by the identifier derived from the directory name.
// Same derivation as the init dialog's default name.
let announced_ids: HashSet<String> =
announcements.iter().map(|a| a.id.clone()).collect();
let local_repos: Vec<PathBuf> = local_repos
@@ -282,8 +281,8 @@ impl SidebarPanel {
})
.cloned()
.collect();
// One merged list: the user's NIP-34 repositories first,
// then the local repositories discovered by the scan.
// One merged list, the user's NIP-34 repositories first.
// Local repositories discovered by the scan follow.
let total = announcements.len() + local_repos.len();
if total == 0 {
@@ -326,8 +325,7 @@ impl SidebarPanel {
})
}
/// One row of the merged sidebar list: a NIP-34 repository or a local
/// repository.
/// One row of the merged sidebar list, a NIP-34 or a local repository.
fn render_repo_row_at(
&self,
announcements: &[Announcement],
@@ -358,8 +356,8 @@ impl SidebarPanel {
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
// A small badge with the unpushed commit count of the repository's
// local checkouts (ready to push to the grasp servers).
// Badge with the unpushed commit count of the repository's local checkouts.
// The commits are ready to push to the grasp servers.
let unpushed: usize = CheckoutsStore::global(cx)
.read(cx)
.push_statuses_of(&announcement.addr())
@@ -379,10 +377,9 @@ impl SidebarPanel {
)
}
/// One local repository row: a deterministic pixel avatar seeded from
/// the path, the directory name, and a warning suffix marking it as
/// not yet set up for NIP-34. Clicking it opens the repository's
/// detail view, which offers to initialize it.
/// One local repository row, a deterministic pixel avatar seeded from the path.
/// The directory name and a warning suffix, the repo is not yet set up for NIP-34.
/// Clicking opens the detail view, which offers to initialize it.
fn render_local_row(&self, path: &Path, cx: &mut Context<Self>) -> impl IntoElement {
let name = path
.file_name()
@@ -410,7 +407,7 @@ impl SidebarPanel {
import_dialog::open(window, cx);
}
/// Render the user avatar and name in the sidebar, wrapped in the window titlebar drag area.
/// Render the user avatar and name in the sidebar, inside the titlebar drag area.
fn render_user(
&self,
profile: &Profile,
@@ -440,8 +437,8 @@ impl SidebarPanel {
)
}
/// Sign-in placeholder shown while logged out: banner artwork behind a
/// scrim so the CTA buttons stay readable in both themes.
/// Sign-in placeholder shown while logged out.
/// Banner artwork behind a scrim keeps the CTA buttons readable in both themes.
fn render_sign_in(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
v_flex()
.size_full()
@@ -15,10 +15,8 @@ pub struct OnboardingState {
}
/// Open the Onboarding dialog for creating a new identity.
///
/// The caller is responsible for creating the input and state entities and
/// passing them in. This function only builds the dialog UI and wires up
/// the continue-button handler.
/// The caller creates the input and state entities and passes them in.
/// This function only builds the dialog UI and wires up the continue-button handler.
pub fn open(
name_input: Entity<InputState>,
pass_input: Entity<InputState>,
@@ -17,9 +17,8 @@ pub struct PassphraseState {
_enter_subscription: Option<Subscription>,
}
/// Open the dialog asking for the passphrase that protects the stored
/// NIP-49 encrypted identity (`ncryptsec1...`).
///
/// Open the dialog asking for the passphrase that protects the stored identity.
/// The identity is NIP-49 encrypted, for example `ncryptsec1...`.
/// Called when the backend emits [`signed_state::BackendEvent::PassphraseRequired`].
pub fn open(window: &mut Window, cx: &mut App) {
let pass_input = cx.new(|cx| {
@@ -98,8 +97,9 @@ pub fn open(window: &mut Window, cx: &mut App) {
});
}
/// Submit the passphrase to the backend. On success the dialog is closed;
/// on failure the error is rendered inline and the dialog stays open.
/// Submit the passphrase to the backend.
/// On success the dialog closes.
/// On failure the error is rendered inline and the dialog stays open.
fn unlock(
pass_input: &Entity<InputState>,
state: &Entity<PassphraseState>,
@@ -1,10 +1,3 @@
//! The Settings dialog, opened from the sidebar's Settings entry.
//!
//! A custom settings layout that divides related settings into sections
//! separated by simple horizontal lines — no `GroupBox` boxes and no settings
//! navigation sidebar. Every control edits the persisted [`SettingsStore`]
//! and applies the change to the live theme immediately.
use std::cell::Cell;
use std::path::PathBuf;
use std::rc::Rc;
@@ -55,8 +48,8 @@ fn theme_options(cx: &App) -> (Vec<SelectOption>, Vec<SelectOption>) {
(light, dark)
}
/// Stateful controls of the settings dialog, created once when it opens so
/// their values survive re-renders of the dialog content.
/// Stateful controls of the settings dialog, created once when it opens.
/// Their values survive re-renders of the dialog content.
struct SettingsControls {
appearance: Entity<SelectState<Vec<SelectOption>>>,
light_theme: Entity<SelectState<Vec<SelectOption>>>,
@@ -66,8 +59,7 @@ struct SettingsControls {
radius: Entity<InputState>,
radius_lg: Entity<InputState>,
grasp_server_input: Entity<InputState>,
/// The effective default create-repository folder, shown in the disabled
/// folder selector.
/// The effective default create-repository folder, shown in the disabled input.
default_folder: Entity<InputState>,
/// Keeps the control subscriptions alive for the dialog's lifetime.
_subscriptions: Vec<Subscription>,
@@ -296,8 +288,8 @@ pub fn open(window: &mut Window, cx: &mut App) {
});
}
/// The settings content: one section per related setting, divided by
/// horizontal separator lines.
/// 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();
@@ -325,8 +317,7 @@ fn appearance_section(controls: &SettingsControls, cx: &App) -> impl IntoElement
))
}
/// Theme configuration: the theme names in the registry plus
/// the visual tweaks the application customizes at startup.
/// 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()
@@ -397,7 +388,7 @@ fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) ->
))
}
/// The default grasp servers offered when the user hasn't published a grasp list (kind `10317`) yet.
/// Default grasp servers offered until the user publishes a kind `10317` grasp list.
fn grasp_servers_section(
settings: &Settings,
controls: &SettingsControls,
@@ -413,8 +404,8 @@ 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.
/// 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],
controls: &SettingsControls,
@@ -478,8 +469,8 @@ fn grasp_server_editor(
)
}
/// The bare host of a grasp server (defaults are entered without a scheme),
/// matching how the publish dialogs display servers.
/// The bare host of a grasp server, defaults are entered without a scheme.
/// Matches how the publish dialogs display servers.
fn display_server(server: &str) -> SharedString {
RelayUrl::parse(server)
.ok()
@@ -513,8 +504,8 @@ fn repositories_section(
))
}
/// The editable list of scan directories plus an add-directory button,
/// styled like the grasp-server list.
/// 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()
.w_full()
@@ -566,8 +557,8 @@ fn scan_paths_editor(scan_paths: &[PathBuf], cx: &App) -> impl IntoElement {
)
}
/// The default-folder selector: a disabled input showing the effective
/// folder plus a picker button, matching the create-repository dialog.
/// 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();
h_flex()
@@ -590,8 +581,8 @@ fn folder_selector(controls: &SettingsControls) -> impl IntoElement {
)
}
/// Parse the server input (accepting a bare host) and append it to the
/// default grasp servers.
/// Parse the server input and append it to the default grasp servers.
/// A bare host is accepted.
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() {
@@ -660,8 +651,8 @@ fn add_scan_path(cx: &mut App) {
.detach();
}
/// Prompt for the folder the Create Repository dialog should default to,
/// remembering it in the settings and showing it in the disabled input.
/// Prompt for the Create Repository dialog's default folder.
/// Remember it in the settings and show 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();
@@ -695,8 +686,9 @@ fn choose_default_folder(default_folder: &Entity<InputState>, window: &mut Windo
.detach();
}
/// Wire a number input to the settings: steps clamp and persist, typed
/// changes parse, clamp and persist.
/// Wire a number input to the settings.
/// Step actions clamp and persist the value.
/// Typed changes parse, clamp and persist.
fn wire_number_input(
state: &Entity<InputState>,
subscriptions: &mut Vec<Subscription>,
+8 -10
View File
@@ -45,10 +45,9 @@ impl Workspace {
let mut subscriptions = vec![];
// A bottom/right dock whose last panel was dragged away is removed
// entirely: base keeps the emptied region, which would otherwise
// linger as a bare strip. Deferred, because the event arrives while
// the area is mid-update.
// A bottom or right dock whose last panel was dragged away is removed entirely.
// The emptied region would otherwise linger as a bare strip.
// The removal is deferred, the event arrives while the area is mid-update.
let dock_for_pruning = dock.clone();
subscriptions.push(cx.subscribe_in(
&dock,
@@ -78,9 +77,8 @@ impl Workspace {
let backend = Backend::global(cx);
// Ask for the passphrase when the stored identity is NIP-49
// encrypted. Subscribed via the window, since opening a dialog
// needs one.
// Ask for the passphrase when the stored identity is NIP-49 encrypted.
// Subscribed via the window, since opening a dialog needs a window.
let passphrase_subscription =
window.subscribe(&backend, cx, |_backend, event, window, cx| {
if matches!(event, BackendEvent::PassphraseRequired) {
@@ -88,9 +86,9 @@ impl Workspace {
}
});
// The event may have fired before this window existed (the backend
// is initialized before the first window opens); fall back to the
// backend state in that case.
// The event may have fired before this window existed.
// The backend is initialized before the first window opens.
// Fall back to the backend state in that case.
if backend.read(cx).passphrase_required() {
passphrase_dialog::open(window, cx);
}